From 01e798a3c9b8084fe4d0bbcb20e87df1f7e7203f Mon Sep 17 00:00:00 2001 From: Heejae Chang <1333179+heejaechang@users.noreply.github.com> Date: Wed, 22 Mar 2023 10:57:15 -0700 Subject: [PATCH 001/336] Share telemetry from core extension. (#20893) it turns out the new telemetry API removed a way to set extension id and version when telemetry reporter is created and it implicitly sets from extension reporter is created. the same way how LSP client is working. since we want to keep using the same extension id and etc for our telemetry, we need the reporter created from core ext. --- src/client/api.ts | 18 ++++-------------- src/client/browser/api.ts | 11 ++++------- src/client/browser/extension.ts | 6 ++++-- src/client/pylanceApi.ts | 26 ++++++++++++++++++++++++++ src/client/telemetry/index.ts | 2 +- 5 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 src/client/pylanceApi.ts diff --git a/src/client/api.ts b/src/client/api.ts index a2f2997c38d0..7ca30b2a6940 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -12,7 +12,6 @@ import { ILanguageServerOutputChannel } from './activation/types'; import { IExtensionApi } from './apiTypes'; import { isTestExecution, PYTHON_LANGUAGE } from './common/constants'; import { IConfigurationService, Resource } from './common/types'; -import { IEnvironmentVariablesProvider } from './common/variables/types'; import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer, IServiceManager } from './ioc/types'; @@ -20,6 +19,8 @@ import { JupyterExtensionIntegration } from './jupyter/jupyterIntegration'; import { traceError } from './logging'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { buildEnvironmentApi } from './environmentApi'; +import { ApiForPylance } from './pylanceApi'; +import { getTelemetryReporter } from './telemetry'; export function buildApi( ready: Promise, @@ -31,7 +32,6 @@ export function buildApi( const interpreterService = serviceContainer.get(IInterpreterService); serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); - const envService = serviceContainer.get(IEnvironmentVariablesProvider); const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); const api: IExtensionApi & { @@ -39,13 +39,7 @@ export function buildApi( * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an * iteration or two. */ - pylance: { - getPythonPathVar: (resource?: Uri) => Promise; - readonly onDidEnvironmentVariablesChange: Event; - createClient(...args: any[]): BaseLanguageClient; - start(client: BaseLanguageClient): Promise; - stop(client: BaseLanguageClient): Promise; - }; + pylance: ApiForPylance; } & { /** * @deprecated Use IExtensionApi.environments API instead. @@ -126,11 +120,6 @@ export function buildApi( : (noop as any), }, pylance: { - getPythonPathVar: async (resource?: Uri) => { - const envs = await envService.getEnvironmentVariables(resource); - return envs.PYTHONPATH; - }, - onDidEnvironmentVariablesChange: envService.onDidEnvironmentVariablesChange, createClient: (...args: any[]): BaseLanguageClient => { // Make sure we share output channel so that we can share one with // Jedi as well. @@ -141,6 +130,7 @@ export function buildApi( }, start: (client: BaseLanguageClient): Promise => client.start(), stop: (client: BaseLanguageClient): Promise => client.stop(), + getTelemetryReporter: () => getTelemetryReporter(), }, environments: buildEnvironmentApi(discoveryApi, serviceContainer), }; diff --git a/src/client/browser/api.ts b/src/client/browser/api.ts index 0536cbadb0bb..ac2df8d0ffed 100644 --- a/src/client/browser/api.ts +++ b/src/client/browser/api.ts @@ -6,21 +6,17 @@ import { BaseLanguageClient } from 'vscode-languageclient'; import { LanguageClient } from 'vscode-languageclient/browser'; import { PYTHON_LANGUAGE } from '../common/constants'; +import { ApiForPylance, TelemetryReporter } from '../pylanceApi'; export interface IBrowserExtensionApi { /** * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an * iteration or two. */ - pylance: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createClient(...args: any[]): BaseLanguageClient; - start(client: BaseLanguageClient): Promise; - stop(client: BaseLanguageClient): Promise; - }; + pylance: ApiForPylance; } -export function buildApi(): IBrowserExtensionApi { +export function buildApi(reporter: TelemetryReporter): IBrowserExtensionApi { const api: IBrowserExtensionApi = { pylance: { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -28,6 +24,7 @@ export function buildApi(): IBrowserExtensionApi { new LanguageClient(PYTHON_LANGUAGE, 'Python Language Server', args[0], args[1]), start: (client: BaseLanguageClient): Promise => client.start(), stop: (client: BaseLanguageClient): Promise => client.stop(), + getTelemetryReporter: () => reporter, }, }; diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts index e1ee59a677e4..6ab6440bbe09 100644 --- a/src/client/browser/extension.ts +++ b/src/client/browser/extension.ts @@ -21,10 +21,12 @@ let languageClient: LanguageClient | undefined; let pylanceApi: PylanceApi | undefined; export async function activate(context: vscode.ExtensionContext): Promise { + const reporter = getTelemetryReporter(); + const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); if (pylanceExtension) { await runPylance(context, pylanceExtension); - return buildApi(); + return buildApi(reporter); } const changeDisposable = vscode.extensions.onDidChange(async () => { @@ -35,7 +37,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { diff --git a/src/client/pylanceApi.ts b/src/client/pylanceApi.ts new file mode 100644 index 000000000000..b839d0d9c2b7 --- /dev/null +++ b/src/client/pylanceApi.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TelemetryEventMeasurements, TelemetryEventProperties } from '@vscode/extension-telemetry'; +import { BaseLanguageClient } from 'vscode-languageclient'; + +export interface TelemetryReporter { + sendTelemetryEvent( + eventName: string, + properties?: TelemetryEventProperties, + measurements?: TelemetryEventMeasurements, + ): void; + sendTelemetryErrorEvent( + eventName: string, + properties?: TelemetryEventProperties, + measurements?: TelemetryEventMeasurements, + ): void; +} + +export interface ApiForPylance { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createClient(...args: any[]): BaseLanguageClient; + start(client: BaseLanguageClient): Promise; + stop(client: BaseLanguageClient): Promise; + getTelemetryReporter(): TelemetryReporter; +} diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index d9091cced48f..74f1ddf0ecf4 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -76,7 +76,7 @@ export function _resetSharedProperties(): void { } let telemetryReporter: TelemetryReporter | undefined; -function getTelemetryReporter() { +export function getTelemetryReporter(): TelemetryReporter { if (!isTestExecution() && telemetryReporter) { return telemetryReporter; } From 61f93c387960a33062d0f40d8a732d4153872cc0 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 22 Mar 2023 14:07:25 -0700 Subject: [PATCH 002/336] Fix title and placeholder for python selection when using create env command (#20896) Closes https://github.com/microsoft/vscode-python/issues/19891 ![image](https://user-images.githubusercontent.com/3840081/227031948-de595993-80e1-43a0-8dd8-658848c6b3d5.png) --- src/client/common/utils/localize.ts | 2 +- .../creation/provider/venvCreationProvider.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index e3a3859840bf..74c367dad223 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -440,7 +440,7 @@ export namespace CreateEnv { export const created = l10n.t('Environment created...'); export const installingPackages = l10n.t('Installing packages...'); export const errorCreatingEnvironment = l10n.t('Error while creating virtual environment.'); - export const selectPythonQuickPickTitle = l10n.t('Select a python to use for environment creation'); + export const selectPythonPlaceHolder = l10n.t('Select a python to create the virtual environment'); export const providerDescription = l10n.t('Creates a `.venv` virtual environment in the current workspace'); export const error = l10n.t('Creating virtual environment failed with error.'); export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml'); diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index 48baa70a745d..cdcb6436643c 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -166,6 +166,8 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { { skipRecommended: true, showBackButton: true, + placeholder: CreateEnv.Venv.selectPythonPlaceHolder, + title: null, }, ); } catch (ex) { From be55c971cc2273243ca6b24ff9bef7e6ea9a09c3 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Thu, 23 Mar 2023 09:17:37 -0700 Subject: [PATCH 003/336] Adjust a few strings that mention Python (#20900) For https://github.com/microsoft/vscode-python/issues/20899 (+ a few other strings I thought could use some tweaking too) --- src/client/common/utils/localize.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 74c367dad223..64198b661d1e 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -206,11 +206,11 @@ export namespace Interpreters { 'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar', ); export const installPythonTerminalMessageLinux = l10n.t( - '💡 Please try installing the python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads', + '💡 Please try installing the Python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads', ); export const installPythonTerminalMacMessage = l10n.t( - '💡 Brew does not seem to be available. Please try to download Python from https://www.python.org/downloads. Alternatively, you can install the python package using some other package manager which is available.', + '💡 Brew does not seem to be available. Please try to download Python from https://www.python.org/downloads. Alternatively, you can install the Python package using some other available package manager.', ); export const changePythonInterpreter = l10n.t('Change Python Interpreter'); export const selectedPythonInterpreter = l10n.t('Selected Python Interpreter'); @@ -440,7 +440,7 @@ export namespace CreateEnv { export const created = l10n.t('Environment created...'); export const installingPackages = l10n.t('Installing packages...'); export const errorCreatingEnvironment = l10n.t('Error while creating virtual environment.'); - export const selectPythonPlaceHolder = l10n.t('Select a python to create the virtual environment'); + export const selectPythonPlaceHolder = l10n.t('Select a Python installation to create the virtual environment'); export const providerDescription = l10n.t('Creates a `.venv` virtual environment in the current workspace'); export const error = l10n.t('Creating virtual environment failed with error.'); export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml'); From 5f9be4eea7e4aafc315e7b4fcdb7cbabc36766db Mon Sep 17 00:00:00 2001 From: Heejae Chang <1333179+heejaechang@users.noreply.github.com> Date: Thu, 23 Mar 2023 21:52:34 -0700 Subject: [PATCH 004/336] Make sure we delay start pylance server (#20910) fixes https://github.com/microsoft/vscode-python/issues/20909 Activating `pylance` extension inside of `python` extension cause a dead lock since they have circular dependency. now we make sure we activate `pylance` once `python` extension is activated. `node` already works this way. it is just browser extension that started `pylance` inside `activate` directly. --- src/client/browser/extension.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts index 6ab6440bbe09..28e1912f67e4 100644 --- a/src/client/browser/extension.ts +++ b/src/client/browser/extension.ts @@ -20,13 +20,15 @@ interface BrowserConfig { let languageClient: LanguageClient | undefined; let pylanceApi: PylanceApi | undefined; -export async function activate(context: vscode.ExtensionContext): Promise { +export function activate(context: vscode.ExtensionContext): Promise { const reporter = getTelemetryReporter(); + const activationPromise = Promise.resolve(buildApi(reporter)); const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); if (pylanceExtension) { - await runPylance(context, pylanceExtension); - return buildApi(reporter); + // Make sure we run pylance once we activated core extension. + activationPromise.then(() => runPylance(context, pylanceExtension)); + return activationPromise; } const changeDisposable = vscode.extensions.onDidChange(async () => { @@ -37,7 +39,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { From 43a21a2348e18eaf615c35857445727123cf2a1b Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 27 Mar 2023 15:41:48 -0700 Subject: [PATCH 005/336] Remove HTTP Client usage (#20922) This is addressing an internal CG issue. --- build/webpack/common.js | 2 - package-lock.json | 697 +++--------------- package.json | 5 +- src/client/common/net/fileDownloader.ts | 106 --- src/client/common/net/httpClient.ts | 74 -- src/client/common/serviceRegistry.ts | 6 - src/client/common/types.ts | 36 - src/test/common/installer.test.ts | 6 - src/test/common/moduleInstaller.test.ts | 6 - .../common/net/fileDownloader.unit.test.ts | 280 ------- src/test/common/net/httpClient.unit.test.ts | 126 ---- src/test/common/serviceRegistry.unit.test.ts | 3 - src/test/performanceTest.ts | 16 +- 13 files changed, 114 insertions(+), 1249 deletions(-) delete mode 100644 src/client/common/net/fileDownloader.ts delete mode 100644 src/client/common/net/httpClient.ts delete mode 100644 src/test/common/net/fileDownloader.unit.test.ts delete mode 100644 src/test/common/net/httpClient.unit.test.ts diff --git a/build/webpack/common.js b/build/webpack/common.js index 5ce66883bb41..d5235db54967 100644 --- a/build/webpack/common.js +++ b/build/webpack/common.js @@ -20,8 +20,6 @@ exports.nodeModulesToExternalize = [ 'unicode/category/Mc', 'unicode/category/Nd', 'unicode/category/Pc', - 'request', - 'request-progress', 'source-map-support', 'diff-match-patch', 'sudo-prompt', diff --git a/package-lock.json b/package-lock.json index 9f05480b28ca..d75ad8b76976 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,8 +26,6 @@ "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", "reflect-metadata": "^0.1.12", - "request": "^2.87.0", - "request-progress": "^3.0.0", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", "semver": "^5.5.0", @@ -50,6 +48,7 @@ }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/bent": "^7.3.0", "@types/chai": "^4.1.2", "@types/chai-arrays": "^2.0.0", "@types/chai-as-promised": "^7.1.0", @@ -62,7 +61,6 @@ "@types/mocha": "^9.1.0", "@types/nock": "^10.0.3", "@types/node": "^14.18.0", - "@types/request": "^2.47.0", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", @@ -76,6 +74,7 @@ "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", "@vscode/test-electron": "^2.1.3", + "bent": "^7.3.12", "chai": "^4.1.2", "chai-arrays": "^2.0.0", "chai-as-promised": "^7.1.1", @@ -129,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.76.0" + "vscode": "^1.77.0-20230309" } }, "node_modules/@azure/abort-controller": { @@ -860,11 +859,14 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, - "node_modules/@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", - "dev": true + "node_modules/@types/bent": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@types/bent/-/bent-7.3.3.tgz", + "integrity": "sha512-5NEIhVzHiZ6wMjFBmJ3gwjxwGug6amMoAn93rtDBttwrODxm+bt63u+MJA7H9NGGM4X1m73sJrAxDapktl036Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/chai": { "version": "4.3.0", @@ -1054,32 +1056,6 @@ "node": ">= 6" } }, - "node_modules/@types/request": { - "version": "2.48.8", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", - "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", - "dev": true, - "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - } - }, - "node_modules/@types/request/node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/@types/semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", @@ -1119,12 +1095,6 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, - "node_modules/@types/tough-cookie": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", - "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==", - "dev": true - }, "node_modules/@types/tunnel": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", @@ -1679,6 +1649,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2119,14 +2090,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, "node_modules/asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", @@ -2148,14 +2111,6 @@ "util": "0.10.3" } }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "engines": { - "node": ">=0.8" - } - }, "node_modules/assert/node_modules/inherits": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", @@ -2295,19 +2250,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, "node_modules/axe-core": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz", @@ -2478,12 +2420,27 @@ } ] }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "node_modules/bent": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", + "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", + "dev": true, "dependencies": { - "tweetnacl": "^0.14.3" + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" + } + }, + "node_modules/bent/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/big-integer": { @@ -2809,6 +2766,12 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "node_modules/bytesish": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", + "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==", + "dev": true + }, "node_modules/cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -2909,7 +2872,8 @@ "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true }, "node_modules/chai": { "version": "4.3.6", @@ -3749,7 +3713,8 @@ "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true }, "node_modules/create-ecdh": { "version": "4.0.3", @@ -3868,17 +3833,6 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -4490,15 +4444,6 @@ "object.defaults": "^1.1.0" } }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.4.92", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.92.tgz", @@ -5799,7 +5744,8 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true }, "node_modules/extend-shallow": { "version": "3.0.2", @@ -5907,14 +5853,6 @@ "node": ">=0.10.0" } }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "engines": [ - "node >=0.6.0" - ] - }, "node_modules/fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", @@ -5933,7 +5871,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-glob": { "version": "3.2.11", @@ -6024,7 +5963,8 @@ "node_modules/fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -6339,27 +6279,6 @@ "node": ">=8" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -6627,14 +6546,6 @@ "node": ">=0.10.0" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -7138,27 +7049,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -7419,20 +7309,6 @@ } } }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, "node_modules/https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -8103,7 +7979,8 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true }, "node_modules/is-unc-path": { "version": "1.0.0", @@ -8185,11 +8062,6 @@ "node": ">=0.10.0" } }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, "node_modules/istanbul-lib-coverage": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", @@ -8646,11 +8518,6 @@ "node": ">=4" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -8675,15 +8542,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -8694,7 +8557,8 @@ "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true }, "node_modules/json5": { "version": "2.2.3", @@ -8713,20 +8577,6 @@ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==" }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/jsx-ast-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", @@ -10507,14 +10357,6 @@ "node": ">=8" } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -11188,11 +11030,6 @@ "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", "dev": true }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -11452,6 +11289,7 @@ "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, "engines": { "node": ">=0.6" } @@ -11836,54 +11674,6 @@ "node": ">= 0.10" } }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", - "dependencies": { - "throttleit": "^1.0.0" - } - }, - "node_modules/request/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -12765,30 +12555,6 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "node_modules/sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stack-chain": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", @@ -13328,11 +13094,6 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "node_modules/throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -13507,18 +13268,6 @@ "node": ">=6" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -13911,6 +13660,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -13918,11 +13668,6 @@ "node": "*" } }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, "node_modules/type": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", @@ -14232,6 +13977,7 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -14364,19 +14110,6 @@ "node": ">= 0.10" } }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/vinyl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", @@ -16025,11 +15758,14 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", - "dev": true + "@types/bent": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@types/bent/-/bent-7.3.3.tgz", + "integrity": "sha512-5NEIhVzHiZ6wMjFBmJ3gwjxwGug6amMoAn93rtDBttwrODxm+bt63u+MJA7H9NGGM4X1m73sJrAxDapktl036Q==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "@types/chai": { "version": "4.3.0", @@ -16218,31 +15954,6 @@ } } }, - "@types/request": { - "version": "2.48.8", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", - "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", - "dev": true, - "requires": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - }, - "dependencies": { - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - } - } - }, "@types/semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", @@ -16282,12 +15993,6 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, - "@types/tough-cookie": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", - "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==", - "dev": true - }, "@types/tunnel": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", @@ -16713,6 +16418,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -17038,14 +16744,6 @@ "es-abstract": "^1.19.0" } }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, "asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", @@ -17084,11 +16782,6 @@ } } }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -17180,16 +16873,6 @@ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, "axe-core": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz", @@ -17327,12 +17010,23 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "bent": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", + "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", + "dev": true, "requires": { - "tweetnacl": "^0.14.3" + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + } } }, "big-integer": { @@ -17605,6 +17299,12 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "bytesish": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", + "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==", + "dev": true + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -17682,7 +17382,8 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true }, "chai": { "version": "4.3.6", @@ -18346,7 +18047,8 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true }, "create-ecdh": { "version": "4.0.3", @@ -18455,14 +18157,6 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -18964,15 +18658,6 @@ "object.defaults": "^1.1.0" } }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "electron-to-chromium": { "version": "1.4.92", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.92.tgz", @@ -19983,7 +19668,8 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true }, "extend-shallow": { "version": "3.0.2", @@ -20071,11 +19757,6 @@ } } }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, "fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", @@ -20091,7 +19772,8 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "fast-glob": { "version": "3.2.11", @@ -20163,7 +19845,8 @@ "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true }, "fast-levenshtein": { "version": "2.0.6", @@ -20410,21 +20093,6 @@ } } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -20638,14 +20306,6 @@ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -21056,20 +20716,6 @@ "duplexer": "^0.1.2" } }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -21271,16 +20917,6 @@ } } }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -21755,7 +21391,8 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true }, "is-unc-path": { "version": "1.0.0", @@ -21816,11 +21453,6 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, "istanbul-lib-coverage": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", @@ -22196,11 +21828,6 @@ } } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -22219,15 +21846,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, - "json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -22238,7 +21861,8 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true }, "json5": { "version": "2.2.3", @@ -22251,17 +21875,6 @@ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==" }, - "jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, "jsx-ast-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", @@ -23684,11 +23297,6 @@ } } }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -24208,11 +23816,6 @@ "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", "dev": true }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -24415,7 +24018,8 @@ "qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true }, "query-string": { "version": "5.1.1", @@ -24712,48 +24316,6 @@ "remove-trailing-separator": "^1.1.0" } }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - } - } - }, - "request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", - "requires": { - "throttleit": "^1.0.0" - } - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -25442,22 +25004,6 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, "stack-chain": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", @@ -25880,11 +25426,6 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" - }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -26025,15 +25566,6 @@ "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", "dev": true }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -26319,15 +25851,11 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, "requires": { "safe-buffer": "^5.0.1" } }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, "type": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", @@ -26581,6 +26109,7 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, "requires": { "punycode": "^2.1.0" } @@ -26701,16 +26230,6 @@ "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", "dev": true }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "vinyl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", diff --git a/package.json b/package.json index cb06f5e6a519..08f071da591d 100644 --- a/package.json +++ b/package.json @@ -1843,8 +1843,6 @@ "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", "reflect-metadata": "^0.1.12", - "request": "^2.87.0", - "request-progress": "^3.0.0", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", "semver": "^5.5.0", @@ -1867,6 +1865,7 @@ }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/bent": "^7.3.0", "@types/chai": "^4.1.2", "@types/chai-arrays": "^2.0.0", "@types/chai-as-promised": "^7.1.0", @@ -1879,7 +1878,6 @@ "@types/mocha": "^9.1.0", "@types/nock": "^10.0.3", "@types/node": "^14.18.0", - "@types/request": "^2.47.0", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", @@ -1893,6 +1891,7 @@ "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", "@vscode/test-electron": "^2.1.3", + "bent": "^7.3.12", "chai": "^4.1.2", "chai-arrays": "^2.0.0", "chai-as-promised": "^7.1.1", diff --git a/src/client/common/net/fileDownloader.ts b/src/client/common/net/fileDownloader.ts deleted file mode 100644 index 6ddd06bcc940..000000000000 --- a/src/client/common/net/fileDownloader.ts +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as requestTypes from 'request'; -import { l10n, Progress } from 'vscode'; -import { traceLog } from '../../logging'; -import { IApplicationShell } from '../application/types'; -import { Octicons } from '../constants'; -import { IFileSystem, WriteStream } from '../platform/types'; -import { DownloadOptions, IFileDownloader, IHttpClient } from '../types'; -import { noop } from '../utils/misc'; - -@injectable() -export class FileDownloader implements IFileDownloader { - constructor( - @inject(IHttpClient) private readonly httpClient: IHttpClient, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - ) {} - public async downloadFile(uri: string, options: DownloadOptions): Promise { - traceLog(l10n.t('Downloading {0}...', uri)); - const tempFile = await this.fs.createTemporaryFile(options.extension); - - await this.downloadFileWithStatusBarProgress(uri, options.progressMessagePrefix, tempFile.filePath).then( - noop, - (ex) => { - tempFile.dispose(); - return Promise.reject(ex); - }, - ); - - return tempFile.filePath; - } - public async downloadFileWithStatusBarProgress( - uri: string, - progressMessage: string, - tmpFilePath: string, - ): Promise { - await this.appShell.withProgressCustomIcon(Octicons.Downloading, async (progress) => { - const req = await this.httpClient.downloadFile(uri); - const fileStream = this.fs.createWriteStream(tmpFilePath); - return this.displayDownloadProgress(uri, progress, req, fileStream, progressMessage); - }); - } - - public async displayDownloadProgress( - uri: string, - progress: Progress<{ message?: string; increment?: number }>, - request: requestTypes.Request, - fileStream: WriteStream, - progressMessagePrefix: string, - ): Promise { - return new Promise((resolve, reject) => { - request.on('response', (response) => { - if (response.statusCode !== 200) { - reject( - new Error(`Failed with status ${response.statusCode}, ${response.statusMessage}, Uri ${uri}`), - ); - } - }); - - const requestProgress = require('request-progress'); - requestProgress(request) - .on('progress', (state: RequestProgressState) => { - const message = formatProgressMessageWithState(progressMessagePrefix, state); - progress.report({ message }); - }) - // Handle errors from download. - .on('error', reject) - .pipe(fileStream) - // Handle error in writing to fs. - .on('error', reject) - .on('close', resolve); - }); - } -} - -type RequestProgressState = { - percent: number; - speed: number; - size: { - total: number; - transferred: number; - }; - time: { - elapsed: number; - remaining: number; - }; -}; - -function formatProgressMessageWithState(progressMessagePrefix: string, state: RequestProgressState): string { - const received = Math.round(state.size.transferred / 1024); - const total = Math.round(state.size.total / 1024); - const percentage = Math.round(100 * state.percent); - - return l10n.t( - '{0}{1} of {2} KB ({3}%)', - progressMessagePrefix, - received.toString(), - total.toString(), - percentage.toString(), - ); -} diff --git a/src/client/common/net/httpClient.ts b/src/client/common/net/httpClient.ts deleted file mode 100644 index 8aac63d17142..000000000000 --- a/src/client/common/net/httpClient.ts +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { parse, ParseError } from 'jsonc-parser'; -import type * as requestTypes from 'request'; -import { IHttpClient } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { traceError } from '../../logging'; -import { IWorkspaceService } from '../application/types'; - -@injectable() -export class HttpClient implements IHttpClient { - public readonly requestOptions: requestTypes.CoreOptions; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - const workspaceService = serviceContainer.get(IWorkspaceService); - this.requestOptions = { proxy: workspaceService.getConfiguration('http').get('proxy', '') }; - } - - public async downloadFile(uri: string): Promise { - const request = ((await import('request')) as any) as typeof requestTypes; - return request(uri, this.requestOptions); - } - - public async getJSON(uri: string, strict: boolean = true): Promise { - const body = await this.getContents(uri); - return this.parseBodyToJSON(body, strict); - } - - public async parseBodyToJSON(body: string, strict: boolean): Promise { - if (strict) { - return JSON.parse(body); - } else { - let errors: ParseError[] = []; - const content = parse(body, errors, { allowTrailingComma: true, disallowComments: false }) as T; - if (errors.length > 0) { - traceError('JSONC parser returned ParseError codes', errors); - } - return content; - } - } - - public async exists(uri: string): Promise { - const request = require('request') as typeof requestTypes; - return new Promise((resolve) => { - try { - request - .get(uri, this.requestOptions) - .on('response', (response) => resolve(response.statusCode === 200)) - .on('error', () => resolve(false)); - } catch { - resolve(false); - } - }); - } - private async getContents(uri: string): Promise { - const request = require('request') as typeof requestTypes; - return new Promise((resolve, reject) => { - request(uri, this.requestOptions, (ex, response, body) => { - if (ex) { - return reject(ex); - } - if (response.statusCode !== 200) { - return reject( - new Error(`Failed with status ${response.statusCode}, ${response.statusMessage}, Uri ${uri}`), - ); - } - resolve(body); - }); - }); - } -} diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index b68f56042c1f..93f069f18288 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -8,8 +8,6 @@ import { IEditorUtils, IExperimentService, IExtensions, - IFileDownloader, - IHttpClient, IInstaller, IInterpreterPathService, IPathUtils, @@ -58,8 +56,6 @@ import { ExperimentService } from './experiments/service'; import { ProductInstaller } from './installer/productInstaller'; import { InterpreterPathService } from './interpreterPathService'; import { BrowserService } from './net/browser'; -import { FileDownloader } from './net/fileDownloader'; -import { HttpClient } from './net/httpClient'; import { PersistentStateFactory } from './persistentState'; import { IS_WINDOWS } from './platform/constants'; import { PathUtils } from './platform/pathUtils'; @@ -128,8 +124,6 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); serviceManager.addSingleton(ILanguageService, LanguageService); serviceManager.addSingleton(IBrowserService, BrowserService); - serviceManager.addSingleton(IHttpClient, HttpClient); - serviceManager.addSingleton(IFileDownloader, FileDownloader); serviceManager.addSingleton(IEditorUtils, EditorUtils); serviceManager.addSingleton(ITerminalActivator, TerminalActivator); serviceManager.addSingleton( diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 8f340c3e01e2..3fac5e7e0044 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -4,7 +4,6 @@ 'use strict'; import { Socket } from 'net'; -import { Request as RequestResult } from 'request'; import { CancellationToken, ConfigurationChangeEvent, @@ -360,41 +359,6 @@ export type DownloadOptions = { extension: 'tmp' | string; }; -export const IFileDownloader = Symbol('IFileDownloader'); -/** - * File downloader, that'll display progress in the status bar. - * - * @export - * @interface IFileDownloader - */ -export interface IFileDownloader { - /** - * Download file and display progress in statusbar. - * Optionnally display progress in the provided output channel. - * - * @param {string} uri - * @param {DownloadOptions} options - * @returns {Promise} - * @memberof IFileDownloader - */ - downloadFile(uri: string, options: DownloadOptions): Promise; -} - -export const IHttpClient = Symbol('IHttpClient'); -export interface IHttpClient { - downloadFile(uri: string): Promise; - /** - * Downloads file from uri as string and parses them into JSON objects - * @param uri The uri to download the JSON from - * @param strict Set `false` to allow trailing comma and comments in the JSON, defaults to `true` - */ - getJSON(uri: string, strict?: boolean): Promise; - /** - * Returns the url is valid (i.e. return status code of 200). - */ - exists(uri: string): Promise; -} - export const IExtensionContext = Symbol('ExtensionContext'); export interface IExtensionContext extends ExtensionContext {} diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 6c1a6383c2b6..daea3e44ec76 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -42,8 +42,6 @@ import { } from '../../client/common/installer/types'; import { InterpreterPathService } from '../../client/common/interpreterPathService'; import { BrowserService } from '../../client/common/net/browser'; -import { FileDownloader } from '../../client/common/net/fileDownloader'; -import { HttpClient } from '../../client/common/net/httpClient'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { PathUtils } from '../../client/common/platform/pathUtils'; import { CurrentProcess } from '../../client/common/process/currentProcess'; @@ -78,8 +76,6 @@ import { IEditorUtils, IExperimentService, IExtensions, - IFileDownloader, - IHttpClient, IInstaller, IInterpreterPathService, IPathUtils, @@ -199,8 +195,6 @@ suite('Installer', () => { ioc.serviceManager.addSingleton(IDebugService, DebugService); ioc.serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); ioc.serviceManager.addSingleton(IBrowserService, BrowserService); - ioc.serviceManager.addSingleton(IHttpClient, HttpClient); - ioc.serviceManager.addSingleton(IFileDownloader, FileDownloader); ioc.serviceManager.addSingleton(IEditorUtils, EditorUtils); ioc.serviceManager.addSingleton(ITerminalActivator, TerminalActivator); ioc.serviceManager.addSingleton( diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index 302587902c16..fe4937f363ec 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -38,8 +38,6 @@ import { ProductInstaller } from '../../client/common/installer/productInstaller import { IModuleInstaller } from '../../client/common/installer/types'; import { InterpreterPathService } from '../../client/common/interpreterPathService'; import { BrowserService } from '../../client/common/net/browser'; -import { FileDownloader } from '../../client/common/net/fileDownloader'; -import { HttpClient } from '../../client/common/net/httpClient'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { FileSystem } from '../../client/common/platform/fileSystem'; import { PathUtils } from '../../client/common/platform/pathUtils'; @@ -77,8 +75,6 @@ import { IEditorUtils, IExperimentService, IExtensions, - IFileDownloader, - IHttpClient, IInstaller, IInterpreterPathService, IPathUtils, @@ -211,8 +207,6 @@ suite('Module Installer', () => { JupyterExtensionDependencyManager, ); ioc.serviceManager.addSingleton(IBrowserService, BrowserService); - ioc.serviceManager.addSingleton(IHttpClient, HttpClient); - ioc.serviceManager.addSingleton(IFileDownloader, FileDownloader); ioc.serviceManager.addSingleton(IEditorUtils, EditorUtils); ioc.serviceManager.addSingleton(ITerminalActivator, TerminalActivator); ioc.serviceManager.addSingleton( diff --git a/src/test/common/net/fileDownloader.unit.test.ts b/src/test/common/net/fileDownloader.unit.test.ts deleted file mode 100644 index 515ed488dc2e..000000000000 --- a/src/test/common/net/fileDownloader.unit.test.ts +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { expect } from 'chai'; -import * as fsExtra from 'fs-extra'; -import * as nock from 'nock'; -import * as path from 'path'; -import rewiremock from 'rewiremock'; -import * as sinon from 'sinon'; -import { Readable, Writable } from 'stream'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Progress } from 'vscode'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { IApplicationShell } from '../../../client/common/application/types'; -import { FileDownloader } from '../../../client/common/net/fileDownloader'; -import { HttpClient } from '../../../client/common/net/httpClient'; -import { FileSystem } from '../../../client/common/platform/fileSystem'; -import { PlatformService } from '../../../client/common/platform/platformService'; -import { IFileSystem } from '../../../client/common/platform/types'; -import { IHttpClient } from '../../../client/common/types'; -import { EXTENSION_ROOT_DIR } from '../../../client/constants'; -import * as logging from '../../../client/logging'; -import { noop } from '../../core'; - -const requestProgress = require('request-progress'); -const request = require('request'); - -type ProgressReporterData = { message?: string; increment?: number }; - -/** - * Writable stream that'll throw an error when written to. - * (used to mimick errors thrown when writing to a file). - * - * @class ErroringMemoryStream - * @extends {Writable} - */ -class ErroringMemoryStream extends Writable { - constructor(private readonly errorMessage: string) { - super(); - } - public _write(_chunk: any, _encoding: any, callback: any) { - super.emit('error', new Error(this.errorMessage)); - return callback(); - } -} -/** - * Readable stream that's slow to return data. - * (used to mimic slow file downloads). - * - * @class DelayedReadMemoryStream - * @extends {Readable} - */ -class DelayedReadMemoryStream extends Readable { - private readCounter = 0; - constructor( - private readonly totalKb: number, - private readonly delayMs: number, - private readonly kbPerIteration: number, - ) { - super(); - } - // @ts-ignore https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-rc/#properties-overridding-accessors-and-vice-versa-is-an-error - public get readableLength() { - return 1024 * 10; - } - public _read() { - // Delay reading data, mimicking slow file downloads. - setTimeout(() => this.sendMessage(), this.delayMs); - } - public sendMessage() { - const i = (this.readCounter += 1); - if (i > this.totalKb / this.kbPerIteration) { - this.push(null); - } else { - this.push(Buffer.from('a'.repeat(this.kbPerIteration), 'ascii')); - } - } -} - -suite('File Downloader', () => { - let fileDownloader: FileDownloader; - let httpClient: IHttpClient; - let fs: IFileSystem; - let appShell: IApplicationShell; - suiteTeardown(() => { - rewiremock.disable(); - sinon.restore(); - }); - suite('File Downloader (real)', () => { - const uri = 'https://python.extension/package.json'; - const packageJsonFile = path.join(EXTENSION_ROOT_DIR, 'package.json'); - setup(() => { - rewiremock.disable(); - httpClient = mock(HttpClient); - appShell = mock(ApplicationShell); - when(httpClient.downloadFile(anything())).thenCall(request); - fs = new FileSystem(); - }); - teardown(() => { - rewiremock.disable(); - sinon.restore(); - }); - test('File gets downloaded', async () => { - // When downloading a uri, point it to package.json file. - nock('https://python.extension') - .get('/package.json') - .reply(200, () => fsExtra.createReadStream(packageJsonFile)); - const progressReportStub = sinon.stub(); - const progressReporter: Progress = { report: progressReportStub }; - const tmpFilePath = await fs.createTemporaryFile('.json'); - when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); - - fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); - await fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', tmpFilePath.filePath); - - // Confirm the package.json file gets downloaded - const expectedFileContents = fsExtra.readFileSync(packageJsonFile).toString(); - assert.strictEqual(fsExtra.readFileSync(tmpFilePath.filePath).toString(), expectedFileContents); - }); - test('Error is throw for http Status !== 200', async () => { - // When downloading a uri, throw status 500 error. - nock('https://python.extension').get('/package.json').reply(500); - const progressReportStub = sinon.stub(); - const progressReporter: Progress = { report: progressReportStub }; - when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); - const tmpFilePath = await fs.createTemporaryFile('.json'); - - fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); - const promise = fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', tmpFilePath.filePath); - - await expect(promise).to.eventually.be.rejectedWith( - 'Failed with status 500, null, Uri https://python.extension/package.json', - ); - }); - test('Error is throw if unable to write to the file stream', async () => { - // When downloading a uri, point it to package.json file. - nock('https://python.extension') - .get('/package.json') - .reply(200, () => fsExtra.createReadStream(packageJsonFile)); - const progressReportStub = sinon.stub(); - const progressReporter: Progress = { report: progressReportStub }; - when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); - - // Use bogus files that cannot be created (on windows, invalid drives, on mac & linux use invalid home directories). - const invalidFileName = new PlatformService().isWindows - ? 'abcd:/bogusFile/one.txt' - : '/bogus file path/.txt'; - fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); - const promise = fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', invalidFileName); - - // Things should fall over. - await expect(promise).to.eventually.be.rejected; - }); - test('Error is throw if file stream throws an error', async () => { - // When downloading a uri, point it to package.json file. - nock('https://python.extension') - .get('/package.json') - .reply(200, () => fsExtra.createReadStream(packageJsonFile)); - const progressReportStub = sinon.stub(); - const progressReporter: Progress = { report: progressReportStub }; - when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); - // Create a file stream that will throw an error when written to (use ErroringMemoryStream). - const tmpFilePath = 'bogus file'; - const fileSystem = mock(FileSystem); - const fileStream = new ErroringMemoryStream('kaboom from fs'); - when(fileSystem.createWriteStream(tmpFilePath)).thenReturn(fileStream as any); - - fileDownloader = new FileDownloader(instance(httpClient), instance(fileSystem), instance(appShell)); - const promise = fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', tmpFilePath); - - // Confirm error from FS is bubbled up. - await expect(promise).to.eventually.be.rejectedWith('kaboom from fs'); - }); - test('Report progress as file gets downloaded', async () => { - const totalKb = 50; - // When downloading a uri, point it to stream that's slow. - // We'll return data from this stream slowly, mimicking a slow download. - // When the download is slow, we can test progress. - nock('https://python.extension') - .get('/package.json') - .reply(200, () => [ - 200, - new DelayedReadMemoryStream(1024 * totalKb, 5, 1024 * 10), - { 'content-length': 1024 * totalKb }, - ]); - const progressReportStub = sinon.stub(); - const progressReporter: Progress = { report: progressReportStub }; - when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); - const tmpFilePath = await fs.createTemporaryFile('.json'); - // Mock request-progress to throttle 1ms, so we can get progress messages. - // I.e. report progress every 1ms. (however since download is delayed to 10ms, - // we'll get progress reported every 10ms. We use 1ms, to ensure its guaranteed - // to be reported. Else changing it to 10ms could result in it being reported in 12ms - rewiremock.enable(); - rewiremock('request-progress').with((reqUri: string) => requestProgress(reqUri, { throttle: 1 })); - - fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); - await fileDownloader.downloadFileWithStatusBarProgress(uri, 'Downloading-something', tmpFilePath.filePath); - - // Since we are throttling the progress notifications for ever 1ms, - // and we're delaying downloading by every 10ms, we'll have progress reported for every 10ms. - // So we'll have progress reported for every 10kb of data downloaded, for a total of 5 times. - expect(progressReportStub.callCount).to.equal(5); - expect(progressReportStub.args[0][0].message).to.equal(getProgressMessage(10, 20)); - expect(progressReportStub.args[1][0].message).to.equal(getProgressMessage(20, 40)); - expect(progressReportStub.args[2][0].message).to.equal(getProgressMessage(30, 60)); - expect(progressReportStub.args[3][0].message).to.equal(getProgressMessage(40, 80)); - expect(progressReportStub.args[4][0].message).to.equal(getProgressMessage(50, 100)); - - function getProgressMessage(downloadedKb: number, percentage: number) { - return `Downloading-something${downloadedKb.toFixed()} of ${totalKb.toFixed()} KB (${percentage.toString()}%)`; - } - }); - }); - suite('File Downloader (mocks)', () => { - let downloadWithProgressStub: sinon.SinonStub; - let traceLogStub: sinon.SinonStub; - setup(() => { - traceLogStub = sinon.stub(logging, 'traceLog'); - httpClient = mock(HttpClient); - fs = mock(FileSystem); - appShell = mock(ApplicationShell); - downloadWithProgressStub = sinon.stub(FileDownloader.prototype, 'displayDownloadProgress'); - downloadWithProgressStub.callsFake(() => Promise.resolve()); - }); - teardown(() => { - sinon.restore(); - }); - test('Create temporary file and return path to that file', async () => { - const tmpFile = { filePath: 'my temp file', dispose: noop }; - when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); - fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); - - const file = await fileDownloader.downloadFile('file', { progressMessagePrefix: '', extension: '.pdf' }); - - verify(fs.createTemporaryFile('.pdf')).once(); - assert.strictEqual(file, 'my temp file'); - }); - test('Display progress message in output channel', async () => { - const tmpFile = { filePath: 'my temp file', dispose: noop }; - when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); - fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); - - await fileDownloader.downloadFile('file to download', { - progressMessagePrefix: '', - extension: '.pdf', - }); - - traceLogStub.calledWithExactly('Downloading file to download...'); - }); - test('Display progress when downloading', async () => { - const tmpFile = { filePath: 'my temp file', dispose: noop }; - when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); - const statusBarProgressStub = sinon.stub(FileDownloader.prototype, 'downloadFileWithStatusBarProgress'); - statusBarProgressStub.callsFake(() => Promise.resolve()); - fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); - - await fileDownloader.downloadFile('file', { progressMessagePrefix: '', extension: '.pdf' }); - - assert.ok(statusBarProgressStub.calledOnce); - }); - test('Dispose temp file and bubble error thrown by status progress', async () => { - const disposeStub = sinon.stub(); - const tmpFile = { filePath: 'my temp file', dispose: disposeStub }; - when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); - const statusBarProgressStub = sinon.stub(FileDownloader.prototype, 'downloadFileWithStatusBarProgress'); - statusBarProgressStub.callsFake(() => Promise.reject(new Error('kaboom'))); - fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); - - const promise = fileDownloader.downloadFile('file', { progressMessagePrefix: '', extension: '.pdf' }); - - await expect(promise).to.eventually.be.rejectedWith('kaboom'); - assert.ok(statusBarProgressStub.calledOnce); - assert.ok(disposeStub.calledOnce); - }); - }); -}); diff --git a/src/test/common/net/httpClient.unit.test.ts b/src/test/common/net/httpClient.unit.test.ts deleted file mode 100644 index 9679b8eb71d1..000000000000 --- a/src/test/common/net/httpClient.unit.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { expect } from 'chai'; - -import rewiremock from 'rewiremock'; -import * as TypeMoq from 'typemoq'; -import { WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../../client/common/application/types'; -import { HttpClient } from '../../../client/common/net/httpClient'; -import { IServiceContainer } from '../../../client/ioc/types'; - -suite('Http Client', () => { - const proxy = 'https://myproxy.net:4242'; - let config: TypeMoq.IMock; - let workSpaceService: TypeMoq.IMock; - let container: TypeMoq.IMock; - let httpClient: HttpClient; - setup(() => { - container = TypeMoq.Mock.ofType(); - workSpaceService = TypeMoq.Mock.ofType(); - config = TypeMoq.Mock.ofType(); - config - .setup((c) => c.get(TypeMoq.It.isValue('proxy'), TypeMoq.It.isValue(''))) - .returns(() => proxy) - .verifiable(TypeMoq.Times.once()); - workSpaceService - .setup((w) => w.getConfiguration(TypeMoq.It.isValue('http'))) - .returns(() => config.object) - .verifiable(TypeMoq.Times.once()); - container.setup((a) => a.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workSpaceService.object); - - httpClient = new HttpClient(container.object); - }); - test('Get proxy info', async () => { - expect(httpClient.requestOptions).to.deep.equal({ proxy: proxy }); - config.verifyAll(); - workSpaceService.verifyAll(); - }); - suite('Test getJSON()', async () => { - teardown(() => { - rewiremock.disable(); - }); - [ - { - name: 'Throw error if request returns with download error', - returnedArgs: ['downloadError', { statusCode: 201 }, undefined], - expectedErrorMessage: 'downloadError', - }, - { - name: 'Throw error if request does not return with status code 200', - returnedArgs: [undefined, { statusCode: 201, statusMessage: 'wrongStatus' }, undefined], - expectedErrorMessage: 'Failed with status 201, wrongStatus, Uri downloadUri', - }, - { - name: 'If strict is set to true, and parsing fails, throw error', - returnedArgs: [undefined, { statusCode: 200 }, '[{ "strictJSON" : true,, }]'], - strict: true, - }, - ].forEach(async (testParams) => { - test(testParams.name, async () => { - const requestMock = (_uri: any, _requestOptions: any, callBackFn: Function) => - callBackFn(...testParams.returnedArgs); - rewiremock.enable(); - rewiremock('request').with(requestMock); - let rejected = true; - try { - await httpClient.getJSON('downloadUri', testParams.strict); - rejected = false; - } catch (ex) { - if (testParams.expectedErrorMessage) { - const error = ex as Error; - // Compare error messages - if (error.message) { - ex = error.message; - } - expect(ex).to.equal( - testParams.expectedErrorMessage, - 'Promise rejected with the wrong error message', - ); - } - } - assert(rejected === true, 'Promise should be rejected'); - }); - }); - - [ - { - name: - "If strict is set to false, and jsonc parsing returns error codes, then log errors and don't throw, return json", - returnedArgs: [undefined, { statusCode: 200 }, '[{ "strictJSON" : false,, }]'], - strict: false, - expectedJSON: [{ strictJSON: false }], - }, - { - name: 'Return expected json if strict is set to true and parsing is successful', - returnedArgs: [undefined, { statusCode: 200 }, '[{ "strictJSON" : true }]'], - strict: true, - expectedJSON: [{ strictJSON: true }], - }, - { - name: 'Return expected json if strict is set to false and parsing is successful', - returnedArgs: [undefined, { statusCode: 200 }, '[{ //Comment \n "strictJSON" : false }]'], - strict: false, - expectedJSON: [{ strictJSON: false }], - }, - ].forEach(async (testParams) => { - test(testParams.name, async () => { - const requestMock = (_uri: any, _requestOptions: any, callBackFn: Function) => - callBackFn(...testParams.returnedArgs); - rewiremock.enable(); - rewiremock('request').with(requestMock); - let json; - try { - json = await httpClient.getJSON('downloadUri', testParams.strict); - } catch (ex) { - assert(false, 'Promise should not be rejected'); - } - assert.deepEqual(json, testParams.expectedJSON, 'Unexpected JSON returned'); - }); - }); - }); -}); diff --git a/src/test/common/serviceRegistry.unit.test.ts b/src/test/common/serviceRegistry.unit.test.ts index 1bb6f30705fd..5d764aac2536 100644 --- a/src/test/common/serviceRegistry.unit.test.ts +++ b/src/test/common/serviceRegistry.unit.test.ts @@ -32,7 +32,6 @@ import { EditorUtils } from '../../client/common/editor'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; import { InterpreterPathService } from '../../client/common/interpreterPathService'; import { BrowserService } from '../../client/common/net/browser'; -import { HttpClient } from '../../client/common/net/httpClient'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { PathUtils } from '../../client/common/platform/pathUtils'; import { CurrentProcess } from '../../client/common/process/currentProcess'; @@ -65,7 +64,6 @@ import { ICurrentProcess, IEditorUtils, IExtensions, - IHttpClient, IInstaller, IInterpreterPathService, IPathUtils, @@ -104,7 +102,6 @@ suite('Common - Service Registry', () => { [IApplicationEnvironment, ApplicationEnvironment], [ILanguageService, LanguageService], [IBrowserService, BrowserService], - [IHttpClient, HttpClient], [IEditorUtils, EditorUtils], [ITerminalActivator, TerminalActivator], [ITerminalActivationHandler, PowershellTerminalActivationFailedHandler], diff --git a/src/test/performanceTest.ts b/src/test/performanceTest.ts index b4e66397e849..d4ac6bf262d0 100644 --- a/src/test/performanceTest.ts +++ b/src/test/performanceTest.ts @@ -19,7 +19,7 @@ import { spawn } from 'child_process'; import * as download from 'download'; import * as fs from 'fs-extra'; import * as path from 'path'; -import * as request from 'request'; +import * as bent from 'bent'; import { LanguageServerType } from '../client/activation/types'; import { EXTENSION_ROOT_DIR, PVSC_EXTENSION_ID } from '../client/common/constants'; import { unzip } from './common'; @@ -123,17 +123,9 @@ class TestRunner { private async getReleaseVersion(): Promise { const url = `https://marketplace.visualstudio.com/items?itemName=${PVSC_EXTENSION_ID}`; - const content = await new Promise((resolve, reject) => { - request(url, (error, response, body) => { - if (error) { - return reject(error); - } - if (response.statusCode === 200) { - return resolve(body); - } - reject(`Status code of ${response.statusCode} received.`); - }); - }); + const request = bent('string', 'GET', 200); + + const content: string = await request(url); const re = NamedRegexp('"version"S?:S?"(:\\d{4}\\.\\d{1,2}\\.\\d{1,2})"', 'g'); const matches = re.exec(content); return matches.groups().version; From d809af4c0ab45eb8e8c7ffd349e6161ea196fb6b Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 27 Mar 2023 16:46:41 -0700 Subject: [PATCH 006/336] Use new formatter install prompt on missing formatter (#20904) --- src/client/formatters/baseFormatter.ts | 18 +++++++++++------- .../prompts/installFormatterPrompt.ts | 18 +++++++++++------- src/client/providers/prompts/types.ts | 5 ++++- src/client/providers/serviceRegistry.ts | 3 +++ 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts index b91a6ac85def..bffa03a8b1e1 100644 --- a/src/client/formatters/baseFormatter.ts +++ b/src/client/formatters/baseFormatter.ts @@ -11,6 +11,7 @@ import { IServiceContainer } from '../ioc/types'; import { traceError, traceLog } from '../logging'; import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor'; import { IFormatterHelper } from './types'; +import { IInstallFormatterPrompt } from '../providers/prompts/types'; export abstract class BaseFormatter { protected readonly workspace: IWorkspaceService; @@ -103,13 +104,16 @@ export abstract class BaseFormatter { let customError = `Formatting with ${this.Id} failed.`; if (isNotInstalledError(error)) { - const installer = this.serviceContainer.get(IInstaller); - const isInstalled = await installer.isInstalled(this.product, resource); - if (!isInstalled) { - customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`; - installer - .promptToInstall(this.product, resource) - .catch((ex) => traceError('Python Extension: promptToInstall', ex)); + const prompt = this.serviceContainer.get(IInstallFormatterPrompt); + if (!(await prompt.showInstallFormatterPrompt(resource))) { + const installer = this.serviceContainer.get(IInstaller); + const isInstalled = await installer.isInstalled(this.product, resource); + if (!isInstalled) { + customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`; + installer + .promptToInstall(this.product, resource) + .catch((ex) => traceError('Python Extension: promptToInstall', ex)); + } } } diff --git a/src/client/providers/prompts/installFormatterPrompt.ts b/src/client/providers/prompts/installFormatterPrompt.ts index db23e130d1fd..5e9ff7f818ef 100644 --- a/src/client/providers/prompts/installFormatterPrompt.ts +++ b/src/client/providers/prompts/installFormatterPrompt.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { Uri } from 'vscode'; +import { inject, injectable } from 'inversify'; import { IDisposableRegistry } from '../../common/types'; import { Common, ToolsExtensions } from '../../common/utils/localize'; import { isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; @@ -18,31 +19,32 @@ import { AUTOPEP8_EXTENSION, BLACK_EXTENSION, IInstallFormatterPrompt } from './ const SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY = 'showFormatterExtensionInstallPrompt'; +@injectable() export class InstallFormatterPrompt implements IInstallFormatterPrompt { private shownThisSession = false; - constructor(private readonly serviceContainer: IServiceContainer) {} + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} - public async showInstallFormatterPrompt(resource?: Uri): Promise { + public async showInstallFormatterPrompt(resource?: Uri): Promise { if (!inFormatterExtensionExperiment(this.serviceContainer)) { - return; + return false; } const promptState = doNotShowPromptState(SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY, this.serviceContainer); if (this.shownThisSession || promptState.value) { - return; + return false; } const config = getConfiguration('python', resource); const formatter = config.get('formatting.provider', 'none'); if (!['autopep8', 'black'].includes(formatter)) { - return; + return false; } const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' }); const defaultFormatter = editorConfig.get('defaultFormatter', ''); if ([BLACK_EXTENSION, AUTOPEP8_EXTENSION].includes(defaultFormatter)) { - return; + return false; } const black = isExtensionEnabled(BLACK_EXTENSION); @@ -111,12 +113,14 @@ export class InstallFormatterPrompt implements IInstallFormatterPrompt { } else if (selection === Common.doNotShowAgain) { await promptState.updateValue(true); } + + return this.shownThisSession; } } export function registerInstallFormatterPrompt(serviceContainer: IServiceContainer): void { const disposables = serviceContainer.get(IDisposableRegistry); - const installFormatterPrompt = new InstallFormatterPrompt(serviceContainer); + const installFormatterPrompt = serviceContainer.get(IInstallFormatterPrompt); disposables.push( onDidSaveTextDocument(async (e) => { const editorConfig = getConfiguration('editor', { uri: e.uri, languageId: 'python' }); diff --git a/src/client/providers/prompts/types.ts b/src/client/providers/prompts/types.ts index 47fead687cf5..4edaadb46b46 100644 --- a/src/client/providers/prompts/types.ts +++ b/src/client/providers/prompts/types.ts @@ -1,9 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { Uri } from 'vscode'; + export const BLACK_EXTENSION = 'ms-python.black-formatter'; export const AUTOPEP8_EXTENSION = 'ms-python.autopep8'; +export const IInstallFormatterPrompt = Symbol('IInstallFormatterPrompt'); export interface IInstallFormatterPrompt { - showInstallFormatterPrompt(): Promise; + showInstallFormatterPrompt(resource?: Uri): Promise; } diff --git a/src/client/providers/serviceRegistry.ts b/src/client/providers/serviceRegistry.ts index a96ec14ff5e9..70fc6dc34135 100644 --- a/src/client/providers/serviceRegistry.ts +++ b/src/client/providers/serviceRegistry.ts @@ -6,10 +6,13 @@ import { IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { CodeActionProviderService } from './codeActionProvider/main'; +import { InstallFormatterPrompt } from './prompts/installFormatterPrompt'; +import { IInstallFormatterPrompt } from './prompts/types'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton( IExtensionSingleActivationService, CodeActionProviderService, ); + serviceManager.addSingleton(IInstallFormatterPrompt, InstallFormatterPrompt); } From 6e31c6a5ae1aab803abe878b23d1e158472a4234 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 28 Mar 2023 11:08:18 -0700 Subject: [PATCH 007/336] Update version of main to release candidate (#20923) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d75ad8b76976..9b2af1552f55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.5.0-dev", + "version": "2023.5.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.5.0-dev", + "version": "2023.5.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 08f071da591d..0cd2e41bc6a4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.5.0-dev", + "version": "2023.5.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From 5fd9f97dd89b11b74449ac52b41c15fa64b8079c Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 28 Mar 2023 11:43:30 -0700 Subject: [PATCH 008/336] Update main to next pre-release (#20928) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b2af1552f55..f445bf7f42e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.5.0-rc", + "version": "2023.7.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.5.0-rc", + "version": "2023.7.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 0cd2e41bc6a4..1331f98e88d3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.5.0-rc", + "version": "2023.7.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From b208384303c674f181513e784b3b2834e49108c7 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 28 Mar 2023 17:02:17 -0700 Subject: [PATCH 009/336] Add diagnostic to validate ComSpec (#20927) Closes https://github.com/microsoft/vscode-python/issues/16692 --- .../diagnostics/checks/pythonInterpreter.ts | 131 ++++++++- .../application/diagnostics/constants.ts | 3 + src/client/common/utils/localize.ts | 1 + .../pythonEnvironments/info/executable.ts | 8 +- .../checks/pythonInterpreter.unit.test.ts | 254 ++++++++++++++++-- 5 files changed, 369 insertions(+), 28 deletions(-) diff --git a/src/client/application/diagnostics/checks/pythonInterpreter.ts b/src/client/application/diagnostics/checks/pythonInterpreter.ts index 4f5133d9dcbe..8fc1ce0c0a90 100644 --- a/src/client/application/diagnostics/checks/pythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/pythonInterpreter.ts @@ -6,7 +6,7 @@ import { inject, injectable } from 'inversify'; import { DiagnosticSeverity, l10n } from 'vscode'; import '../../../common/extensions'; import * as path from 'path'; -import { IDisposableRegistry, IInterpreterPathService, Resource } from '../../../common/types'; +import { IConfigurationService, IDisposableRegistry, IInterpreterPathService, Resource } from '../../../common/types'; import { IInterpreterService } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; @@ -28,6 +28,12 @@ import { EventName } from '../../../telemetry/constants'; import { IExtensionSingleActivationService } from '../../../activation/types'; import { cache } from '../../../common/utils/decorators'; import { noop } from '../../../common/utils/misc'; +import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; +import { IFileSystem } from '../../../common/platform/types'; +import { traceError } from '../../../logging'; +import { getExecutable } from '../../../common/process/internal/python'; +import { getSearchPathEnvVarNames } from '../../../common/utils/exec'; +import { IProcessServiceFactory } from '../../../common/process/types'; const messages = { [DiagnosticCodes.NoPythonInterpretersDiagnostic]: l10n.t( @@ -36,6 +42,15 @@ const messages = { [DiagnosticCodes.InvalidPythonInterpreterDiagnostic]: l10n.t( 'An Invalid Python interpreter is selected{0}, please try changing it to enable features such as IntelliSense, linting, and debugging. See output for more details regarding why the interpreter is invalid.', ), + [DiagnosticCodes.InvalidComspecDiagnostic]: l10n.t( + 'We detected an issue with one of your environment variables that breaks features such as IntelliSense, linting and debugging. Try setting the "ComSpec" variable to a valid Command Prompt path in your system to fix it.', + ), + [DiagnosticCodes.IncompletePathVarDiagnostic]: l10n.t( + 'We detected an issue with "Path" environment variable that breaks features such as IntelliSense, linting and debugging. Please edit it to make sure it contains the "SystemRoot" subdirectories.', + ), + [DiagnosticCodes.DefaultShellErrorDiagnostic]: l10n.t( + 'We detected an issue with your default shell that breaks features such as IntelliSense, linting and debugging. Try resetting "ComSpec" and "Path" environment variables to fix it.', + ), }; export class InvalidPythonInterpreterDiagnostic extends BaseDiagnostic { @@ -61,6 +76,17 @@ export class InvalidPythonInterpreterDiagnostic extends BaseDiagnostic { } } +type DefaultShellDiagnostics = + | DiagnosticCodes.InvalidComspecDiagnostic + | DiagnosticCodes.IncompletePathVarDiagnostic + | DiagnosticCodes.DefaultShellErrorDiagnostic; + +export class DefaultShellDiagnostic extends BaseDiagnostic { + constructor(code: DefaultShellDiagnostics, resource: Resource, scope = DiagnosticScope.Global) { + super(code, messages[code], DiagnosticSeverity.Error, scope, resource, undefined, 'always'); + } +} + export const InvalidPythonInterpreterServiceId = 'InvalidPythonInterpreterServiceId'; @injectable() @@ -73,7 +99,13 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, ) { super( - [DiagnosticCodes.NoPythonInterpretersDiagnostic, DiagnosticCodes.InvalidPythonInterpreterDiagnostic], + [ + DiagnosticCodes.NoPythonInterpretersDiagnostic, + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + DiagnosticCodes.InvalidComspecDiagnostic, + DiagnosticCodes.IncompletePathVarDiagnostic, + DiagnosticCodes.DefaultShellErrorDiagnostic, + ], serviceContainer, disposableRegistry, false, @@ -95,14 +127,17 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService ); } - // eslint-disable-next-line class-methods-use-this - public async diagnose(_resource: Resource): Promise { - return []; + public async diagnose(resource: Resource): Promise { + return this.diagnoseDefaultShell(resource); } public async _manualDiagnose(resource: Resource): Promise { const workspaceService = this.serviceContainer.get(IWorkspaceService); const interpreterService = this.serviceContainer.get(IInterpreterService); + const diagnostics = await this.diagnoseDefaultShell(resource); + if (diagnostics.length > 0) { + return diagnostics; + } const hasInterpreters = await interpreterService.hasInterpreters(); const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); const isInterpreterSetToDefault = interpreterPathService.get(resource) === 'python'; @@ -140,6 +175,72 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService return false; } + private async diagnoseDefaultShell(resource: Resource): Promise { + await this.isPathVarIncomplete(); + if (getOSType() !== OSType.Windows) { + return []; + } + const interpreterService = this.serviceContainer.get(IInterpreterService); + const currentInterpreter = await interpreterService.getActiveInterpreter(resource); + if (currentInterpreter) { + return []; + } + try { + await this.shellExecPython(); + } catch (ex) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((ex as any).errno === -4058) { + // ENOENT (-4058) error is thrown by Node when the default shell is invalid. + traceError('ComSpec is likely set to an invalid value', getEnvironmentVariable('ComSpec')); + if (await this.isComspecInvalid()) { + return [new DefaultShellDiagnostic(DiagnosticCodes.InvalidComspecDiagnostic, resource)]; + } + if (this.isPathVarIncomplete()) { + traceError('PATH env var appears to be incomplete', process.env.Path, process.env.PATH); + return [new DefaultShellDiagnostic(DiagnosticCodes.IncompletePathVarDiagnostic, resource)]; + } + return [new DefaultShellDiagnostic(DiagnosticCodes.DefaultShellErrorDiagnostic, resource)]; + } + } + return []; + } + + private async isComspecInvalid() { + const comSpec = getEnvironmentVariable('ComSpec') ?? ''; + const fs = this.serviceContainer.get(IFileSystem); + return fs.fileExists(comSpec).then((exists) => !exists); + } + + // eslint-disable-next-line class-methods-use-this + private isPathVarIncomplete() { + const envVars = getSearchPathEnvVarNames(); + const systemRoot = getEnvironmentVariable('SystemRoot') ?? 'C:\\WINDOWS'; + for (const envVar of envVars) { + const value = getEnvironmentVariable(envVar); + if (value?.includes(systemRoot)) { + return false; + } + } + return true; + } + + @cache(-1, true) + // eslint-disable-next-line class-methods-use-this + private async shellExecPython() { + const configurationService = this.serviceContainer.get(IConfigurationService); + const { pythonPath } = configurationService.getSettings(); + const [args] = getExecutable(); + const argv = [pythonPath, ...args]; + // Concat these together to make a set of quoted strings + const quoted = argv.reduce( + (p, c) => (p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`), + '', + ); + const processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + const service = await processServiceFactory.create(); + return service.shellExec(quoted, { timeout: 15000 }); + } + @cache(1000, true) // This is to handle throttling of multiple events. protected async onHandle(diagnostics: IDiagnostic[]): Promise { if (diagnostics.length === 0) { @@ -163,6 +264,26 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] { const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); + if ( + diagnostic.code === DiagnosticCodes.InvalidComspecDiagnostic || + diagnostic.code === DiagnosticCodes.IncompletePathVarDiagnostic || + diagnostic.code === DiagnosticCodes.DefaultShellErrorDiagnostic + ) { + const links: Record = { + InvalidComspecDiagnostic: 'https://aka.ms/AAk3djo', + IncompletePathVarDiagnostic: 'https://aka.ms/AAk744c', + DefaultShellErrorDiagnostic: 'https://aka.ms/AAk7qix', + }; + return [ + { + prompt: Common.seeInstructions, + command: commandFactory.createCommand(diagnostic, { + type: 'launch', + options: links[diagnostic.code], + }), + }, + ]; + } const prompts = [ { prompt: Common.selectPythonInterpreter, diff --git a/src/client/application/diagnostics/constants.ts b/src/client/application/diagnostics/constants.ts index 9fdd6ff13723..ca2867fc4f49 100644 --- a/src/client/application/diagnostics/constants.ts +++ b/src/client/application/diagnostics/constants.ts @@ -12,6 +12,9 @@ export enum DiagnosticCodes { InvalidPythonPathInDebuggerLaunchDiagnostic = 'InvalidPythonPathInDebuggerLaunchDiagnostic', EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic = 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic', InvalidPythonInterpreterDiagnostic = 'InvalidPythonInterpreterDiagnostic', + InvalidComspecDiagnostic = 'InvalidComspecDiagnostic', + IncompletePathVarDiagnostic = 'IncompletePathVarDiagnostic', + DefaultShellErrorDiagnostic = 'DefaultShellErrorDiagnostic', LSNotSupportedDiagnostic = 'LSNotSupportedDiagnostic', PythonPathDeprecatedDiagnostic = 'PythonPathDeprecatedDiagnostic', JustMyCodeDiagnostic = 'JustMyCodeDiagnostic', diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 64198b661d1e..39bf24415908 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -49,6 +49,7 @@ export namespace Diagnostics { export namespace Common { export const allow = l10n.t('Allow'); + export const seeInstructions = l10n.t('See Instructions'); export const close = l10n.t('Close'); export const bannerLabelYes = l10n.t('Yes'); export const bannerLabelNo = l10n.t('No'); diff --git a/src/client/pythonEnvironments/info/executable.ts b/src/client/pythonEnvironments/info/executable.ts index 9b16d04f5753..70c74329c49b 100644 --- a/src/client/pythonEnvironments/info/executable.ts +++ b/src/client/pythonEnvironments/info/executable.ts @@ -14,11 +14,7 @@ import { copyPythonExecInfo, PythonExecInfo } from '../exec'; * @param python - the information to use when running Python * @param shellExec - the function to use to run Python */ -export async function getExecutablePath( - python: PythonExecInfo, - shellExec: ShellExecFunc, - timeout?: number, -): Promise { +export async function getExecutablePath(python: PythonExecInfo, shellExec: ShellExecFunc): Promise { try { const [args, parse] = getExecutable(); const info = copyPythonExecInfo(python, args); @@ -28,7 +24,7 @@ export async function getExecutablePath( (p, c) => (p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`), '', ); - const result = await shellExec(quoted, { timeout: timeout ?? 15000 }); + const result = await shellExec(quoted, { timeout: 15000 }); const executable = parse(result.stdout.trim()); if (executable === '') { throw new Error(`${quoted} resulted in empty stdout`); diff --git a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts index ea9bc9ae62d5..dec9ad41298e 100644 --- a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -9,6 +9,7 @@ import { EventEmitter, Uri } from 'vscode'; import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; import { InvalidLaunchJsonDebuggerDiagnostic } from '../../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; import { + DefaultShellDiagnostic, InvalidPythonInterpreterDiagnostic, InvalidPythonInterpreterService, } from '../../../../client/application/diagnostics/checks/pythonInterpreter'; @@ -27,13 +28,21 @@ import { import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; import { ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; import { Commands } from '../../../../client/common/constants'; -import { IPlatformService } from '../../../../client/common/platform/types'; -import { IDisposable, IDisposableRegistry, IInterpreterPathService, Resource } from '../../../../client/common/types'; +import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../../../client/common/process/types'; +import { + IConfigurationService, + IDisposable, + IDisposableRegistry, + IInterpreterPathService, + Resource, +} from '../../../../client/common/types'; import { Common } from '../../../../client/common/utils/localize'; import { noop } from '../../../../client/common/utils/misc'; -import { IInterpreterHelper, IInterpreterService } from '../../../../client/interpreter/contracts'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../../client/ioc/types'; import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { getOSType, OSType } from '../../../common'; import { sleep } from '../../../core'; suite('Application Diagnostics - Checks Python Interpreter', () => { @@ -44,13 +53,28 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { let platformService: typemoq.IMock; let workspaceService: typemoq.IMock; let commandManager: typemoq.IMock; - let helper: typemoq.IMock; + let configService: typemoq.IMock; + let fs: typemoq.IMock; let serviceContainer: typemoq.IMock; + let processService: typemoq.IMock; let interpreterPathService: typemoq.IMock; + const oldComSpec = process.env.ComSpec; + const oldPath = process.env.Path; function createContainer() { + fs = typemoq.Mock.ofType(); + fs.setup((f) => f.fileExists(process.env.ComSpec ?? 'exists')).returns(() => Promise.resolve(true)); serviceContainer = typemoq.Mock.ofType(); + processService = typemoq.Mock.ofType(); + const processServiceFactory = typemoq.Mock.ofType(); + processServiceFactory.setup((p) => p.create()).returns(() => Promise.resolve(processService.object)); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IProcessServiceFactory))) + .returns(() => processServiceFactory.object); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processService.setup((p) => (p as any).then).returns(() => undefined); workspaceService = typemoq.Mock.ofType(); commandManager = typemoq.Mock.ofType(); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IFileSystem))).returns(() => fs.object); serviceContainer.setup((s) => s.get(typemoq.It.isValue(ICommandManager))).returns(() => commandManager.object); workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); serviceContainer @@ -82,8 +106,11 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { serviceContainer .setup((s) => s.get(typemoq.It.isValue(IInterpreterPathService))) .returns(() => interpreterPathService.object); - helper = typemoq.Mock.ofType(); - serviceContainer.setup((s) => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); + configService = typemoq.Mock.ofType(); + configService.setup((c) => c.getSettings()).returns(() => ({ pythonPath: 'pythonPath' } as any)); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); return serviceContainer.object; } @@ -102,6 +129,11 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { (diagnosticService as any)._clear(); }); + teardown(() => { + process.env.ComSpec = oldComSpec; + process.env.Path = oldPath; + }); + test('Registers command to trigger environment prompts', async () => { let triggerFunction: ((resource: Resource) => Promise) | undefined; commandManager @@ -191,7 +223,96 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { 'not the same', ); }); - test('Should return invalid diagnostics if there are interpreters but no current interpreter', async () => { + test('Should return comspec diagnostics if comspec is configured incorrectly', async function () { + if (getOSType() !== OSType.Windows) { + return this.skip(); + } + // No interpreter should exist if comspec is incorrectly configured. + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve(undefined); + }); + // Should fail with this error code if comspec is incorrectly configured. + processService + .setup((p) => p.shellExec(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.reject({ errno: -4058 })); + // Should be set to an invalid value in this case. + process.env.ComSpec = 'doesNotExist'; + fs.setup((f) => f.fileExists('doesNotExist')).returns(() => Promise.resolve(false)); + + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new DefaultShellDiagnostic(DiagnosticCodes.InvalidComspecDiagnostic, undefined)], + 'not the same', + ); + }); + test('Should return incomplete path diagnostics if `Path` variable is incomplete and execution fails', async function () { + if (getOSType() !== OSType.Windows) { + return this.skip(); + } + // No interpreter should exist if execution is failing. + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve(undefined); + }); + processService + .setup((p) => p.shellExec(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.reject({ errno: -4058 })); + process.env.Path = 'SystemRootDoesNotExist'; + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new DefaultShellDiagnostic(DiagnosticCodes.IncompletePathVarDiagnostic, undefined)], + 'not the same', + ); + }); + test('Should return default shell error diagnostic if execution fails but we do not identify the cause', async function () { + if (getOSType() !== OSType.Windows) { + return this.skip(); + } + // No interpreter should exist if execution is failing. + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve(undefined); + }); + processService + .setup((p) => p.shellExec(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.reject({ errno: -4058 })); + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new DefaultShellDiagnostic(DiagnosticCodes.DefaultShellErrorDiagnostic, undefined)], + 'not the same', + ); + }); + test('Should return invalid interpreter diagnostics on non-Windows if there is no current interpreter and execution fails', async function () { + if (getOSType() === OSType.Windows) { + return this.skip(); + } + interpreterService.setup((i) => i.hasInterpreters()).returns(() => Promise.resolve(false)); + // No interpreter should exist if execution is failing. + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve(undefined); + }); + processService + .setup((p) => p.shellExec(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.reject({ errno: -4058 })); + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [ + new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + undefined, + workspaceService.object, + ), + ], + 'not the same', + ); + }); + test('Should return invalid interpreter diagnostics if there are interpreters but no current interpreter', async () => { interpreterService .setup((i) => i.hasInterpreters()) .returns(() => Promise.resolve(true)) @@ -200,8 +321,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) .returns(() => { return Promise.resolve(undefined); - }) - .verifiable(typemoq.Times.once()); + }); const diagnostics = await diagnosticService._manualDiagnose(undefined); expect(diagnostics).to.be.deep.equal( @@ -214,24 +334,124 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { ], 'not the same', ); - interpreterService.verifyAll(); }); test('Should return empty diagnostics if there are interpreters and a current interpreter', async () => { - interpreterService - .setup((i) => i.hasInterpreters()) - .returns(() => Promise.resolve(true)) - .verifiable(typemoq.Times.once()); + interpreterService.setup((i) => i.hasInterpreters()).returns(() => Promise.resolve(true)); interpreterService .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) .returns(() => { return Promise.resolve({ envType: EnvironmentType.Unknown } as any); - }) - .verifiable(typemoq.Times.once()); + }); const diagnostics = await diagnosticService._manualDiagnose(undefined); expect(diagnostics).to.be.deep.equal([], 'not the same'); - interpreterService.verifyAll(); }); + + test('Handling comspec diagnostic should launch expected browser link', async () => { + const diagnostic = new DefaultShellDiagnostic(DiagnosticCodes.InvalidComspecDiagnostic, undefined); + const cmd = ({} as any) as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith>({ + type: 'launch', + options: 'https://aka.ms/AAk3djo', + }), + ), + ) + .returns(() => cmd) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([ + { + prompt: Common.seeInstructions, + command: cmd, + }, + ]); + }); + + test('Handling incomplete path diagnostic should launch expected browser link', async () => { + const diagnostic = new DefaultShellDiagnostic(DiagnosticCodes.IncompletePathVarDiagnostic, undefined); + const cmd = ({} as any) as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith>({ + type: 'launch', + options: 'https://aka.ms/AAk744c', + }), + ), + ) + .returns(() => cmd) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([ + { + prompt: Common.seeInstructions, + command: cmd, + }, + ]); + }); + + test('Handling default shell error diagnostic should launch expected browser link', async () => { + const diagnostic = new DefaultShellDiagnostic(DiagnosticCodes.DefaultShellErrorDiagnostic, undefined); + const cmd = ({} as any) as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith>({ + type: 'launch', + options: 'https://aka.ms/AAk7qix', + }), + ), + ) + .returns(() => cmd) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([ + { + prompt: Common.seeInstructions, + command: cmd, + }, + ]); + }); + test('Handling no interpreters diagnostic should return select interpreter cmd', async () => { const diagnostic = new InvalidPythonInterpreterDiagnostic( DiagnosticCodes.NoPythonInterpretersDiagnostic, From ef6511efcbbce1c8c9ad351c3286b8b825074186 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 29 Mar 2023 10:00:54 -0700 Subject: [PATCH 010/336] Fix telemetry when updating pip (#20903) @luabud This PR adds a minor telemetry change to create environment. There is a new telemetry point indicating pip upgrade. --- pythonFiles/create_venv.py | 16 +++++------ pythonFiles/tests/test_create_venv.py | 20 +++++--------- src/client/common/utils/localize.ts | 1 + .../provider/venvProgressAndTelemetry.ts | 27 ++++++++++++++++--- src/client/telemetry/index.ts | 4 +-- 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/pythonFiles/create_venv.py b/pythonFiles/create_venv.py index a97199c4c6c6..2a2768c993ae 100644 --- a/pythonFiles/create_venv.py +++ b/pythonFiles/create_venv.py @@ -90,14 +90,12 @@ def install_requirements(venv_path: str, requirements: List[str]) -> None: if not requirements: return - print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}") - args = [] for requirement in requirements: - args += ["-r", requirement] - run_process( - [venv_path, "-m", "pip", "install"] + args, - "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS", - ) + print(f"VENV_INSTALLING_REQUIREMENTS: {requirement}") + run_process( + [venv_path, "-m", "pip", "install", "-r", requirement], + "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS", + ) print("CREATE_VENV.PIP_INSTALLED_REQUIREMENTS") @@ -111,10 +109,12 @@ def install_toml(venv_path: str, extras: List[str]) -> None: def upgrade_pip(venv_path: str) -> None: + print("CREATE_VENV.UPGRADING_PIP") run_process( [venv_path, "-m", "pip", "install", "--upgrade", "pip"], - "CREATE_VENV.PIP_UPGRADE_FAILED", + "CREATE_VENV.UPGRADE_PIP_FAILED", ) + print("CREATE_VENV.UPGRADED_PIP") def add_gitignore(name: str) -> None: diff --git a/pythonFiles/tests/test_create_venv.py b/pythonFiles/tests/test_create_venv.py index e70a4d90c99b..95ec863373d8 100644 --- a/pythonFiles/tests/test_create_venv.py +++ b/pythonFiles/tests/test_create_venv.py @@ -100,7 +100,7 @@ def run_process(args, error_message): nonlocal pip_upgraded, installing if args[1:] == ["-m", "pip", "install", "--upgrade", "pip"]: pip_upgraded = True - assert error_message == "CREATE_VENV.PIP_UPGRADE_FAILED" + assert error_message == "CREATE_VENV.UPGRADE_PIP_FAILED" elif args[1:-1] == ["-m", "pip", "install", "-r"]: installing = "requirements" assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS" @@ -146,22 +146,16 @@ def run_process(args, error_message): @pytest.mark.parametrize( ("extras", "expected"), [ - ([], None), + ([], []), ( ["requirements/test.txt"], - [sys.executable, "-m", "pip", "install", "-r", "requirements/test.txt"], + [[sys.executable, "-m", "pip", "install", "-r", "requirements/test.txt"]], ), ( ["requirements/test.txt", "requirements/doc.txt"], [ - sys.executable, - "-m", - "pip", - "install", - "-r", - "requirements/test.txt", - "-r", - "requirements/doc.txt", + [sys.executable, "-m", "pip", "install", "-r", "requirements/test.txt"], + [sys.executable, "-m", "pip", "install", "-r", "requirements/doc.txt"], ], ), ], @@ -169,11 +163,11 @@ def run_process(args, error_message): def test_requirements_args(extras, expected): importlib.reload(create_venv) - actual = None + actual = [] def run_process(args, error_message): nonlocal actual - actual = args + actual.append(args) create_venv.run_process = run_process diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 39bf24415908..509287d87533 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -439,6 +439,7 @@ export namespace CreateEnv { export namespace Venv { export const creating = l10n.t('Creating venv...'); export const created = l10n.t('Environment created...'); + export const upgradingPip = l10n.t('Upgrading pip...'); export const installingPackages = l10n.t('Installing packages...'); export const errorCreatingEnvironment = l10n.t('Error while creating virtual environment.'); export const selectPythonPlaceHolder = l10n.t('Select a Python installation to create the virtual environment'); diff --git a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts index 96749fc3d564..f9de4a94689a 100644 --- a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts +++ b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts @@ -18,13 +18,19 @@ export const CREATE_VENV_FAILED_MARKER = 'CREATE_VENV.VENV_FAILED_CREATION'; export const VENV_ALREADY_EXISTS_MARKER = 'CREATE_VENV.VENV_ALREADY_EXISTS'; export const INSTALLED_REQUIREMENTS_MARKER = 'CREATE_VENV.PIP_INSTALLED_REQUIREMENTS'; export const INSTALLED_PYPROJECT_MARKER = 'CREATE_VENV.PIP_INSTALLED_PYPROJECT'; -export const PIP_UPGRADE_FAILED_MARKER = 'CREATE_VENV.PIP_UPGRADE_FAILED'; +export const UPGRADE_PIP_FAILED_MARKER = 'CREATE_VENV.UPGRADE_PIP_FAILED'; +export const UPGRADING_PIP_MARKER = 'CREATE_VENV.UPGRADING_PIP'; +export const UPGRADED_PIP_MARKER = 'CREATE_VENV.UPGRADED_PIP'; export class VenvProgressAndTelemetry { private venvCreatedReported = false; private venvOrPipMissingReported = false; + private venvUpgradingPipReported = false; + + private venvUpgradedPipReported = false; + private venvFailedReported = false; private venvInstallingPackagesReported = false; @@ -70,6 +76,15 @@ export class VenvProgressAndTelemetry { reason: 'noPip', }); this.lastError = PIP_NOT_INSTALLED_MARKER; + } else if (!this.venvUpgradingPipReported && output.includes(UPGRADING_PIP_MARKER)) { + this.venvUpgradingPipReported = true; + this.progress.report({ + message: CreateEnv.Venv.upgradingPip, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); } else if (!this.venvFailedReported && output.includes(CREATE_VENV_FAILED_MARKER)) { this.venvFailedReported = true; sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { @@ -95,13 +110,13 @@ export class VenvProgressAndTelemetry { environmentType: 'venv', using: 'pyproject.toml', }); - } else if (!this.venvInstallingPackagesFailedReported && output.includes(PIP_UPGRADE_FAILED_MARKER)) { + } else if (!this.venvInstallingPackagesFailedReported && output.includes(UPGRADE_PIP_FAILED_MARKER)) { this.venvInstallingPackagesFailedReported = true; sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { environmentType: 'venv', using: 'pipUpgrade', }); - this.lastError = PIP_UPGRADE_FAILED_MARKER; + this.lastError = UPGRADE_PIP_FAILED_MARKER; } else if (!this.venvInstallingPackagesFailedReported && output.includes(INSTALL_REQUIREMENTS_FAILED_MARKER)) { this.venvInstallingPackagesFailedReported = true; sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { @@ -116,6 +131,12 @@ export class VenvProgressAndTelemetry { using: 'pyproject.toml', }); this.lastError = INSTALL_PYPROJECT_FAILED_MARKER; + } else if (!this.venvUpgradedPipReported && output.includes(UPGRADED_PIP_MARKER)) { + this.venvUpgradedPipReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); } else if (!this.venvInstalledPackagesReported && output.includes(INSTALLED_REQUIREMENTS_MARKER)) { this.venvInstalledPackagesReported = true; sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 74f1ddf0ecf4..973e55302635 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2054,7 +2054,7 @@ export interface IEventNamePropertyMapping { */ [EventName.ENVIRONMENT_INSTALLING_PACKAGES]: { environmentType: 'venv' | 'conda'; - using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipUpgrade'; }; /** * Telemetry event sent after installing packages. @@ -2067,7 +2067,7 @@ export interface IEventNamePropertyMapping { */ [EventName.ENVIRONMENT_INSTALLED_PACKAGES]: { environmentType: 'venv' | 'conda'; - using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipUpgrade'; }; /** * Telemetry event sent if installing packages failed. From 8d8784f73d8509c17be3ac8c53d98b909d74e9d1 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 29 Mar 2023 10:44:48 -0700 Subject: [PATCH 011/336] Fix diagnostic to use "System32" instead of "SystemRoot" (#20937) For https://github.com/microsoft/vscode-python/issues/16692 Follow up to https://github.com/microsoft/vscode-python/pull/20927 --- .../application/diagnostics/checks/pythonInterpreter.ts | 7 ++++--- .../diagnostics/checks/pythonInterpreter.unit.test.ts | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/application/diagnostics/checks/pythonInterpreter.ts b/src/client/application/diagnostics/checks/pythonInterpreter.ts index 8fc1ce0c0a90..31da53e75357 100644 --- a/src/client/application/diagnostics/checks/pythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/pythonInterpreter.ts @@ -34,6 +34,7 @@ import { traceError } from '../../../logging'; import { getExecutable } from '../../../common/process/internal/python'; import { getSearchPathEnvVarNames } from '../../../common/utils/exec'; import { IProcessServiceFactory } from '../../../common/process/types'; +import { normCasePath } from '../../../common/platform/fs-paths'; const messages = { [DiagnosticCodes.NoPythonInterpretersDiagnostic]: l10n.t( @@ -46,7 +47,7 @@ const messages = { 'We detected an issue with one of your environment variables that breaks features such as IntelliSense, linting and debugging. Try setting the "ComSpec" variable to a valid Command Prompt path in your system to fix it.', ), [DiagnosticCodes.IncompletePathVarDiagnostic]: l10n.t( - 'We detected an issue with "Path" environment variable that breaks features such as IntelliSense, linting and debugging. Please edit it to make sure it contains the "SystemRoot" subdirectories.', + 'We detected an issue with "Path" environment variable that breaks features such as IntelliSense, linting and debugging. Please edit it to make sure it contains the "System32" subdirectories.', ), [DiagnosticCodes.DefaultShellErrorDiagnostic]: l10n.t( 'We detected an issue with your default shell that breaks features such as IntelliSense, linting and debugging. Try resetting "ComSpec" and "Path" environment variables to fix it.', @@ -176,7 +177,6 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService } private async diagnoseDefaultShell(resource: Resource): Promise { - await this.isPathVarIncomplete(); if (getOSType() !== OSType.Windows) { return []; } @@ -215,9 +215,10 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService private isPathVarIncomplete() { const envVars = getSearchPathEnvVarNames(); const systemRoot = getEnvironmentVariable('SystemRoot') ?? 'C:\\WINDOWS'; + const system32 = path.join(systemRoot, 'system32'); for (const envVar of envVars) { const value = getEnvironmentVariable(envVar); - if (value?.includes(systemRoot)) { + if (value && normCasePath(value).includes(normCasePath(system32))) { return false; } } diff --git a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts index dec9ad41298e..2397743274c1 100644 --- a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -280,6 +280,7 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { processService .setup((p) => p.shellExec(typemoq.It.isAny(), typemoq.It.isAny())) .returns(() => Promise.reject({ errno: -4058 })); + process.env.Path = 'C:\\Windows\\System32'; const diagnostics = await diagnosticService._manualDiagnose(undefined); expect(diagnostics).to.be.deep.equal( [new DefaultShellDiagnostic(DiagnosticCodes.DefaultShellErrorDiagnostic, undefined)], From 34c54f4a5f47913d41d3f69b614f7c404ce3cf48 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Wed, 29 Mar 2023 11:16:29 -0700 Subject: [PATCH 012/336] No longer install the Jupyter extension by default (#20218) For https://github.com/microsoft/vscode-python/issues/18073 --------- Co-authored-by: Brett Cannon --- README.md | 4 +++- gulpfile.js | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cd99d54bd221..5fda8b0fe0de 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,9 @@ Extensions installed through the marketplace are subject to the [Marketplace Ter ## Jupyter Notebook quick start -The Python extension and the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) work together to give you a great Notebook experience in VS Code. +The Python extension no longer offers support for Jupyter notebooks out of the box. However, it works with the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. + +- Install the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter). - Open or create a Jupyter Notebook file (.ipynb) and start coding in our Notebook Editor! diff --git a/gulpfile.js b/gulpfile.js index 2949490abd66..a344b165a6cc 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -82,9 +82,13 @@ async function addExtensionPackDependencies() { // extension dependencies need not be installed during development const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8'); const packageJson = JSON.parse(packageJsonContents); - packageJson.extensionPack = ['ms-toolsai.jupyter', 'ms-python.vscode-pylance'].concat( + packageJson.extensionPack = ['ms-python.vscode-pylance'].concat( packageJson.extensionPack ? packageJson.extensionPack : [], ); + // Remove potential duplicates. + packageJson.extensionPack = packageJson.extensionPack.filter( + (item, index) => packageJson.extensionPack.indexOf(item) === index, + ); await fsExtra.writeFile('package.json', JSON.stringify(packageJson, null, 4), 'utf-8'); } From bc59a7d53d685f13221ef5d02f3ed6deb74b47d8 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Wed, 29 Mar 2023 13:28:25 -0700 Subject: [PATCH 013/336] Fix linting in APIs (#20623) Fix lint in api.ts --- .eslintignore | 1 - src/client/api.ts | 12 +++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.eslintignore b/.eslintignore index bce20d67c2c9..e1fe53377064 100644 --- a/.eslintignore +++ b/.eslintignore @@ -148,7 +148,6 @@ src/client/interpreter/helpers.ts src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts src/client/interpreter/display/index.ts -src/client/api.ts src/client/extension.ts src/client/sourceMapSupport.ts src/client/startupTelemetry.ts diff --git a/src/client/api.ts b/src/client/api.ts index 7ca30b2a6940..7bc3fc81373b 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -16,6 +17,7 @@ import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extens import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer, IServiceManager } from './ioc/types'; import { JupyterExtensionIntegration } from './jupyter/jupyterIntegration'; +import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types'; import { traceError } from './logging'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { buildEnvironmentApi } from './environmentApi'; @@ -23,7 +25,7 @@ import { ApiForPylance } from './pylanceApi'; import { getTelemetryReporter } from './telemetry'; export function buildApi( - ready: Promise, + ready: Promise, serviceManager: IServiceManager, serviceContainer: IServiceContainer, discoveryApi: IDiscoveryAPI, @@ -89,7 +91,7 @@ export function buildApi( async getRemoteLauncherCommand( host: string, port: number, - waitUntilDebuggerAttaches: boolean = true, + waitUntilDebuggerAttaches = true, ): Promise { return getDebugpyLauncherArgs({ host, @@ -104,7 +106,7 @@ export function buildApi( settings: { onDidChangeExecutionDetails: interpreterService.onDidChangeInterpreterConfiguration, getExecutionDetails(resource?: Resource) { - const pythonPath = configurationService.getSettings(resource).pythonPath; + const { pythonPath } = configurationService.getSettings(resource); // If pythonPath equals an empty string, no interpreter is set. return { execCommand: pythonPath === '' ? undefined : [pythonPath] }; }, @@ -114,10 +116,10 @@ export function buildApi( datascience: { registerRemoteServerProvider: jupyterIntegration ? jupyterIntegration.registerRemoteServerProvider.bind(jupyterIntegration) - : (noop as any), + : ((noop as unknown) as (serverProvider: IJupyterUriProvider) => void), showDataViewer: jupyterIntegration ? jupyterIntegration.showDataViewer.bind(jupyterIntegration) - : (noop as any), + : ((noop as unknown) as (dataProvider: IDataViewerDataProvider, title: string) => Promise), }, pylance: { createClient: (...args: any[]): BaseLanguageClient => { From 37a70fd428cdebecbdbfa2a9cbc9274ef748160e Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Wed, 29 Mar 2023 16:12:47 -0700 Subject: [PATCH 014/336] Update note on the Jupyter extension in README (#20946) Karthik brought this up offline and I agree, instead of saying we don't offer support out of box, we say it needs to be via the Jupyter extension. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5fda8b0fe0de..d27ec8e762f5 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Extensions installed through the marketplace are subject to the [Marketplace Ter ## Jupyter Notebook quick start -The Python extension no longer offers support for Jupyter notebooks out of the box. However, it works with the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. +The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. - Install the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter). From 96aa8f8ba0ecc7895fdea877d0497f99821e4527 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 30 Mar 2023 12:09:14 -0700 Subject: [PATCH 015/336] Do not deprecate `python.terminal.activateEnvironmentInTerminal` setting along with terminal env var experiment (#20952) I initially deprecated this setting as I thought folks only disabled it because we were sending commands which was annoying. --- src/client/common/terminal/activator/index.ts | 7 ++----- .../terminals/activator/index.unit.test.ts | 17 ++--------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/client/common/terminal/activator/index.ts b/src/client/common/terminal/activator/index.ts index 752d512d4c30..5bc76c0cb0f8 100644 --- a/src/client/common/terminal/activator/index.ts +++ b/src/client/common/terminal/activator/index.ts @@ -5,8 +5,7 @@ import { inject, injectable, multiInject } from 'inversify'; import { Terminal } from 'vscode'; -import { inTerminalEnvVarExperiment } from '../../experiments/helpers'; -import { IConfigurationService, IExperimentService } from '../../types'; +import { IConfigurationService } from '../../types'; import { ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, TerminalActivationOptions } from '../types'; import { BaseTerminalActivator } from './base'; @@ -18,7 +17,6 @@ export class TerminalActivator implements ITerminalActivator { @inject(ITerminalHelper) readonly helper: ITerminalHelper, @multiInject(ITerminalActivationHandler) private readonly handlers: ITerminalActivationHandler[], @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(IExperimentService) private readonly experimentService: IExperimentService, ) { this.initialize(); } @@ -39,8 +37,7 @@ export class TerminalActivator implements ITerminalActivator { options?: TerminalActivationOptions, ): Promise { const settings = this.configurationService.getSettings(options?.resource); - const activateEnvironment = - settings.terminal.activateEnvironment && !inTerminalEnvVarExperiment(this.experimentService); + const activateEnvironment = settings.terminal.activateEnvironment; if (!activateEnvironment || options?.hideFromUser) { return false; } diff --git a/src/test/common/terminals/activator/index.unit.test.ts b/src/test/common/terminals/activator/index.unit.test.ts index 6ed47fdbe2d3..9dff5a800cad 100644 --- a/src/test/common/terminals/activator/index.unit.test.ts +++ b/src/test/common/terminals/activator/index.unit.test.ts @@ -12,12 +12,7 @@ import { ITerminalActivator, ITerminalHelper, } from '../../../../client/common/terminal/types'; -import { - IConfigurationService, - IExperimentService, - IPythonSettings, - ITerminalSettings, -} from '../../../../client/common/types'; +import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../../client/common/types'; suite('Terminal Activator', () => { let activator: TerminalActivator; @@ -25,14 +20,12 @@ suite('Terminal Activator', () => { let handler1: TypeMoq.IMock; let handler2: TypeMoq.IMock; let terminalSettings: TypeMoq.IMock; - let experimentService: TypeMoq.IMock; setup(() => { baseActivator = TypeMoq.Mock.ofType(); terminalSettings = TypeMoq.Mock.ofType(); handler1 = TypeMoq.Mock.ofType(); handler2 = TypeMoq.Mock.ofType(); const configService = TypeMoq.Mock.ofType(); - experimentService = TypeMoq.Mock.ofType(); configService .setup((c) => c.getSettings(TypeMoq.It.isAny())) .returns(() => { @@ -40,17 +33,11 @@ suite('Terminal Activator', () => { terminal: terminalSettings.object, } as unknown) as IPythonSettings; }); - experimentService.setup((e) => e.inExperimentSync(TypeMoq.It.isAny())).returns(() => false); activator = new (class extends TerminalActivator { protected initialize() { this.baseActivator = baseActivator.object; } - })( - TypeMoq.Mock.ofType().object, - [handler1.object, handler2.object], - configService.object, - experimentService.object, - ); + })(TypeMoq.Mock.ofType().object, [handler1.object, handler2.object], configService.object); }); async function testActivationAndHandlers( activationSuccessful: boolean, From 56d1912407a7e072442de73a9f199ebb0e1b9b04 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 30 Mar 2023 23:29:49 -0700 Subject: [PATCH 016/336] Fix running Untitled files with the play button (#20955) --- .../codeExecution/codeExecutionManager.ts | 7 +++-- src/client/terminals/codeExecution/helper.ts | 15 +++++++--- src/client/terminals/types.ts | 4 +-- .../terminals/codeExecution/helper.test.ts | 30 +++++++++++++------ 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 6b4e5947b3db..ed671f2846a2 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -91,11 +91,14 @@ export class CodeExecutionManager implements ICodeExecutionManager { sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'file', trigger }); const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); file = file instanceof Uri ? file : undefined; - const fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); + let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); if (!fileToExecute) { return; } - await codeExecutionHelper.saveFileIfDirty(fileToExecute); + const fileAfterSave = await codeExecutionHelper.saveFileIfDirty(fileToExecute); + if (fileAfterSave) { + fileToExecute = fileAfterSave; + } try { const contents = await this.fileSystem.readFile(fileToExecute.fsPath); diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index d4f205883cef..eee7d91a0db2 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -5,7 +5,7 @@ import '../../common/extensions'; import { inject, injectable } from 'inversify'; import { l10n, Position, Range, TextEditor, Uri } from 'vscode'; -import { IApplicationShell, IDocumentManager } from '../../common/application/types'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../../common/application/types'; import { PYTHON_LANGUAGE } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; import { IProcessServiceFactory } from '../../common/process/types'; @@ -14,6 +14,7 @@ import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { ICodeExecutionHelper } from '../types'; import { traceError } from '../../logging'; +import { Resource } from '../../common/types'; @injectable() export class CodeExecutionHelper implements ICodeExecutionHelper { @@ -25,7 +26,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { private readonly interpreterService: IInterpreterService; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { this.documentManager = serviceContainer.get(IDocumentManager); this.applicationShell = serviceContainer.get(IApplicationShell); this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); @@ -119,11 +120,17 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { return code; } - public async saveFileIfDirty(file: Uri): Promise { + public async saveFileIfDirty(file: Uri): Promise { const docs = this.documentManager.textDocuments.filter((d) => d.uri.path === file.path); if (docs.length === 1 && docs[0].isDirty) { - await docs[0].save(); + const deferred = createDeferred(); + this.documentManager.onDidSaveTextDocument((e) => deferred.resolve(e.uri)); + const commandManager = this.serviceContainer.get(ICommandManager); + await commandManager.executeCommand('workbench.action.files.save', file); + const savedFileUri = await deferred.promise; + return savedFileUri; } + return undefined; } } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index ee96e72b07c4..cf31f4ef1dd0 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { Event, Terminal, TextEditor, Uri } from 'vscode'; -import { IDisposable } from '../common/types'; +import { IDisposable, Resource } from '../common/types'; export const ICodeExecutionService = Symbol('ICodeExecutionService'); @@ -17,7 +17,7 @@ export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); export interface ICodeExecutionHelper { normalizeLines(code: string): Promise; getFileToExecute(): Promise; - saveFileIfDirty(file: Uri): Promise; + saveFileIfDirty(file: Uri): Promise; getSelectedTextToExecute(textEditor: TextEditor): Promise; } diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index 07a91f8e10de..57bf51883eb8 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -8,8 +8,8 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import { Position, Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode'; -import { IApplicationShell, IDocumentManager } from '../../../client/common/application/types'; +import { EventEmitter, Position, Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../../../client/common/application/types'; import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../../client/common/constants'; import '../../../client/common/extensions'; import { ProcessService } from '../../../client/common/process/proc'; @@ -37,6 +37,7 @@ suite('Terminal - Code Execution Helper', () => { let editor: TypeMoq.IMock; let processService: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; const workingPython: PythonEnvironment = { path: PYTHON_PATH, version: new SemVer('3.6.6-final'), @@ -49,6 +50,7 @@ suite('Terminal - Code Execution Helper', () => { setup(() => { const serviceContainer = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); documentManager = TypeMoq.Mock.ofType(); applicationShell = TypeMoq.Mock.ofType(); const envVariablesProvider = TypeMoq.Mock.ofType(); @@ -79,6 +81,7 @@ suite('Terminal - Code Execution Helper', () => { serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) .returns(() => applicationShell.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => commandManager.object); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())) .returns(() => envVariablesProvider.object); @@ -364,15 +367,24 @@ suite('Terminal - Code Execution Helper', () => { .setup((d) => d.textDocuments) .returns(() => [document.object]) .verifiable(TypeMoq.Times.once()); - document.setup((doc) => doc.isUntitled).returns(() => false); + const saveEmitter = new EventEmitter(); + documentManager.setup((d) => d.onDidSaveTextDocument).returns(() => saveEmitter.event); + document.setup((doc) => doc.isUntitled).returns(() => true); document.setup((doc) => doc.isDirty).returns(() => true); document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); - const expectedUri = Uri.file('one.py'); - document.setup((doc) => doc.uri).returns(() => expectedUri); - - await helper.saveFileIfDirty(expectedUri); - documentManager.verifyAll(); - document.verify((doc) => doc.save(), TypeMoq.Times.once()); + const untitledUri = Uri.file('Untitled-1'); + document.setup((doc) => doc.uri).returns(() => untitledUri); + const savedDocument = TypeMoq.Mock.ofType(); + const expectedSavedUri = Uri.file('one.py'); + savedDocument.setup((doc) => doc.uri).returns(() => expectedSavedUri); + commandManager + .setup((c) => c.executeCommand('workbench.action.files.save', untitledUri)) + .callback(() => saveEmitter.fire(savedDocument.object)) + .returns(() => Promise.resolve()); + + const savedUri = await helper.saveFileIfDirty(untitledUri); + + expect(savedUri?.fsPath).to.be.equal(expectedSavedUri.fsPath); }); test('File will be not saved if file is not dirty', async () => { From 5528e0efb9b8c91c83dfb356391085280fb63417 Mon Sep 17 00:00:00 2001 From: Philipp A Date: Fri, 31 Mar 2023 19:19:03 +0200 Subject: [PATCH 017/336] Add nushell support to venv activation (#20842) Fixes #19359 --------- Co-authored-by: Kartik Raj --- .eslintignore | 4 - build/existingFiles.json | 1 + src/client/common/serviceRegistry.ts | 6 + .../baseActivationProvider.ts | 5 +- .../environmentActivationProviders/bash.ts | 11 +- .../commandPrompt.ts | 28 +-- .../environmentActivationProviders/nushell.ts | 40 ++++ .../pyenvActivationProvider.ts | 5 +- src/client/common/terminal/helper.ts | 7 +- .../shellDetectors/baseShellDetector.ts | 2 + src/client/common/terminal/types.ts | 2 + src/test/common/installer.test.ts | 6 + src/test/common/moduleInstaller.test.ts | 6 + src/test/common/serviceRegistry.unit.test.ts | 2 + .../terminals/activation.bash.unit.test.ts | 174 +++++++++--------- .../terminals/activation.conda.unit.test.ts | 2 + .../terminals/activation.nushell.unit.test.ts | 72 ++++++++ src/test/common/terminals/helper.unit.test.ts | 29 ++- 18 files changed, 287 insertions(+), 115 deletions(-) create mode 100644 src/client/common/terminal/environmentActivationProviders/nushell.ts create mode 100644 src/test/common/terminals/activation.nushell.unit.test.ts diff --git a/.eslintignore b/.eslintignore index e1fe53377064..ca0ffe5f5847 100644 --- a/.eslintignore +++ b/.eslintignore @@ -207,10 +207,6 @@ src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts src/client/common/terminal/shellDetectors/settingsShellDetector.ts src/client/common/terminal/shellDetectors/baseShellDetector.ts -src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts -src/client/common/terminal/environmentActivationProviders/commandPrompt.ts -src/client/common/terminal/environmentActivationProviders/bash.ts -src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts src/client/common/utils/decorators.ts src/client/common/utils/enum.ts src/client/common/utils/platform.ts diff --git a/build/existingFiles.json b/build/existingFiles.json index 0d7e0c3c41cc..1f5acc727d8e 100644 --- a/build/existingFiles.json +++ b/build/existingFiles.json @@ -379,6 +379,7 @@ "src/test/common/socketStream.test.ts", "src/test/common/terminals/activation.bash.unit.test.ts", "src/test/common/terminals/activation.commandPrompt.unit.test.ts", + "src/test/common/terminals/activation.nushell.unit.test.ts", "src/test/common/terminals/activation.conda.unit.test.ts", "src/test/common/terminals/activation.unit.test.ts", "src/test/common/terminals/activator/base.unit.test.ts", diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 93f069f18288..8e2c4d4d3ebe 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -65,6 +65,7 @@ import { IProcessLogger } from './process/types'; import { TerminalActivator } from './terminal/activator'; import { PowershellTerminalActivationFailedHandler } from './terminal/activator/powershellFailedHandler'; import { Bash } from './terminal/environmentActivationProviders/bash'; +import { Nushell } from './terminal/environmentActivationProviders/nushell'; import { CommandPromptAndPowerShell } from './terminal/environmentActivationProviders/commandPrompt'; import { CondaActivationCommandProvider } from './terminal/environmentActivationProviders/condaActivationProvider'; import { PipEnvActivationCommandProvider } from './terminal/environmentActivationProviders/pipEnvActivationProvider'; @@ -143,6 +144,11 @@ export function registerTypes(serviceManager: IServiceManager): void { CommandPromptAndPowerShell, TerminalActivationProviders.commandPromptAndPowerShell, ); + serviceManager.addSingleton( + ITerminalActivationCommandProvider, + Nushell, + TerminalActivationProviders.nushell, + ); serviceManager.addSingleton( ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, diff --git a/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts index ca87b172f0a5..abc2ff89df63 100644 --- a/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -48,6 +49,7 @@ abstract class BaseActivationCommandProvider implements ITerminalActivationComma constructor(@inject(IServiceContainer) protected readonly serviceContainer: IServiceContainer) {} public abstract isShellSupported(targetShell: TerminalShellType): boolean; + public async getActivationCommands( resource: Uri | undefined, targetShell: TerminalShellType, @@ -60,13 +62,14 @@ abstract class BaseActivationCommandProvider implements ITerminalActivationComma } return this.getActivationCommandsForInterpreter(interpreter.path, targetShell); } + public abstract getActivationCommandsForInterpreter( pythonPath: string, targetShell: TerminalShellType, ): Promise; } -export type ActivationScripts = Record; +export type ActivationScripts = Partial>; export abstract class VenvBaseActivationCommandProvider extends BaseActivationCommandProvider { public isShellSupported(targetShell: TerminalShellType): boolean { diff --git a/src/client/common/terminal/environmentActivationProviders/bash.ts b/src/client/common/terminal/environmentActivationProviders/bash.ts index 827201037570..00c4d3da114c 100644 --- a/src/client/common/terminal/environmentActivationProviders/bash.ts +++ b/src/client/common/terminal/environmentActivationProviders/bash.ts @@ -7,7 +7,7 @@ import { TerminalShellType } from '../types'; import { ActivationScripts, VenvBaseActivationCommandProvider } from './baseActivationProvider'; // For a given shell the scripts are in order of precedence. -const SCRIPTS: ActivationScripts = ({ +const SCRIPTS: ActivationScripts = { // Group 1 [TerminalShellType.wsl]: ['activate.sh', 'activate'], [TerminalShellType.ksh]: ['activate.sh', 'activate'], @@ -19,13 +19,12 @@ const SCRIPTS: ActivationScripts = ({ [TerminalShellType.cshell]: ['activate.csh'], // Group 3 [TerminalShellType.fish]: ['activate.fish'], -} as unknown) as ActivationScripts; +}; export function getAllScripts(): string[] { const scripts: string[] = []; - for (const key of Object.keys(SCRIPTS)) { - const shell = key as TerminalShellType; - for (const name of SCRIPTS[shell]) { + for (const names of Object.values(SCRIPTS)) { + for (const name of names) { if (!scripts.includes(name)) { scripts.push(name); } @@ -44,7 +43,7 @@ export class Bash extends VenvBaseActivationCommandProvider { ): Promise { const scriptFile = await this.findScriptFile(pythonPath, targetShell); if (!scriptFile) { - return; + return undefined; } return [`source ${scriptFile.fileToCommandArgumentForPythonExt()}`]; } diff --git a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts index 25ab46ca1fb4..6d40e2c390a0 100644 --- a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts +++ b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts @@ -9,19 +9,18 @@ import { TerminalShellType } from '../types'; import { ActivationScripts, VenvBaseActivationCommandProvider } from './baseActivationProvider'; // For a given shell the scripts are in order of precedence. -const SCRIPTS: ActivationScripts = ({ +const SCRIPTS: ActivationScripts = { // Group 1 [TerminalShellType.commandPrompt]: ['activate.bat', 'Activate.ps1'], // Group 2 [TerminalShellType.powershell]: ['Activate.ps1', 'activate.bat'], [TerminalShellType.powershellCore]: ['Activate.ps1', 'activate.bat'], -} as unknown) as ActivationScripts; +}; export function getAllScripts(pathJoin: (...p: string[]) => string): string[] { const scripts: string[] = []; - for (const key of Object.keys(SCRIPTS)) { - const shell = key as TerminalShellType; - for (const name of SCRIPTS[shell]) { + for (const names of Object.values(SCRIPTS)) { + for (const name of names) { if (!scripts.includes(name)) { scripts.push( name, @@ -38,13 +37,14 @@ export function getAllScripts(pathJoin: (...p: string[]) => string): string[] { @injectable() export class CommandPromptAndPowerShell extends VenvBaseActivationCommandProvider { protected readonly scripts: ActivationScripts; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super(serviceContainer); - this.scripts = ({} as unknown) as ActivationScripts; - for (const key of Object.keys(SCRIPTS)) { + this.scripts = {}; + for (const [key, names] of Object.entries(SCRIPTS)) { const shell = key as TerminalShellType; const scripts: string[] = []; - for (const name of SCRIPTS[shell]) { + for (const name of names) { scripts.push( name, // We also add scripts in subdirs. @@ -62,21 +62,23 @@ export class CommandPromptAndPowerShell extends VenvBaseActivationCommandProvide ): Promise { const scriptFile = await this.findScriptFile(pythonPath, targetShell); if (!scriptFile) { - return; + return undefined; } if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('activate.bat')) { return [scriptFile.fileToCommandArgumentForPythonExt()]; - } else if ( + } + if ( (targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore) && scriptFile.endsWith('Activate.ps1') ) { return [`& ${scriptFile.fileToCommandArgumentForPythonExt()}`]; - } else if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('Activate.ps1')) { + } + if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('Activate.ps1')) { // lets not try to run the powershell file from command prompt (user may not have powershell) return []; - } else { - return; } + + return undefined; } } diff --git a/src/client/common/terminal/environmentActivationProviders/nushell.ts b/src/client/common/terminal/environmentActivationProviders/nushell.ts new file mode 100644 index 000000000000..333fd5167770 --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/nushell.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import '../../extensions'; +import { TerminalShellType } from '../types'; +import { ActivationScripts, VenvBaseActivationCommandProvider } from './baseActivationProvider'; + +// For a given shell the scripts are in order of precedence. +const SCRIPTS: ActivationScripts = { + [TerminalShellType.nushell]: ['activate.nu'], +}; + +export function getAllScripts(): string[] { + const scripts: string[] = []; + for (const names of Object.values(SCRIPTS)) { + for (const name of names) { + if (!scripts.includes(name)) { + scripts.push(name); + } + } + } + return scripts; +} + +@injectable() +export class Nushell extends VenvBaseActivationCommandProvider { + protected readonly scripts = SCRIPTS; + + public async getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise { + const scriptFile = await this.findScriptFile(pythonPath, targetShell); + if (!scriptFile) { + return undefined; + } + return [`overlay use ${scriptFile.fileToCommandArgumentForPythonExt()}`]; + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts index 91347f35ae95..6b5ced048672 100644 --- a/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts @@ -14,6 +14,7 @@ import { ITerminalActivationCommandProvider, TerminalShellType } from '../types' export class PyEnvActivationCommandProvider implements ITerminalActivationCommandProvider { constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} + // eslint-disable-next-line class-methods-use-this public isShellSupported(_targetShell: TerminalShellType): boolean { return true; } @@ -23,7 +24,7 @@ export class PyEnvActivationCommandProvider implements ITerminalActivationComman .get(IInterpreterService) .getActiveInterpreter(resource); if (!interpreter || interpreter.envType !== EnvironmentType.Pyenv || !interpreter.envName) { - return; + return undefined; } return [`pyenv shell ${interpreter.envName.toCommandArgumentForPythonExt()}`]; @@ -37,7 +38,7 @@ export class PyEnvActivationCommandProvider implements ITerminalActivationComman .get(IInterpreterService) .getInterpreterDetails(pythonPath); if (!interpreter || interpreter.envType !== EnvironmentType.Pyenv || !interpreter.envName) { - return; + return undefined; } return [`pyenv shell ${interpreter.envName.toCommandArgumentForPythonExt()}`]; diff --git a/src/client/common/terminal/helper.ts b/src/client/common/terminal/helper.ts index 304c98b4cd81..f1a89df10786 100644 --- a/src/client/common/terminal/helper.ts +++ b/src/client/common/terminal/helper.ts @@ -42,6 +42,9 @@ export class TerminalHelper implements ITerminalHelper { @named(TerminalActivationProviders.commandPromptAndPowerShell) private readonly commandPromptAndPowerShell: ITerminalActivationCommandProvider, @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.nushell) + private readonly nushell: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.pyenv) private readonly pyenv: ITerminalActivationCommandProvider, @inject(ITerminalActivationCommandProvider) @@ -72,7 +75,7 @@ export class TerminalHelper implements ITerminalHelper { resource?: Uri, interpreter?: PythonEnvironment, ): Promise { - const providers = [this.pipenv, this.pyenv, this.bashCShellFish, this.commandPromptAndPowerShell]; + const providers = [this.pipenv, this.pyenv, this.bashCShellFish, this.commandPromptAndPowerShell, this.nushell]; const promise = this.getActivationCommands(resource || undefined, interpreter, terminalShellType, providers); this.sendTelemetry( terminalShellType, @@ -90,7 +93,7 @@ export class TerminalHelper implements ITerminalHelper { if (this.platform.osType === OSType.Unknown) { return; } - const providers = [this.bashCShellFish, this.commandPromptAndPowerShell]; + const providers = [this.bashCShellFish, this.commandPromptAndPowerShell, this.nushell]; const promise = this.getActivationCommands(resource, interpreter, shell, providers); this.sendTelemetry( shell, diff --git a/src/client/common/terminal/shellDetectors/baseShellDetector.ts b/src/client/common/terminal/shellDetectors/baseShellDetector.ts index 657c51e82af6..d3c3967d3e3a 100644 --- a/src/client/common/terminal/shellDetectors/baseShellDetector.ts +++ b/src/client/common/terminal/shellDetectors/baseShellDetector.ts @@ -30,6 +30,7 @@ const IS_POWERSHELL_CORE = /(pwsh$)/i; const IS_FISH = /(fish$)/i; const IS_CSHELL = /(csh$)/i; const IS_TCSHELL = /(tcsh$)/i; +const IS_NUSHELL = /(nu$)/i; const IS_XONSH = /(xonsh$)/i; const detectableShells = new Map(); @@ -43,6 +44,7 @@ detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND); detectableShells.set(TerminalShellType.fish, IS_FISH); detectableShells.set(TerminalShellType.tcshell, IS_TCSHELL); detectableShells.set(TerminalShellType.cshell, IS_CSHELL); +detectableShells.set(TerminalShellType.nushell, IS_NUSHELL); detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE); detectableShells.set(TerminalShellType.xonsh, IS_XONSH); diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index e02fa1c03fd7..880bf0dd72fb 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -11,6 +11,7 @@ import { IDisposable, Resource } from '../types'; export enum TerminalActivationProviders { bashCShellFish = 'bashCShellFish', commandPromptAndPowerShell = 'commandPromptAndPowerShell', + nushell = 'nushell', pyenv = 'pyenv', conda = 'conda', pipenv = 'pipenv', @@ -26,6 +27,7 @@ export enum TerminalShellType { fish = 'fish', cshell = 'cshell', tcshell = 'tshell', + nushell = 'nushell', wsl = 'wsl', xonsh = 'xonsh', other = 'other', diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index daea3e44ec76..7ff0ee81c27f 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -51,6 +51,7 @@ import { TerminalActivator } from '../../client/common/terminal/activator'; import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { Nushell } from '../../client/common/terminal/environmentActivationProviders/nushell'; import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; @@ -214,6 +215,11 @@ suite('Installer', () => { CommandPromptAndPowerShell, TerminalActivationProviders.commandPromptAndPowerShell, ); + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + Nushell, + TerminalActivationProviders.nushell, + ); ioc.serviceManager.addSingleton( ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index fe4937f363ec..a6b647ad181d 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -50,6 +50,7 @@ import { TerminalActivator } from '../../client/common/terminal/activator'; import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { Nushell } from '../../client/common/terminal/environmentActivationProviders/nushell'; import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; @@ -225,6 +226,11 @@ suite('Module Installer', () => { CommandPromptAndPowerShell, TerminalActivationProviders.commandPromptAndPowerShell, ); + ioc.serviceManager.addSingleton( + ITerminalActivationCommandProvider, + Nushell, + TerminalActivationProviders.nushell, + ); ioc.serviceManager.addSingleton( ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, diff --git a/src/test/common/serviceRegistry.unit.test.ts b/src/test/common/serviceRegistry.unit.test.ts index 5d764aac2536..2964455ada37 100644 --- a/src/test/common/serviceRegistry.unit.test.ts +++ b/src/test/common/serviceRegistry.unit.test.ts @@ -40,6 +40,7 @@ import { TerminalActivator } from '../../client/common/terminal/activator'; import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { Nushell } from '../../client/common/terminal/environmentActivationProviders/nushell'; import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; @@ -113,6 +114,7 @@ suite('Common - Service Registry', () => { CommandPromptAndPowerShell, TerminalActivationProviders.commandPromptAndPowerShell, ], + [ITerminalActivationCommandProvider, Nushell, TerminalActivationProviders.nushell], [IToolExecutionPath, PipEnvExecutionPath, ToolExecutionPath.pipenv], [ITerminalActivationCommandProvider, CondaActivationCommandProvider, TerminalActivationProviders.conda], [ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv], diff --git a/src/test/common/terminals/activation.bash.unit.test.ts b/src/test/common/terminals/activation.bash.unit.test.ts index 28bab49fd55a..cd057e7be3e5 100644 --- a/src/test/common/terminals/activation.bash.unit.test.ts +++ b/src/test/common/terminals/activation.bash.unit.test.ts @@ -24,107 +24,113 @@ suite('Terminal Environment Activation (bash)', () => { ? 'and there are spaces in the script file (pythonpath),' : 'and there are no spaces in the script file (pythonpath),'; suite(suiteTitle, () => { - ['activate', 'activate.sh', 'activate.csh', 'activate.fish', 'activate.bat', 'Activate.ps1'].forEach( - (scriptFileName) => { - suite(`and script file is ${scriptFileName}`, () => { - let serviceContainer: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - fileSystem = TypeMoq.Mock.ofType(); - serviceContainer.setup((c) => c.get(IFileSystem)).returns(() => fileSystem.object); + [ + 'activate', + 'activate.sh', + 'activate.csh', + 'activate.fish', + 'activate.bat', + 'activate.nu', + 'Activate.ps1', + ].forEach((scriptFileName) => { + suite(`and script file is ${scriptFileName}`, () => { + let serviceContainer: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + serviceContainer.setup((c) => c.get(IFileSystem)).returns(() => fileSystem.object); - interpreterService = TypeMoq.Mock.ofType(); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - serviceContainer - .setup((c) => c.get(IInterpreterService)) - .returns(() => interpreterService.object); - }); + interpreterService = TypeMoq.Mock.ofType(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + serviceContainer + .setup((c) => c.get(IInterpreterService)) + .returns(() => interpreterService.object); + }); + + getNamesAndValues(TerminalShellType).forEach((shellType) => { + let isScriptFileSupported = false; + switch (shellType.value) { + case TerminalShellType.zsh: + case TerminalShellType.ksh: + case TerminalShellType.wsl: + case TerminalShellType.gitbash: + case TerminalShellType.bash: { + isScriptFileSupported = ['activate', 'activate.sh'].indexOf(scriptFileName) >= 0; + break; + } + case TerminalShellType.fish: { + isScriptFileSupported = ['activate.fish'].indexOf(scriptFileName) >= 0; + break; + } + case TerminalShellType.tcshell: + case TerminalShellType.cshell: { + isScriptFileSupported = ['activate.csh'].indexOf(scriptFileName) >= 0; + break; + } + default: { + isScriptFileSupported = false; + } + } + const titleTitle = isScriptFileSupported + ? `Ensure bash Activation command returns activation command (Shell: ${shellType.name})` + : `Ensure bash Activation command returns undefined (Shell: ${shellType.name})`; + + test(titleTitle, async () => { + const bash = new Bash(serviceContainer.object); - getNamesAndValues(TerminalShellType).forEach((shellType) => { - let isScriptFileSupported = false; + const supported = bash.isShellSupported(shellType.value); switch (shellType.value) { + case TerminalShellType.wsl: case TerminalShellType.zsh: case TerminalShellType.ksh: - case TerminalShellType.wsl: + case TerminalShellType.bash: case TerminalShellType.gitbash: - case TerminalShellType.bash: { - isScriptFileSupported = ['activate', 'activate.sh'].indexOf(scriptFileName) >= 0; - break; - } - case TerminalShellType.fish: { - isScriptFileSupported = ['activate.fish'].indexOf(scriptFileName) >= 0; - break; - } case TerminalShellType.tcshell: - case TerminalShellType.cshell: { - isScriptFileSupported = ['activate.csh'].indexOf(scriptFileName) >= 0; + case TerminalShellType.cshell: + case TerminalShellType.fish: { + expect(supported).to.be.equal( + true, + `${shellType.name} shell not supported (it should be)`, + ); break; } default: { - isScriptFileSupported = false; + expect(supported).to.be.equal( + false, + `${shellType.name} incorrectly supported (should not be)`, + ); + // No point proceeding with other tests. + return; } } - const titleTitle = isScriptFileSupported - ? `Ensure bash Activation command returns activation command (Shell: ${shellType.name})` - : `Ensure bash Activation command returns undefined (Shell: ${shellType.name})`; - test(titleTitle, async () => { - const bash = new Bash(serviceContainer.object); + const pathToScriptFile = path.join(path.dirname(pythonPath), scriptFileName); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(undefined, shellType.value); - const supported = bash.isShellSupported(shellType.value); - switch (shellType.value) { - case TerminalShellType.wsl: - case TerminalShellType.zsh: - case TerminalShellType.ksh: - case TerminalShellType.bash: - case TerminalShellType.gitbash: - case TerminalShellType.tcshell: - case TerminalShellType.cshell: - case TerminalShellType.fish: { - expect(supported).to.be.equal( - true, - `${shellType.name} shell not supported (it should be)`, - ); - break; - } - default: { - expect(supported).to.be.equal( - false, - `${shellType.name} incorrectly supported (should not be)`, - ); - // No point proceeding with other tests. - return; - } - } - - const pathToScriptFile = path.join(path.dirname(pythonPath), scriptFileName); - fileSystem - .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) - .returns(() => Promise.resolve(true)); - const command = await bash.getActivationCommands(undefined, shellType.value); - - if (isScriptFileSupported) { - // Ensure the script file is of the following form: - // source "" - // Ensure the path is quoted if it contains any spaces. - // Ensure it contains the name of the environment as an argument to the script file. + if (isScriptFileSupported) { + // Ensure the script file is of the following form: + // source "" + // Ensure the path is quoted if it contains any spaces. + // Ensure it contains the name of the environment as an argument to the script file. - expect(command).to.be.deep.equal( - [`source ${pathToScriptFile.fileToCommandArgumentForPythonExt()}`.trim()], - 'Invalid command', - ); - } else { - expect(command).to.be.equal(undefined, 'Command should be undefined'); - } - }); + expect(command).to.be.deep.equal( + [`source ${pathToScriptFile.fileToCommandArgumentForPythonExt()}`.trim()], + 'Invalid command', + ); + } else { + expect(command).to.be.equal(undefined, 'Command should be undefined'); + } }); }); - }, - ); + }); + }); }); }); }); diff --git a/src/test/common/terminals/activation.conda.unit.test.ts b/src/test/common/terminals/activation.conda.unit.test.ts index 904752d698c9..84e4bffacfc1 100644 --- a/src/test/common/terminals/activation.conda.unit.test.ts +++ b/src/test/common/terminals/activation.conda.unit.test.ts @@ -12,6 +12,7 @@ import { IFileSystem, IPlatformService } from '../../../client/common/platform/t import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { Nushell } from '../../../client/common/terminal/environmentActivationProviders/nushell'; import { CondaActivationCommandProvider, _getPowershellCommands, @@ -110,6 +111,7 @@ suite('Terminal Environment Activation conda', () => { ), instance(bash), mock(CommandPromptAndPowerShell), + mock(Nushell), mock(PyEnvActivationCommandProvider), mock(PipEnvActivationCommandProvider), [], diff --git a/src/test/common/terminals/activation.nushell.unit.test.ts b/src/test/common/terminals/activation.nushell.unit.test.ts new file mode 100644 index 000000000000..bf748bc7c053 --- /dev/null +++ b/src/test/common/terminals/activation.nushell.unit.test.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import '../../../client/common/extensions'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { Nushell } from '../../../client/common/terminal/environmentActivationProviders/nushell'; +import { TerminalShellType } from '../../../client/common/terminal/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +const pythonPath = 'usr/bin/python'; + +suite('Terminal Environment Activation (nushell)', () => { + for (const scriptFileName of ['activate', 'activate.sh', 'activate.nu']) { + suite(`and script file is ${scriptFileName}`, () => { + let serviceContainer: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + serviceContainer.setup((c) => c.get(IFileSystem)).returns(() => fileSystem.object); + + interpreterService = TypeMoq.Mock.ofType(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + serviceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); + }); + + for (const { name, value } of getNamesAndValues(TerminalShellType)) { + const isNushell = value === TerminalShellType.nushell; + const isScriptFileSupported = isNushell && ['activate.nu'].includes(scriptFileName); + const expectedReturn = isScriptFileSupported ? 'activation command' : 'undefined'; + + // eslint-disable-next-line no-loop-func -- setup() takes care of shellType and fileSystem reinitialization + test(`Ensure nushell Activation command returns ${expectedReturn} (Shell: ${name})`, async () => { + const nu = new Nushell(serviceContainer.object); + + const supported = nu.isShellSupported(value); + if (isNushell) { + expect(supported).to.be.equal(true, `${name} shell not supported (it should be)`); + } else { + expect(supported).to.be.equal(false, `${name} incorrectly supported (should not be)`); + // No point proceeding with other tests. + return; + } + + const pathToScriptFile = path.join(path.dirname(pythonPath), scriptFileName); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await nu.getActivationCommands(undefined, value); + + if (isScriptFileSupported) { + expect(command).to.be.deep.equal( + [`overlay use ${pathToScriptFile.fileToCommandArgumentForPythonExt()}`.trim()], + 'Invalid command', + ); + } else { + expect(command).to.be.equal(undefined, 'Command should be undefined'); + } + }); + } + }); + } +}); diff --git a/src/test/common/terminals/helper.unit.test.ts b/src/test/common/terminals/helper.unit.test.ts index 59ac56ebf82e..b6a8d44ac030 100644 --- a/src/test/common/terminals/helper.unit.test.ts +++ b/src/test/common/terminals/helper.unit.test.ts @@ -13,6 +13,7 @@ import { PlatformService } from '../../../client/common/platform/platformService import { IPlatformService } from '../../../client/common/platform/types'; import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { Nushell } from '../../../client/common/terminal/environmentActivationProviders/nushell'; import { CondaActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; import { PipEnvActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; import { PyEnvActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; @@ -42,6 +43,7 @@ suite('Terminal Service helpers', () => { let condaActivationProvider: ITerminalActivationCommandProvider; let bashActivationProvider: ITerminalActivationCommandProvider; let cmdActivationProvider: ITerminalActivationCommandProvider; + let nushellActivationProvider: ITerminalActivationCommandProvider; let pyenvActivationProvider: ITerminalActivationCommandProvider; let pipenvActivationProvider: ITerminalActivationCommandProvider; let pythonSettings: PythonSettings; @@ -67,6 +69,7 @@ suite('Terminal Service helpers', () => { condaActivationProvider = mock(CondaActivationCommandProvider); bashActivationProvider = mock(Bash); cmdActivationProvider = mock(CommandPromptAndPowerShell); + nushellActivationProvider = mock(Nushell); pyenvActivationProvider = mock(PyEnvActivationCommandProvider); pipenvActivationProvider = mock(PipEnvActivationCommandProvider); pythonSettings = mock(PythonSettings); @@ -80,6 +83,7 @@ suite('Terminal Service helpers', () => { instance(condaActivationProvider), instance(bashActivationProvider), instance(cmdActivationProvider), + instance(nushellActivationProvider), instance(pyenvActivationProvider), instance(pipenvActivationProvider), [instance(mockDetector)], @@ -213,6 +217,7 @@ suite('Terminal Service helpers', () => { when(bashActivationProvider.isShellSupported(anything())).thenReturn(false); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(false); + when(nushellActivationProvider.isShellSupported(anything())).thenReturn(false); when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); @@ -225,6 +230,7 @@ suite('Terminal Service helpers', () => { verify(pythonSettings.pythonPath).once(); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); @@ -238,6 +244,7 @@ suite('Terminal Service helpers', () => { when(bashActivationProvider.isShellSupported(anything())).thenReturn(true); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(false); + when(nushellActivationProvider.isShellSupported(anything())).thenReturn(false); when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); @@ -248,6 +255,7 @@ suite('Terminal Service helpers', () => { verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); @@ -262,7 +270,12 @@ suite('Terminal Service helpers', () => { ); when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(true); - [bashActivationProvider, cmdActivationProvider, pyenvActivationProvider].forEach((provider) => { + [ + bashActivationProvider, + cmdActivationProvider, + nushellActivationProvider, + pyenvActivationProvider, + ].forEach((provider) => { when(provider.getActivationCommands(resource, anything())).thenResolve(['Something']); when(provider.isShellSupported(anything())).thenReturn(true); }); @@ -278,6 +291,7 @@ suite('Terminal Service helpers', () => { verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.getActivationCommands(resource, anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); }); test('Activation command must return command from Command Prompt if that is supported and others are not', async () => { const pythonPath = 'some python Path value'; @@ -288,6 +302,7 @@ suite('Terminal Service helpers', () => { when(bashActivationProvider.isShellSupported(anything())).thenReturn(false); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(true); + when(nushellActivationProvider.isShellSupported(anything())).thenReturn(false); when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); @@ -297,21 +312,24 @@ suite('Terminal Service helpers', () => { verify(pythonSettings.pythonPath).once(); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.getActivationCommands(resource, anything())).once(); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); }); - test('Activation command must return command from Command Prompt if that is supported, and so is bash but no commands are returned', async () => { + test('Activation command must return command from Command Prompt if that is supported, and so is bash and nushell but no commands are returned', async () => { const pythonPath = 'some python Path value'; const expectCommand = ['one', 'two']; ensureCondaIsSupported(false, pythonPath, []); when(cmdActivationProvider.getActivationCommands(resource, anything())).thenResolve(expectCommand); when(bashActivationProvider.getActivationCommands(resource, anything())).thenResolve([]); + when(nushellActivationProvider.getActivationCommands(resource, anything())).thenResolve([]); when(bashActivationProvider.isShellSupported(anything())).thenReturn(true); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(true); + when(nushellActivationProvider.isShellSupported(anything())).thenReturn(true); when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); @@ -320,12 +338,15 @@ suite('Terminal Service helpers', () => { expect(cmd).to.deep.equal(expectCommand); verify(pythonSettings.pythonPath).once(); verify(condaService.isCondaEnvironment(pythonPath)).once(); - verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); verify(cmdActivationProvider.getActivationCommands(resource, anything())).once(); + // It should not be called as command prompt already returns the activation commands and is higher priority. + verify(nushellActivationProvider.getActivationCommands(resource, anything())).never(); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); }); [undefined, pythonInterpreter].forEach((interpreter) => { test('Activation command for Shell must be empty for unknown os', async () => { @@ -353,6 +374,7 @@ suite('Terminal Service helpers', () => { when(platformService.osType).thenReturn(osType); when(bashActivationProvider.isShellSupported(shellToExpect)).thenReturn(false); when(cmdActivationProvider.isShellSupported(shellToExpect)).thenReturn(false); + when(nushellActivationProvider.isShellSupported(shellToExpect)).thenReturn(false); const cmd = await helper.getEnvironmentActivationShellCommands( resource, @@ -367,6 +389,7 @@ suite('Terminal Service helpers', () => { verify(pyenvActivationProvider.isShellSupported(anything())).never(); verify(pipenvActivationProvider.isShellSupported(anything())).never(); verify(cmdActivationProvider.isShellSupported(shellToExpect)).atLeast(1); + verify(nushellActivationProvider.isShellSupported(shellToExpect)).atLeast(1); }); }); }); From 6fec579d9df4abcc1bd156d091f73071197ad06b Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 3 Apr 2023 14:02:11 -0700 Subject: [PATCH 018/336] Auto add community feedback comment (#20956) Closes https://github.com/microsoft/vscode-python/issues/20943 --- .../community-feedback-auto-comment.yml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/community-feedback-auto-comment.yml diff --git a/.github/workflows/community-feedback-auto-comment.yml b/.github/workflows/community-feedback-auto-comment.yml new file mode 100644 index 000000000000..584af90d6d82 --- /dev/null +++ b/.github/workflows/community-feedback-auto-comment.yml @@ -0,0 +1,30 @@ +name: Community Feedback Auto Comment + +on: + issues: + types: + - labeled +jobs: + add-comment: + if: github.event.label.name == 'needs community feedback' + runs-on: ubuntu-latest + + permissions: + issues: write + + steps: + - name: Check For Existing Comment + uses: peter-evans/find-comment@v2 + id: finder + with: + issue-number: ${{ github.event.issue.number }} + comment-author: 'github-actions[bot]' + body-includes: Thanks for the feature request! We are going to give the community + + - name: Add Community Feedback Comment + if: steps.finder.outputs.comment-id == '' + uses: peter-evans/create-or-update-comment@v2 + with: + issue-number: ${{ github.event.issue.number }} + body: | + Thanks for the feature request! We are going to give the community 60 days from when this issue was created to provide 7 👍 upvotes on the opening comment to gauge general interest in this idea. If there's enough upvotes then we will consider this feature request in our future planning. If there's unfortunately not enough upvotes then we will close this issue. From 1840a1550e265f8761f959dd5276737308e36d26 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 3 Apr 2023 22:18:17 -0700 Subject: [PATCH 019/336] Move 'Run in ...' commands to sub-menu (#20965) Closes https://github.com/microsoft/vscode-python/issues/14350 ![image](https://user-images.githubusercontent.com/3840081/229255829-5ede6390-4411-4636-889b-d1a1d4b4360e.png) --- package.json | 26 ++++++++++++++++++++------ package.nls.json | 1 + 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 1331f98e88d3..a8d499ea463f 100644 --- a/package.json +++ b/package.json @@ -1702,6 +1702,19 @@ } ], "editor/context": [ + { + "submenu": "python.run", + "group": "Python", + "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported" + }, + { + "command": "python.sortImports", + "group": "Refactor", + "title": "%python.command.python.sortImports.title%", + "when": "editorLangId == python && !notebookEditorFocused && !virtualWorkspace && shellExecutionSupported" + } + ], + "python.run": [ { "command": "python.execInTerminal", "group": "Python", @@ -1716,12 +1729,6 @@ "command": "python.execSelectionInTerminal", "group": "Python", "when": "editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported" - }, - { - "command": "python.sortImports", - "group": "Refactor", - "title": "%python.command.python.sortImports.title%", - "when": "editorLangId == python && !notebookEditorFocused && !virtualWorkspace && shellExecutionSupported" } ], "editor/title": [ @@ -1767,6 +1774,13 @@ } ] }, + "submenus": [ + { + "id": "python.run", + "label": "%python.editor.context.submenu.runPython%", + "icon": "$(play)" + } + ], "viewsWelcome": [ { "view": "testing", diff --git a/package.nls.json b/package.nls.json index b36b1b829609..95cb54a1b509 100644 --- a/package.nls.json +++ b/package.nls.json @@ -25,6 +25,7 @@ "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", "python.menu.createNewFile.title": "Python File", + "python.editor.context.submenu.runPython": "Run Python", "python.activeStateToolPath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).", "python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", "python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).", From d637d92d018667a071d46404c05e9afafeaa95a9 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 4 Apr 2023 13:04:09 -0700 Subject: [PATCH 020/336] Switch to UUID from CWD for testing (#20980) switches to using the UUID of the discover/run call for unittest and pytest instead of the CWD. This allows for multiple calls from single workspace to be handled. --- .../testing/testController/common/server.ts | 6 +- .../testing/testController/common/types.ts | 2 +- .../pytest/pytestDiscoveryAdapter.ts | 81 +++++++++-------- .../pytest/pytestExecutionAdapter.ts | 86 +++++++++---------- .../unittest/testDiscoveryAdapter.ts | 46 +++++----- .../unittest/testExecutionAdapter.ts | 49 ++++++----- .../testDiscoveryAdapter.unit.test.ts | 13 ++- 7 files changed, 140 insertions(+), 143 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 48c0b81972af..98005c1eb160 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -46,12 +46,12 @@ export class PythonTestServer implements ITestServer, Disposable { // If yes, process the response, if not, ignore it. const cwd = this.uuids.get(uuid); if (cwd) { - this._onDataReceived.fire({ cwd, data }); + this._onDataReceived.fire({ uuid, data }); this.uuids.delete(uuid); } } catch (ex) { traceLog(`Error processing test server request: ${ex} observe`); - this._onDataReceived.fire({ cwd: '', data: '' }); + this._onDataReceived.fire({ uuid: '', data: '' }); } }; @@ -142,7 +142,7 @@ export class PythonTestServer implements ITestServer, Disposable { } catch (ex) { this.uuids.delete(uuid); this._onDataReceived.fire({ - cwd: options.cwd, + uuid, data: JSON.stringify({ status: 'error', errors: [(ex as Error).message], diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index e9eebb4c44d2..3bab4f02fd13 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -128,7 +128,7 @@ export type RawDiscoveredTests = { // New test discovery adapter types export type DataReceivedEvent = { - cwd: string; + uuid: string; data: string; }; diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index e2108b872845..2787ea180bdb 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -17,20 +17,19 @@ import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, ITestS * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied */ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { - private deferred: Deferred | undefined; + private promiseMap: Map> = new Map(); - private cwd: string | undefined; + private deferred: Deferred | undefined; constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { testServer.onDataReceived(this.onDataReceivedHandler, this); } - public onDataReceivedHandler({ cwd, data }: DataReceivedEvent): void { - if (this.deferred && cwd === this.cwd) { - const testData: DiscoveredTestPayload = JSON.parse(data); - - this.deferred.resolve(testData); - this.deferred = undefined; + public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { + const deferred = this.promiseMap.get(uuid); + if (deferred) { + deferred.resolve(JSON.parse(data)); + this.promiseMap.delete(uuid); } } @@ -51,43 +50,43 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // } async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { - if (!this.deferred) { - this.deferred = createDeferred(); - const relativePathToPytest = 'pythonFiles'; - const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - const uuid = this.testServer.createUUID(uri.fsPath); - const settings = this.configSettings.getSettings(uri); - const { pytestArgs } = settings.testing; + const deferred = createDeferred(); + this.deferred = createDeferred(); + const relativePathToPytest = 'pythonFiles'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + const uuid = this.testServer.createUUID(uri.fsPath); + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; - const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; - const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - const spawnOptions: SpawnOptions = { - cwd: uri.fsPath, - throwOnStdErr: true, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.testServer.getPort().toString(), - }, - }; + const spawnOptions: SpawnOptions = { + cwd: uri.fsPath, + throwOnStdErr: true, + extraVariables: { + PYTHONPATH: pythonPathCommand, + TEST_UUID: uuid.toString(), + TEST_PORT: this.testServer.getPort().toString(), + }, + }; - // Create the Python environment in which to execute the command. - const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { - allowEnvironmentFetchExceptions: false, - resource: uri, - }; - const execService = await executionFactory.createActivatedEnvironment(creationOptions); + // Create the Python environment in which to execute the command. + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); - try { - execService.exec( - ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), - spawnOptions, - ); - } catch (ex) { - console.error(ex); - } + try { + execService.exec( + ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), + spawnOptions, + ); + } catch (ex) { + console.error(ex); } - return this.deferred.promise; + + return deferred.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index eaabb57691d0..25b7d92b767b 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -12,20 +12,19 @@ import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestSe */ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { - private deferred: Deferred | undefined; + private promiseMap: Map> = new Map(); - private cwd: string | undefined; + private deferred: Deferred | undefined; constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { testServer.onDataReceived(this.onDataReceivedHandler, this); } - public onDataReceivedHandler({ cwd, data }: DataReceivedEvent): void { - if (this.deferred && cwd === this.cwd) { - const testData: ExecutionTestPayload = JSON.parse(data); - - this.deferred.resolve(testData); - this.deferred = undefined; + public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { + const deferred = this.promiseMap.get(uuid); + if (deferred) { + deferred.resolve(JSON.parse(data)); + this.promiseMap.delete(uuid); } } @@ -42,48 +41,45 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // debugBool?: boolean, // executionFactory?: IPythonExecutionFactory, // ): Promise { - // if (!this.deferred) { - // this.deferred = createDeferred(); - // const relativePathToPytest = 'pythonFiles'; - // const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - // this.configSettings.isTestExecution(); - // const uuid = this.testServer.createUUID(uri.fsPath); - // const settings = this.configSettings.getSettings(uri); - // const { pytestArgs } = settings.testing; + // const deferred = createDeferred(); + // const relativePathToPytest = 'pythonFiles'; + // const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + // this.configSettings.isTestExecution(); + // const uuid = this.testServer.createUUID(uri.fsPath); + // this.promiseMap.set(uuid, deferred); + // const settings = this.configSettings.getSettings(uri); + // const { pytestArgs } = settings.testing; - // const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; - // const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + // const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + // const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - // const spawnOptions: SpawnOptions = { - // cwd: uri.fsPath, - // throwOnStdErr: true, - // extraVariables: { - // PYTHONPATH: pythonPathCommand, - // TEST_UUID: uuid.toString(), - // TEST_PORT: this.testServer.getPort().toString(), - // }, - // }; + // const spawnOptions: SpawnOptions = { + // cwd: uri.fsPath, + // throwOnStdErr: true, + // extraVariables: { + // PYTHONPATH: pythonPathCommand, + // TEST_UUID: uuid.toString(), + // TEST_PORT: this.testServer.getPort().toString(), + // }, + // }; - // // Create the Python environment in which to execute the command. - // const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { - // allowEnvironmentFetchExceptions: false, - // resource: uri, - // }; - // // need to check what will happen in the exec service is NOT defined and is null - // const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + // // Create the Python environment in which to execute the command. + // const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + // allowEnvironmentFetchExceptions: false, + // resource: uri, + // }; + // // need to check what will happen in the exec service is NOT defined and is null + // const execService = await executionFactory?.createActivatedEnvironment(creationOptions); - // const testIdsString = testIds.join(' '); - // console.debug('what to do with debug bool?', debugBool); - // try { - // execService?.exec( - // ['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), - // spawnOptions, - // ); - // } catch (ex) { - // console.error(ex); - // } + // const testIdsString = testIds.join(' '); + // console.debug('what to do with debug bool?', debugBool); + // try { + // execService?.exec(['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), spawnOptions); + // } catch (ex) { + // console.error(ex); // } - // return this.deferred.promise; + + // return deferred.promise; // } // } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index f0a5a957807c..6e7d4371971b 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -19,7 +19,7 @@ import { * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. */ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { - private deferred: Deferred | undefined; + private promiseMap: Map> = new Map(); private cwd: string | undefined; @@ -27,38 +27,36 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { testServer.onDataReceived(this.onDataReceivedHandler, this); } - public onDataReceivedHandler({ cwd, data }: DataReceivedEvent): void { - if (this.deferred && cwd === this.cwd) { - const testData: DiscoveredTestPayload = JSON.parse(data); - - this.deferred.resolve(testData); - this.deferred = undefined; + public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { + const deferred = this.promiseMap.get(uuid); + if (deferred) { + deferred.resolve(JSON.parse(data)); + this.promiseMap.delete(uuid); } } public async discoverTests(uri: Uri): Promise { - if (!this.deferred) { - const settings = this.configSettings.getSettings(uri); - const { unittestArgs } = settings.testing; - - const command = buildDiscoveryCommand(unittestArgs); + const deferred = createDeferred(); + const settings = this.configSettings.getSettings(uri); + const { unittestArgs } = settings.testing; - this.cwd = uri.fsPath; + const command = buildDiscoveryCommand(unittestArgs); - const options: TestCommandOptions = { - workspaceFolder: uri, - command, - cwd: this.cwd, - }; + this.cwd = uri.fsPath; - this.deferred = createDeferred(); + const options: TestCommandOptions = { + workspaceFolder: uri, + command, + cwd: this.cwd, + }; + const uuid = this.testServer.createUUID(uri.fsPath); + this.promiseMap.set(uuid, deferred); - // Send the test command to the server. - // The server will fire an onDataReceived event once it gets a response. - this.testServer.sendCommand(options); - } + // Send the test command to the server. + // The server will fire an onDataReceived event once it gets a response. + this.testServer.sendCommand(options); - return this.deferred.promise; + return deferred.promise; } } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index d71dea9fea36..65cf1955970e 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -20,7 +20,7 @@ import { */ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { - private deferred: Deferred | undefined; + private promiseMap: Map> = new Map(); private cwd: string | undefined; @@ -28,38 +28,37 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { testServer.onDataReceived(this.onDataReceivedHandler, this); } - public onDataReceivedHandler({ cwd, data }: DataReceivedEvent): void { - if (this.deferred && cwd === this.cwd) { - const testData: ExecutionTestPayload = JSON.parse(data); - - this.deferred.resolve(testData); - this.deferred = undefined; + public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { + const deferred = this.promiseMap.get(uuid); + if (deferred) { + deferred.resolve(JSON.parse(data)); + this.promiseMap.delete(uuid); } } public async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { - if (!this.deferred) { - const settings = this.configSettings.getSettings(uri); - const { unittestArgs } = settings.testing; + const deferred = createDeferred(); + const settings = this.configSettings.getSettings(uri); + const { unittestArgs } = settings.testing; - const command = buildExecutionCommand(unittestArgs); - this.cwd = uri.fsPath; + const command = buildExecutionCommand(unittestArgs); + this.cwd = uri.fsPath; - const options: TestCommandOptions = { - workspaceFolder: uri, - command, - cwd: this.cwd, - debugBool, - testIds, - }; + const options: TestCommandOptions = { + workspaceFolder: uri, + command, + cwd: this.cwd, + debugBool, + testIds, + }; + const uuid = this.testServer.createUUID(uri.fsPath); + this.promiseMap.set(uuid, deferred); - this.deferred = createDeferred(); + // send test command to server + // server fire onDataReceived event once it gets response + this.testServer.sendCommand(options); - // send test command to server - // server fire onDataReceived event once it gets response - this.testServer.sendCommand(options); - } - return this.deferred.promise; + return deferred.promise; } } diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index cee4353db09a..3d80358678b7 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -31,6 +31,7 @@ suite('Unittest test discovery adapter', () => { onDataReceived: () => { // no body }, + createUUID: () => '123456789', } as unknown) as ITestServer; const uri = Uri.file('/foo/bar'); @@ -54,15 +55,16 @@ suite('Unittest test discovery adapter', () => { onDataReceived: () => { // no body }, + createUUID: () => '123456789', } as unknown) as ITestServer; const uri = Uri.file('/foo/bar'); const data = { status: 'success' }; - + const uuid = '123456789'; const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); const promise = adapter.discoverTests(uri); - adapter.onDataReceivedHandler({ cwd: uri.fsPath, data: JSON.stringify(data) }); + adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); const result = await promise; @@ -70,6 +72,8 @@ suite('Unittest test discovery adapter', () => { }); test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { + const correctUuid = '123456789'; + const incorrectUuid = '987654321'; const stubTestServer = ({ sendCommand(): Promise { return Promise.resolve(); @@ -77,6 +81,7 @@ suite('Unittest test discovery adapter', () => { onDataReceived: () => { // no body }, + createUUID: () => correctUuid, } as unknown) as ITestServer; const uri = Uri.file('/foo/bar'); @@ -85,10 +90,10 @@ suite('Unittest test discovery adapter', () => { const promise = adapter.discoverTests(uri); const data = { status: 'success' }; - adapter.onDataReceivedHandler({ cwd: 'some/other/path', data: JSON.stringify(data) }); + adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); const nextData = { status: 'error' }; - adapter.onDataReceivedHandler({ cwd: uri.fsPath, data: JSON.stringify(nextData) }); + adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); const result = await promise; From e97e237d52f3b18527134415dbde4884164c0893 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 5 Apr 2023 10:16:33 -0700 Subject: [PATCH 021/336] Automatically add and remove the "info needed" label (#20970) Closes https://github.com/microsoft/vscode-python/issues/20944 --- .github/workflows/triage-info-needed.yml | 100 +++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .github/workflows/triage-info-needed.yml diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml new file mode 100644 index 000000000000..fa878868699a --- /dev/null +++ b/.github/workflows/triage-info-needed.yml @@ -0,0 +1,100 @@ +name: Triage "info-needed" label + +on: + issue_comment: + types: [created] + +env: + TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd", "brettcannon"]' + +jobs: + add_label: + runs-on: ubuntu-latest + if: contains(github.event.issue.labels.*.name, 'triage-needed') && !contains(github.event.issue.labels.*.name, 'info-needed') + env: + KEYWORDS: '["\\?", "please", "kindly", "let me know", "try", "can you", "could you", "would you", "may I", "provide", "let us know", "tell me", "give me", "send me", "what", "when", "where", "why", "how"]' + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Check for author + uses: actions/github-script@v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issue = await github.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + const commentAuthor = context.payload.comment.user.login; + const commentBody = context.payload.comment.body; + const isTeamMember = ${{ env.TRIAGERS }}.includes(commentAuthor); + + const keywords = ${{ env.KEYWORDS }}; + const isRequestForInfo = new RegExp(keywords.join('|'), 'i').test(commentBody); + + const shouldAddLabel = isTeamMember && commentAuthor !== issue.data.user.login && isRequestForInfo; + + if (shouldAddLabel) { + await github.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['info-needed'] + }); + } + + remove_label: + if: contains(github.event.issue.labels.*.name, 'info-needed') && contains(github.event.issue.labels.*.name, 'triage-needed') + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Check for author + uses: actions/github-script@v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issue = await github.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + const commentAuthor = context.payload.comment.user.login; + const issueAuthor = issue.data.user.login; + if (commentAuthor === issueAuthor) { + await github.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: 'info-needed' + }); + return; + } + if (${{ env.TRIAGERS }}.includes(commentAuthor)) { + // If one of triagers made a comment, ignore it + return; + } + // Loop through all the comments on the issue in reverse order and find the last username that a TRIAGER mentioned + // If the comment author is the last mentioned username, remove the "info-needed" label + const comments = await github.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + for (const comment of comments.data.slice().reverse()) { + if (!${{ env.TRIAGERS }}.includes(comment.user.login)) { + continue; + } + const matches = comment.body.match(/@\w+/g) || []; + const mentionedUsernames = matches.map(match => match.replace('@', '')); + if (mentionedUsernames.includes(commentAuthor)) { + await github.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: 'info-needed' + }); + break; + } + } From 22ba6bd9243927d7e0eded835be1ddcb73d4fb28 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 5 Apr 2023 12:29:42 -0700 Subject: [PATCH 022/336] Don't show create env in diff editor (#20998) For https://github.com/microsoft/vscode-python/issues/20992 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a8d499ea463f..57d969221a86 100644 --- a/package.json +++ b/package.json @@ -1693,12 +1693,12 @@ { "group": "Python", "command": "python.createEnvironment", - "when": "resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported" + "when": "resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" }, { "group": "Python", "command": "python.createEnvironment", - "when": "resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported" + "when": "resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" } ], "editor/context": [ From 3c84470185edde5f16122b7462dd096b27b6fb8c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 5 Apr 2023 13:10:43 -0700 Subject: [PATCH 023/336] Fix UUID unittest (#20996) UUID is now handled by the adapter therefore is added to options sent to the server from the adapter. Since the UUID is no longer being added by the server, the server tests should no longer test if the server is correctly handling the UUID. This logic is now passed to the adapter and therefore tests related to processing non-existent UUID and UUID not currently active is the adapters role. --- .../testing/testController/common/server.ts | 2 +- .../testing/testController/common/types.ts | 1 + .../unittest/testDiscoveryAdapter.ts | 4 +- .../unittest/testExecutionAdapter.ts | 4 +- .../testController/server.unit.test.ts | 141 +----------------- .../testDiscoveryAdapter.unit.test.ts | 1 + 6 files changed, 14 insertions(+), 139 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 98005c1eb160..305e18f373ba 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -87,7 +87,7 @@ export class PythonTestServer implements ITestServer, Disposable { } async sendCommand(options: TestCommandOptions): Promise { - const uuid = this.createUUID(options.cwd); + const { uuid } = options; const spawnOptions: SpawnOptions = { token: options.token, cwd: options.cwd, diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 3bab4f02fd13..1fe480c45f26 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -146,6 +146,7 @@ export type TestCommandOptions = { workspaceFolder: Uri; cwd: string; command: TestDiscoveryCommand | TestExecutionCommand; + uuid: string; token?: CancellationToken; outChannel?: OutputChannel; debugBool?: boolean; diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 6e7d4371971b..fa411b50216f 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -43,13 +43,15 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const command = buildDiscoveryCommand(unittestArgs); this.cwd = uri.fsPath; + const uuid = this.testServer.createUUID(uri.fsPath); const options: TestCommandOptions = { workspaceFolder: uri, command, cwd: this.cwd, + uuid, }; - const uuid = this.testServer.createUUID(uri.fsPath); + this.promiseMap.set(uuid, deferred); // Send the test command to the server. diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 65cf1955970e..98ba8bfeb937 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -43,15 +43,17 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const command = buildExecutionCommand(unittestArgs); this.cwd = uri.fsPath; + const uuid = this.testServer.createUUID(uri.fsPath); const options: TestCommandOptions = { workspaceFolder: uri, command, cwd: this.cwd, + uuid, debugBool, testIds, }; - const uuid = this.testServer.createUUID(uri.fsPath); + this.promiseMap.set(uuid, deferred); // send test command to server diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 59aeeda333b2..4af3fd551007 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -48,11 +48,12 @@ suite('Python Test Server', () => { server.dispose(); }); - test('sendCommand should add the port and uuid to the command being sent', async () => { + test('sendCommand should add the port to the command being sent', async () => { const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', + uuid: fakeUuid, }; server = new PythonTestServer(stubExecutionFactory, debugLauncher); @@ -75,6 +76,7 @@ suite('Python Test Server', () => { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', + uuid: fakeUuid, outChannel, }; @@ -101,6 +103,7 @@ suite('Python Test Server', () => { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', + uuid: fakeUuid, }; server = new PythonTestServer(stubExecutionFactory, debugLauncher); @@ -116,53 +119,13 @@ suite('Python Test Server', () => { assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); }); - test('If the server receives data, it should fire an event if it is a known uuid', async () => { - const deferred = createDeferred(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - }; - - let response; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - server.onDataReceived(({ data }) => { - response = data; - deferred.resolve(); - }); - - await server.sendCommand(options); - - // Send data back. - const port = server.getPort(); - const requestOptions = { - hostname: 'localhost', - method: 'POST', - port, - headers: { 'Request-uuid': fakeUuid }, - }; - - const request = http.request(requestOptions, (res) => { - res.setEncoding('utf8'); - }); - - const postData = JSON.stringify({ status: 'success', uuid: fakeUuid }); - request.write(postData); - request.end(); - - await deferred.promise; - - assert.deepStrictEqual(response, postData); - }); test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { const deferred = createDeferred(); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', + uuid: fakeUuid, }; let response; @@ -198,98 +161,4 @@ suite('Python Test Server', () => { sinon.assert.calledOnce(traceLogStub); assert.deepStrictEqual(response, ''); }); - - test('If the server receives data, it should not fire an event if it is an unknown uuid', async () => { - const deferred = createDeferred(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - }; - - let response; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - server.onDataReceived(({ data }) => { - response = data; - deferred.resolve(); - }); - - await server.sendCommand(options); - - // Send data back. - const port = server.getPort(); - const requestOptions = { - hostname: 'localhost', - method: 'POST', - port, - headers: { 'Request-uuid': fakeUuid }, - }; - // request.hasHeader() - const request = http.request(requestOptions, (res) => { - res.setEncoding('utf8'); - }); - const postData = JSON.stringify({ status: 'success', uuid: fakeUuid, payload: 'foo' }); - request.write(postData); - request.end(); - - await deferred.promise; - - assert.deepStrictEqual(response, postData); - }); - - test('If the server receives data, it should not fire an event if there is no uuid', async () => { - const deferred = createDeferred(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - }; - - let response; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - server.onDataReceived(({ data }) => { - response = data; - deferred.resolve(); - }); - - await server.sendCommand(options); - - // Send data back. - const port = server.getPort(); - const requestOptions = { - hostname: 'localhost', - method: 'POST', - port, - headers: { 'Request-uuid': 'some-other-uuid' }, - }; - const requestOptions2 = { - hostname: 'localhost', - method: 'POST', - port, - headers: { 'Request-uuid': fakeUuid }, - }; - const requestOne = http.request(requestOptions, (res) => { - res.setEncoding('utf8'); - }); - const postDataOne = JSON.stringify({ status: 'success', uuid: 'some-other-uuid', payload: 'foo' }); - requestOne.write(postDataOne); - requestOne.end(); - - const requestTwo = http.request(requestOptions2, (res) => { - res.setEncoding('utf8'); - }); - const postDataTwo = JSON.stringify({ status: 'success', uuid: fakeUuid, payload: 'foo' }); - requestTwo.write(postDataTwo); - requestTwo.end(); - - await deferred.promise; - - assert.deepStrictEqual(response, postDataTwo); - }); }); diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index 3d80358678b7..113e4175fe69 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -44,6 +44,7 @@ suite('Unittest test discovery adapter', () => { workspaceFolder: uri, cwd: uri.fsPath, command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, + uuid: '123456789', }); }); From 7241760247d1a9a3134fe3d8e2913b5073b80c25 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 5 Apr 2023 16:25:23 -0700 Subject: [PATCH 024/336] Add `microvenv` support for non-windows platforms (#20985) This PR contains: 1. `microvenv` fallback if `venv` is not available (implemented in python with tests) 2. Updates to telemetry to include microvenv. Closes https://github.com/microsoft/vscode-python/issues/20905 --- pythonFiles/create_conda.py | 2 +- pythonFiles/create_microvenv.py | 97 ++++ pythonFiles/create_venv.py | 32 +- pythonFiles/tests/test_create_microvenv.py | 60 +++ pythonFiles/tests/test_create_venv.py | 35 +- requirements.in | 3 + requirements.txt | 8 +- src/client/common/utils/localize.ts | 4 + .../provider/venvProgressAndTelemetry.ts | 415 ++++++++++++------ src/client/telemetry/index.ts | 14 +- .../venvProgressAndTelemetry.unit.test.ts | 54 +++ 11 files changed, 570 insertions(+), 154 deletions(-) create mode 100644 pythonFiles/create_microvenv.py create mode 100644 pythonFiles/tests/test_create_microvenv.py create mode 100644 src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts diff --git a/pythonFiles/create_conda.py b/pythonFiles/create_conda.py index 9a34de47d51f..15320a8a1ce6 100644 --- a/pythonFiles/create_conda.py +++ b/pythonFiles/create_conda.py @@ -9,7 +9,7 @@ from typing import Optional, Sequence, Union CONDA_ENV_NAME = ".conda" -CWD = pathlib.PurePath(os.getcwd()) +CWD = pathlib.Path.cwd() class VenvError(Exception): diff --git a/pythonFiles/create_microvenv.py b/pythonFiles/create_microvenv.py new file mode 100644 index 000000000000..3dfd554a3012 --- /dev/null +++ b/pythonFiles/create_microvenv.py @@ -0,0 +1,97 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import os +import pathlib +import subprocess +import sys +import urllib.request as url_lib +from typing import Optional, Sequence + +VENV_NAME = ".venv" +LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python" +CWD = pathlib.Path.cwd() + + +class MicroVenvError(Exception): + pass + + +def run_process(args: Sequence[str], error_message: str) -> None: + try: + print("Running: " + " ".join(args)) + subprocess.run(args, cwd=os.getcwd(), check=True) + except subprocess.CalledProcessError: + raise MicroVenvError(error_message) + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser() + + parser.add_argument( + "--install-pip", + action="store_true", + default=False, + help="Install pip into the virtual environment.", + ) + + parser.add_argument( + "--name", + default=VENV_NAME, + type=str, + help="Name of the virtual environment.", + metavar="NAME", + action="store", + ) + return parser.parse_args(argv) + + +def create_microvenv(name: str): + run_process( + [sys.executable, os.fspath(LIB_ROOT / "microvenv.py"), name], + "CREATE_MICROVENV.MICROVENV_FAILED_CREATION", + ) + + +def download_pip_pyz(name: str): + url = "https://bootstrap.pypa.io/pip/pip.pyz" + print("CREATE_MICROVENV.DOWNLOADING_PIP") + + try: + with url_lib.urlopen(url) as response: + pip_pyz_path = os.fspath(CWD / name / "pip.pyz") + with open(pip_pyz_path, "wb") as out_file: + data = response.read() + out_file.write(data) + out_file.flush() + except Exception: + raise MicroVenvError("CREATE_MICROVENV.DOWNLOAD_PIP_FAILED") + + +def install_pip(name: str): + pip_pyz_path = os.fspath(CWD / name / "pip.pyz") + executable = os.fspath(CWD / name / "bin" / "python") + print("CREATE_MICROVENV.INSTALLING_PIP") + run_process( + [executable, pip_pyz_path, "install", "pip"], + "CREATE_MICROVENV.INSTALL_PIP_FAILED", + ) + + +def main(argv: Optional[Sequence[str]] = None) -> None: + if argv is None: + argv = [] + args = parse_args(argv) + + print("CREATE_MICROVENV.CREATING_MICROVENV") + create_microvenv(args.name) + print("CREATE_MICROVENV.CREATED_MICROVENV") + + if args.install_pip: + download_pip_pyz(args.name) + install_pip(args.name) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/pythonFiles/create_venv.py b/pythonFiles/create_venv.py index 2a2768c993ae..8ebafdfca32c 100644 --- a/pythonFiles/create_venv.py +++ b/pythonFiles/create_venv.py @@ -10,7 +10,8 @@ from typing import List, Optional, Sequence, Union VENV_NAME = ".venv" -CWD = pathlib.PurePath(os.getcwd()) +CWD = pathlib.Path.cwd() +MICROVENV_SCRIPT_PATH = pathlib.Path(__file__).parent / "create_microvenv.py" class VenvError(Exception): @@ -130,22 +131,39 @@ def main(argv: Optional[Sequence[str]] = None) -> None: argv = [] args = parse_args(argv) + use_micro_venv = False if not is_installed("venv"): - raise VenvError("CREATE_VENV.VENV_NOT_FOUND") + if sys.platform == "win32": + raise VenvError("CREATE_VENV.VENV_NOT_FOUND") + else: + use_micro_venv = True pip_installed = is_installed("pip") deps_needed = args.requirements or args.extras or args.toml - if deps_needed and not pip_installed: + if deps_needed and not pip_installed and not use_micro_venv: raise VenvError("CREATE_VENV.PIP_NOT_FOUND") if venv_exists(args.name): venv_path = get_venv_path(args.name) print(f"EXISTING_VENV:{venv_path}") else: - run_process( - [sys.executable, "-m", "venv", args.name], - "CREATE_VENV.VENV_FAILED_CREATION", - ) + if use_micro_venv: + run_process( + [ + sys.executable, + os.fspath(MICROVENV_SCRIPT_PATH), + "--install-pip", + "--name", + args.name, + ], + "CREATE_VENV.MICROVENV_FAILED_CREATION", + ) + pip_installed = True + else: + run_process( + [sys.executable, "-m", "venv", args.name], + "CREATE_VENV.VENV_FAILED_CREATION", + ) venv_path = get_venv_path(args.name) print(f"CREATED_VENV:{venv_path}") if args.git_ignore: diff --git a/pythonFiles/tests/test_create_microvenv.py b/pythonFiles/tests/test_create_microvenv.py new file mode 100644 index 000000000000..26a57dda26a1 --- /dev/null +++ b/pythonFiles/tests/test_create_microvenv.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import importlib +import os +import sys + +import create_microvenv +import pytest + + +def test_create_microvenv(): + importlib.reload(create_microvenv) + run_process_called = False + + def run_process(args, error_message): + nonlocal run_process_called + run_process_called = True + assert args == [ + sys.executable, + os.fspath(create_microvenv.LIB_ROOT / "microvenv.py"), + create_microvenv.VENV_NAME, + ] + assert error_message == "CREATE_MICROVENV.MICROVENV_FAILED_CREATION" + + create_microvenv.run_process = run_process + + create_microvenv.main() + assert run_process_called == True + + +def test_create_microvenv_with_pip(): + importlib.reload(create_microvenv) + + download_pip_pyz_called = False + + def download_pip_pyz(name): + nonlocal download_pip_pyz_called + download_pip_pyz_called = True + assert name == create_microvenv.VENV_NAME + + create_microvenv.download_pip_pyz = download_pip_pyz + + run_process_called = False + + def run_process(args, error_message): + if "install" in args and "pip" in args: + nonlocal run_process_called + run_process_called = True + pip_pyz_path = os.fspath( + create_microvenv.CWD / create_microvenv.VENV_NAME / "pip.pyz" + ) + executable = os.fspath( + create_microvenv.CWD / create_microvenv.VENV_NAME / "bin" / "python" + ) + assert args == [executable, pip_pyz_path, "install", "pip"] + assert error_message == "CREATE_MICROVENV.INSTALL_PIP_FAILED" + + create_microvenv.run_process = run_process + create_microvenv.main(["--install-pip"]) diff --git a/pythonFiles/tests/test_create_venv.py b/pythonFiles/tests/test_create_venv.py index 95ec863373d8..2949a18ebd40 100644 --- a/pythonFiles/tests/test_create_venv.py +++ b/pythonFiles/tests/test_create_venv.py @@ -2,13 +2,46 @@ # Licensed under the MIT License. import importlib +import os import sys import create_venv import pytest -def test_venv_not_installed(): +@pytest.mark.skipif( + sys.platform == "win32", reason="Windows does not have micro venv fallback." +) +def test_venv_not_installed_unix(): + importlib.reload(create_venv) + create_venv.is_installed = lambda module: module != "venv" + run_process_called = False + + def run_process(args, error_message): + nonlocal run_process_called + if "--install-pip" in args: + run_process_called = True + assert args == [ + sys.executable, + os.fspath(create_venv.MICROVENV_SCRIPT_PATH), + "--install-pip", + "--name", + ".test_venv", + ] + assert error_message == "CREATE_VENV.MICROVENV_FAILED_CREATION" + + create_venv.run_process = run_process + + create_venv.main(["--name", ".test_venv"]) + + # run_process is called when the venv does not exist + assert run_process_called == True + + +@pytest.mark.skipif( + sys.platform != "win32", reason="Windows does not have microvenv fallback." +) +def test_venv_not_installed_windows(): importlib.reload(create_venv) create_venv.is_installed = lambda module: module != "venv" with pytest.raises(create_venv.VenvError) as e: diff --git a/requirements.in b/requirements.in index c394c0feb0cf..8b76e392917e 100644 --- a/requirements.in +++ b/requirements.in @@ -5,3 +5,6 @@ # Unittest test adapter typing-extensions==4.5.0 + +# Fallback env creator for debian +microvenv diff --git a/requirements.txt b/requirements.txt index 1cdb049c430a..07145e1832d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,13 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # pip-compile --generate-hashes requirements.in # +microvenv==2023.2.0 \ + --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ + --hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3 + # via -r requirements.in typing-extensions==4.5.0 \ --hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \ --hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4 diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 509287d87533..10c70c8c6cd0 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -438,7 +438,11 @@ export namespace CreateEnv { export namespace Venv { export const creating = l10n.t('Creating venv...'); + export const creatingMicrovenv = l10n.t('Creating microvenv...'); export const created = l10n.t('Environment created...'); + export const existing = l10n.t('Using existing environment...'); + export const downloadingPip = l10n.t('Downloading pip...'); + export const installingPip = l10n.t('Installing pip...'); export const upgradingPip = l10n.t('Upgrading pip...'); export const installingPackages = l10n.t('Installing packages...'); export const errorCreatingEnvironment = l10n.t('Error while creating virtual environment.'); diff --git a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts index f9de4a94689a..b8efff3afbd8 100644 --- a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts +++ b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts @@ -8,151 +8,294 @@ import { CreateEnvironmentProgress } from '../types'; export const VENV_CREATED_MARKER = 'CREATED_VENV:'; export const VENV_EXISTING_MARKER = 'EXISTING_VENV:'; -export const INSTALLING_REQUIREMENTS = 'VENV_INSTALLING_REQUIREMENTS:'; -export const INSTALLING_PYPROJECT = 'VENV_INSTALLING_PYPROJECT:'; -export const PIP_NOT_INSTALLED_MARKER = 'CREATE_VENV.PIP_NOT_FOUND'; -export const VENV_NOT_INSTALLED_MARKER = 'CREATE_VENV.VENV_NOT_FOUND'; -export const INSTALL_REQUIREMENTS_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS'; -export const INSTALL_PYPROJECT_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT'; -export const CREATE_VENV_FAILED_MARKER = 'CREATE_VENV.VENV_FAILED_CREATION'; -export const VENV_ALREADY_EXISTS_MARKER = 'CREATE_VENV.VENV_ALREADY_EXISTS'; -export const INSTALLED_REQUIREMENTS_MARKER = 'CREATE_VENV.PIP_INSTALLED_REQUIREMENTS'; -export const INSTALLED_PYPROJECT_MARKER = 'CREATE_VENV.PIP_INSTALLED_PYPROJECT'; -export const UPGRADE_PIP_FAILED_MARKER = 'CREATE_VENV.UPGRADE_PIP_FAILED'; -export const UPGRADING_PIP_MARKER = 'CREATE_VENV.UPGRADING_PIP'; -export const UPGRADED_PIP_MARKER = 'CREATE_VENV.UPGRADED_PIP'; +const INSTALLING_REQUIREMENTS = 'VENV_INSTALLING_REQUIREMENTS:'; +const INSTALLING_PYPROJECT = 'VENV_INSTALLING_PYPROJECT:'; +const PIP_NOT_INSTALLED_MARKER = 'CREATE_VENV.PIP_NOT_FOUND'; +const VENV_NOT_INSTALLED_MARKER = 'CREATE_VENV.VENV_NOT_FOUND'; +const INSTALL_REQUIREMENTS_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS'; +const INSTALL_PYPROJECT_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT'; +const CREATE_VENV_FAILED_MARKER = 'CREATE_VENV.VENV_FAILED_CREATION'; +const VENV_ALREADY_EXISTS_MARKER = 'CREATE_VENV.VENV_ALREADY_EXISTS'; +const INSTALLED_REQUIREMENTS_MARKER = 'CREATE_VENV.PIP_INSTALLED_REQUIREMENTS'; +const INSTALLED_PYPROJECT_MARKER = 'CREATE_VENV.PIP_INSTALLED_PYPROJECT'; +const UPGRADE_PIP_FAILED_MARKER = 'CREATE_VENV.UPGRADE_PIP_FAILED'; +const UPGRADING_PIP_MARKER = 'CREATE_VENV.UPGRADING_PIP'; +const UPGRADED_PIP_MARKER = 'CREATE_VENV.UPGRADED_PIP'; +const CREATING_MICROVENV_MARKER = 'CREATE_MICROVENV.CREATING_MICROVENV'; +const CREATE_MICROVENV_FAILED_MARKER = 'CREATE_VENV.MICROVENV_FAILED_CREATION'; +const CREATE_MICROVENV_FAILED_MARKER2 = 'CREATE_MICROVENV.MICROVENV_FAILED_CREATION'; +const MICROVENV_CREATED_MARKER = 'CREATE_MICROVENV.CREATED_MICROVENV'; +const INSTALLING_PIP_MARKER = 'CREATE_MICROVENV.INSTALLING_PIP'; +const INSTALL_PIP_FAILED_MARKER = 'CREATE_MICROVENV.INSTALL_PIP_FAILED'; +const DOWNLOADING_PIP_MARKER = 'CREATE_MICROVENV.DOWNLOADING_PIP'; +const DOWNLOAD_PIP_FAILED_MARKER = 'CREATE_MICROVENV.DOWNLOAD_PIP_FAILED'; export class VenvProgressAndTelemetry { - private venvCreatedReported = false; + private readonly processed = new Set(); - private venvOrPipMissingReported = false; - - private venvUpgradingPipReported = false; - - private venvUpgradedPipReported = false; - - private venvFailedReported = false; - - private venvInstallingPackagesReported = false; - - private venvInstallingPackagesFailedReported = false; - - private venvInstalledPackagesReported = false; + private readonly reportActions = new Map string | undefined>([ + [ + VENV_CREATED_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.created }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'created', + }); + return undefined; + }, + ], + [ + VENV_EXISTING_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.existing }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'existing', + }); + return undefined; + }, + ], + [ + INSTALLING_REQUIREMENTS, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPackages }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return undefined; + }, + ], + [ + INSTALLING_PYPROJECT, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPackages }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return undefined; + }, + ], + [ + PIP_NOT_INSTALLED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noPip', + }); + return PIP_NOT_INSTALLED_MARKER; + }, + ], + [ + VENV_NOT_INSTALLED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noVenv', + }); + return VENV_NOT_INSTALLED_MARKER; + }, + ], + [ + INSTALL_REQUIREMENTS_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return INSTALL_REQUIREMENTS_FAILED_MARKER; + }, + ], + [ + INSTALL_PYPROJECT_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return INSTALL_PYPROJECT_FAILED_MARKER; + }, + ], + [ + CREATE_VENV_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'other', + }); + return CREATE_VENV_FAILED_MARKER; + }, + ], + [ + VENV_ALREADY_EXISTS_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'existing', + }); + return undefined; + }, + ], + [ + INSTALLED_REQUIREMENTS_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return undefined; + }, + ], + [ + INSTALLED_PYPROJECT_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return undefined; + }, + ], + [ + UPGRADED_PIP_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + return undefined; + }, + ], + [ + UPGRADE_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + return UPGRADE_PIP_FAILED_MARKER; + }, + ], + [ + DOWNLOADING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.downloadingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipDownload', + }); + return undefined; + }, + ], + [ + DOWNLOAD_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipDownload', + }); + return DOWNLOAD_PIP_FAILED_MARKER; + }, + ], + [ + INSTALLING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipInstall', + }); + return undefined; + }, + ], + [ + INSTALL_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipInstall', + }); + return INSTALL_PIP_FAILED_MARKER; + }, + ], + [ + CREATING_MICROVENV_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.creatingMicrovenv }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'microvenv', + pythonVersion: undefined, + }); + return undefined; + }, + ], + [ + CREATE_MICROVENV_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'microvenv', + reason: 'other', + }); + return CREATE_MICROVENV_FAILED_MARKER; + }, + ], + [ + CREATE_MICROVENV_FAILED_MARKER2, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'microvenv', + reason: 'other', + }); + return CREATE_MICROVENV_FAILED_MARKER2; + }, + ], + [ + MICROVENV_CREATED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'microvenv', + reason: 'created', + }); + return undefined; + }, + ], + [ + UPGRADING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.upgradingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'microvenv', + using: 'pipUpgrade', + }); + return undefined; + }, + ], + ]); private lastError: string | undefined = undefined; constructor(private readonly progress: CreateEnvironmentProgress) {} - public process(output: string): void { - if (!this.venvCreatedReported && output.includes(VENV_CREATED_MARKER)) { - this.venvCreatedReported = true; - this.progress.report({ - message: CreateEnv.Venv.created, - }); - sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { - environmentType: 'venv', - reason: 'created', - }); - } else if (!this.venvCreatedReported && output.includes(VENV_EXISTING_MARKER)) { - this.venvCreatedReported = true; - this.progress.report({ - message: CreateEnv.Venv.created, - }); - sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { - environmentType: 'venv', - reason: 'existing', - }); - } else if (!this.venvOrPipMissingReported && output.includes(VENV_NOT_INSTALLED_MARKER)) { - this.venvOrPipMissingReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { - environmentType: 'venv', - reason: 'noVenv', - }); - this.lastError = VENV_NOT_INSTALLED_MARKER; - } else if (!this.venvOrPipMissingReported && output.includes(PIP_NOT_INSTALLED_MARKER)) { - this.venvOrPipMissingReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { - environmentType: 'venv', - reason: 'noPip', - }); - this.lastError = PIP_NOT_INSTALLED_MARKER; - } else if (!this.venvUpgradingPipReported && output.includes(UPGRADING_PIP_MARKER)) { - this.venvUpgradingPipReported = true; - this.progress.report({ - message: CreateEnv.Venv.upgradingPip, - }); - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { - environmentType: 'venv', - using: 'pipUpgrade', - }); - } else if (!this.venvFailedReported && output.includes(CREATE_VENV_FAILED_MARKER)) { - this.venvFailedReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { - environmentType: 'venv', - reason: 'other', - }); - this.lastError = CREATE_VENV_FAILED_MARKER; - } else if (!this.venvInstallingPackagesReported && output.includes(INSTALLING_REQUIREMENTS)) { - this.venvInstallingPackagesReported = true; - this.progress.report({ - message: CreateEnv.Venv.installingPackages, - }); - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { - environmentType: 'venv', - using: 'requirements.txt', - }); - } else if (!this.venvInstallingPackagesReported && output.includes(INSTALLING_PYPROJECT)) { - this.venvInstallingPackagesReported = true; - this.progress.report({ - message: CreateEnv.Venv.installingPackages, - }); - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { - environmentType: 'venv', - using: 'pyproject.toml', - }); - } else if (!this.venvInstallingPackagesFailedReported && output.includes(UPGRADE_PIP_FAILED_MARKER)) { - this.venvInstallingPackagesFailedReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { - environmentType: 'venv', - using: 'pipUpgrade', - }); - this.lastError = UPGRADE_PIP_FAILED_MARKER; - } else if (!this.venvInstallingPackagesFailedReported && output.includes(INSTALL_REQUIREMENTS_FAILED_MARKER)) { - this.venvInstallingPackagesFailedReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { - environmentType: 'venv', - using: 'requirements.txt', - }); - this.lastError = INSTALL_REQUIREMENTS_FAILED_MARKER; - } else if (!this.venvInstallingPackagesFailedReported && output.includes(INSTALL_PYPROJECT_FAILED_MARKER)) { - this.venvInstallingPackagesFailedReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { - environmentType: 'venv', - using: 'pyproject.toml', - }); - this.lastError = INSTALL_PYPROJECT_FAILED_MARKER; - } else if (!this.venvUpgradedPipReported && output.includes(UPGRADED_PIP_MARKER)) { - this.venvUpgradedPipReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { - environmentType: 'venv', - using: 'pipUpgrade', - }); - } else if (!this.venvInstalledPackagesReported && output.includes(INSTALLED_REQUIREMENTS_MARKER)) { - this.venvInstalledPackagesReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { - environmentType: 'venv', - using: 'requirements.txt', - }); - } else if (!this.venvInstalledPackagesReported && output.includes(INSTALLED_PYPROJECT_MARKER)) { - this.venvInstalledPackagesReported = true; - sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { - environmentType: 'venv', - using: 'pyproject.toml', - }); - } - } - public getLastError(): string | undefined { return this.lastError; } + + public process(output: string): void { + const keys: string[] = Array.from(this.reportActions.keys()); + + for (const key of keys) { + if (output.includes(key) && !this.processed.has(key)) { + const action = this.reportActions.get(key); + if (action) { + const err = action(this.progress); + if (err) { + this.lastError = err; + } + } + this.processed.add(key); + } + } + } } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 973e55302635..df2b454cbe27 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2014,7 +2014,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_CREATING]: { - environmentType: 'venv' | 'conda'; + environmentType: 'venv' | 'conda' | 'microvenv'; pythonVersion: string | undefined; }; /** @@ -2027,7 +2027,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_CREATED]: { - environmentType: 'venv' | 'conda'; + environmentType: 'venv' | 'conda' | 'microvenv'; reason: 'created' | 'existing'; }; /** @@ -2040,7 +2040,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_FAILED]: { - environmentType: 'venv' | 'conda'; + environmentType: 'venv' | 'conda' | 'microvenv'; reason: 'noVenv' | 'noPip' | 'other'; }; /** @@ -2053,8 +2053,8 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_INSTALLING_PACKAGES]: { - environmentType: 'venv' | 'conda'; - using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipUpgrade'; + environmentType: 'venv' | 'conda' | 'microvenv'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipUpgrade' | 'pipInstall' | 'pipDownload'; }; /** * Telemetry event sent after installing packages. @@ -2079,8 +2079,8 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED]: { - environmentType: 'venv' | 'conda'; - using: 'pipUpgrade' | 'requirements.txt' | 'pyproject.toml' | 'environment.yml'; + environmentType: 'venv' | 'conda' | 'microvenv'; + using: 'pipUpgrade' | 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipDownload' | 'pipInstall'; }; /** * Telemetry event sent when a linter or formatter extension is already installed. diff --git a/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts new file mode 100644 index 000000000000..ecb7d1434ada --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { + VENV_CREATED_MARKER, + VenvProgressAndTelemetry, +} from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; +import { CreateEnvironmentProgress } from '../../../../client/pythonEnvironments/creation/types'; +import * as telemetry from '../../../../client/telemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; + +suite('Venv Progress and Telemetry', () => { + let sendTelemetryEventStub: sinon.SinonStub; + let progressReporterMock: typemoq.IMock; + + setup(() => { + sendTelemetryEventStub = sinon.stub(telemetry, 'sendTelemetryEvent'); + progressReporterMock = typemoq.Mock.ofType(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Ensure telemetry event and progress are sent', async () => { + const progressReporter = progressReporterMock.object; + progressReporterMock + .setup((p) => p.report({ message: CreateEnv.Venv.created })) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + + const progressAndTelemetry = new VenvProgressAndTelemetry(progressReporter); + progressAndTelemetry.process(VENV_CREATED_MARKER); + assert.isTrue(sendTelemetryEventStub.calledOnce); + progressReporterMock.verifyAll(); + }); + + test('Do not trigger telemetry event the second time', async () => { + const progressReporter = progressReporterMock.object; + progressReporterMock + .setup((p) => p.report({ message: CreateEnv.Venv.created })) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + + const progressAndTelemetry = new VenvProgressAndTelemetry(progressReporter); + progressAndTelemetry.process(VENV_CREATED_MARKER); + progressAndTelemetry.process(VENV_CREATED_MARKER); + assert.isTrue(sendTelemetryEventStub.calledOnce); + progressReporterMock.verifyAll(); + }); +}); From 51d7963f796d9f010873764d6389a75c0a4e5201 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 6 Apr 2023 13:56:40 -0700 Subject: [PATCH 025/336] Update to latest `debugpy` version 1.6.7 (#21001) --- pythonFiles/install_debugpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonFiles/install_debugpy.py b/pythonFiles/install_debugpy.py index b7f663c01907..cabb620ea1f2 100644 --- a/pythonFiles/install_debugpy.py +++ b/pythonFiles/install_debugpy.py @@ -13,7 +13,7 @@ DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") DEBUGGER_PACKAGE = "debugpy" DEBUGGER_PYTHON_ABI_VERSIONS = ("cp310",) -DEBUGGER_VERSION = "1.6.6" # can also be "latest" +DEBUGGER_VERSION = "1.6.7" # can also be "latest" def _contains(s, parts=()): From e1b74f97e73eaa44f8482ffbfb359f15c87a9b4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 13:57:17 -0700 Subject: [PATCH 026/336] Bump actions/checkout from 2 to 3 (#21006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
Release notes

Sourced from actions/checkout's releases.

v3.0.0

  • Updated to the node16 runtime by default
    • This requires a minimum Actions Runner version of v2.285.0 to run, which is by default available in GHES 3.4 or later.

v2.7.0

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2.6.0...v2.7.0

v2.6.0

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2.5.0...v2.6.0

v2.5.0

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2...v2.5.0

v2.4.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2...v2.4.2

v2.4.1

  • Fixed an issue where checkout failed to run in container jobs due to the new git setting safe.directory

v2.4.0

  • Convert SSH URLs like org-<ORG_ID>@github.com: to https://github.com/ - pr

v2.3.5

Update dependencies

v2.3.4

v2.3.3

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

v3.4.0

v3.3.0

v3.2.0

v3.1.0

v3.0.2

v3.0.1

v3.0.0

v2.3.1

v2.3.0

v2.2.0

v2.1.1

  • Changes to support GHES (here and here)

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/triage-info-needed.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index fa878868699a..1431d27e01c3 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -15,7 +15,7 @@ jobs: KEYWORDS: '["\\?", "please", "kindly", "let me know", "try", "can you", "could you", "would you", "may I", "provide", "let us know", "tell me", "give me", "send me", "what", "when", "where", "why", "how"]' steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Check for author uses: actions/github-script@v4 with: @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Check for author uses: actions/github-script@v4 with: From 562ed79db57723d426fed0dcf98163c6c0f43f96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 13:57:29 -0700 Subject: [PATCH 027/336] Bump actions/github-script from 4 to 6 (#21007) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/github-script](https://github.com/actions/github-script) from 4 to 6.
Release notes

Sourced from actions/github-script's releases.

v6.0.0

What's Changed

Breaking Changes

With the update to Node 16 in #235, all scripts will now be run with Node 16 rather than Node 12.

New Contributors

Full Changelog: https://github.com/actions/github-script/compare/v5...v6.0.0

v5.2.0

What's Changed

Full Changelog: https://github.com/actions/github-script/compare/v5.1.1...v5.2.0

v5.1.1

What's Changed

Full Changelog: https://github.com/actions/github-script/compare/v5.1.0...v5.1.1

v5.1.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/github-script/compare/v5.0.0...v5.1.0

v5.0.0

What's Changed

Breaking Changes

As part of this update, the Octokit context available via github no longer has REST methods directly. These methods are available via github.rest.* - https://github.com/octokit/plugin-rest-endpoint-methods.js/releases/tag/v5.0.0

... (truncated)

Commits
  • 98814c5 Merge pull request #339 from actions/joshmgross/bump-package-version
  • ba6cf3f Bump version to 6.4.0
  • bcc3891 Merge pull request #321 from danmichaelo/node-fetch
  • da88180 Merge main
  • 4d93f38 Update dist and audit deps
  • 0550e85 Merge pull request #336 from actions/joshmgross/minimatch-license
  • 5420835 Merge branch 'main' into joshmgross/minimatch-license
  • 0337783 Merge pull request #338 from actions/joshmgross/fix-dist
  • 8c6dda0 Regenerate dist
  • ee0d992 Use Node 16 in CI and examples
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/github-script&package-manager=github_actions&previous-version=4&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/triage-info-needed.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index 1431d27e01c3..3b4165c74c55 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -17,7 +17,7 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - name: Check for author - uses: actions/github-script@v4 + uses: actions/github-script@v6 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -51,7 +51,7 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - name: Check for author - uses: actions/github-script@v4 + uses: actions/github-script@v6 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | From 407717ee4d32bc1ed9e032332210cc49f4142886 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 13:57:47 -0700 Subject: [PATCH 028/336] Bump peter-evans/create-or-update-comment from 2 to 3 (#21005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 2 to 3.
Release notes

Sourced from peter-evans/create-or-update-comment's releases.

Create or Update Comment v3.0.0

What's new

  • Now supports replacing reactions on a comment by setting input reactions-edit-mode to replace. The default behaviour has not changed and will append reactions.
  • Chose how the action should append the comment body with input append-separator. The default behaviour is to add a newline. Other options are space and none.
  • body-file is deprecated in favour of body-path. The behaviour is unchanged.

What's Changed

New Contributors

Full Changelog: https://github.com/peter-evans/create-or-update-comment/compare/v2.1.1...v3.0.0

Create or Update Comment v2.1.1

⚙️ Fixes the recent Json5 vulnerability.

What's Changed

Full Changelog: https://github.com/peter-evans/create-or-update-comment/compare/v2.1.0...v2.1.1

Create or Update Comment v2.1.0

⭐ Adds input body-file, the path to a file containing the comment body.

What's Changed

New Contributors

Full Changelog: https://github.com/peter-evans/create-or-update-comment/compare/v2.0.1...v2.1.0

Create or Update Comment v2.0.1

⚙️ Bumps @actions/core to transition away from deprecated runner commands.

What's Changed

Full Changelog: https://github.com/peter-evans/create-or-update-comment/compare/v2.0.0...v2.0.1

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=peter-evans/create-or-update-comment&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/community-feedback-auto-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/community-feedback-auto-comment.yml b/.github/workflows/community-feedback-auto-comment.yml index 584af90d6d82..cbba6db30b50 100644 --- a/.github/workflows/community-feedback-auto-comment.yml +++ b/.github/workflows/community-feedback-auto-comment.yml @@ -23,7 +23,7 @@ jobs: - name: Add Community Feedback Comment if: steps.finder.outputs.comment-id == '' - uses: peter-evans/create-or-update-comment@v2 + uses: peter-evans/create-or-update-comment@v3 with: issue-number: ${{ github.event.issue.number }} body: | From d036567b4fea96dd791b66102a5a9362f52c10b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Pi=C3=B1a=20Martinez?= Date: Thu, 6 Apr 2023 23:00:37 +0200 Subject: [PATCH 029/336] Remove not-existent files from .eslintignore (#21011) Solves #17181 --- .eslintignore | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.eslintignore b/.eslintignore index ca0ffe5f5847..ad69cab31ea7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -48,8 +48,6 @@ src/test/testing/common/services/configSettingService.unit.test.ts src/test/common/exitCIAfterTestReporter.ts -src/test/common/net/fileDownloader.unit.test.ts -src/test/common/net/httpClient.unit.test.ts src/test/common/terminals/activator/index.unit.test.ts src/test/common/terminals/activator/base.unit.test.ts @@ -76,7 +74,6 @@ src/test/common/platform/filesystem.test.ts src/test/common/utils/cacheUtils.unit.test.ts src/test/common/utils/decorators.unit.test.ts -src/test/common/utils/localize.functional.test.ts src/test/common/utils/version.unit.test.ts src/test/common/configSettings/configSettings.unit.test.ts @@ -108,7 +105,6 @@ src/test/common/interpreterPathService.unit.test.ts src/test/pythonFiles/formatting/dummy.ts -src/test/debugger/extension/banner.unit.test.ts src/test/debugger/extension/adapter/adapter.test.ts src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts src/test/debugger/extension/adapter/factory.unit.test.ts @@ -124,7 +120,6 @@ src/test/telemetry/index.unit.test.ts src/test/telemetry/envFileTelemetry.unit.test.ts src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts -src/test/application/diagnostics/checks/pythonPathDeprecated.unit.test.ts src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts @@ -139,7 +134,6 @@ src/test/performance/load.perf.test.ts src/client/interpreter/configuration/interpreterSelector/commands/base.ts src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts -src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts src/client/interpreter/configuration/services/globalUpdaterService.ts src/client/interpreter/configuration/services/workspaceUpdaterService.ts @@ -177,12 +171,9 @@ src/client/testing/common/runner.ts src/client/common/helpers.ts src/client/common/net/browser.ts -src/client/common/net/fileDownloader.ts -src/client/common/net/httpClient.ts src/client/common/net/socket/socketCallbackHandler.ts src/client/common/net/socket/socketServer.ts src/client/common/net/socket/SocketStream.ts -src/client/common/asyncDisposableRegistry.ts src/client/common/editor.ts src/client/common/contextKey.ts src/client/common/experiments/telemetry.ts From 2401e13f440dc8a36c8ceeff9b231f0c036107f5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 6 Apr 2023 14:32:55 -0700 Subject: [PATCH 030/336] Add an automation to close issue with response if Python 2.7 was selected (#21003) For https://github.com/microsoft/vscode-python/issues/21002 --- .github/workflows/python27-issue-response.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/python27-issue-response.yml diff --git a/.github/workflows/python27-issue-response.yml b/.github/workflows/python27-issue-response.yml new file mode 100644 index 000000000000..4d51e9921ab4 --- /dev/null +++ b/.github/workflows/python27-issue-response.yml @@ -0,0 +1,14 @@ +on: + issues: + types: [opened] + +jobs: + python27-issue-response: + runs-on: ubuntu-latest + if: "contains(github.event.issue.body, 'Python version (& distribution if applicable, e.g. Anaconda): 2.7')" + steps: + - name: Check for Python 2.7 string + run: | + response="We're sorry, but we no longer support Python 2.7. If you need to work with Python 2.7, you will have to pin to 2022.2.* version of the extension, which was the last version that had the debugger (debugpy) with support for python 2.7, and was tested with `2.7`. Thank you for your understanding! \n ![https://user-images.githubusercontent.com/51720070/80000627-39dacc00-8472-11ea-9755-ac7ba0acbb70.gif](https://user-images.githubusercontent.com/51720070/80000627-39dacc00-8472-11ea-9755-ac7ba0acbb70.gif)" + gh issue comment ${{ github.event.issue.number }} --body "$response" + gh issue close ${{ github.event.issue.number }} From 869dc64f4734064ccb44011446398e1275647867 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 6 Apr 2023 15:15:35 -0700 Subject: [PATCH 031/336] switch to using tcp for comm with server (#20981) code written by @karthiknadig and @eleanorjboyd, which switches to using TCP as the communication channel between the test adapter in the extension and the node server that handles the discovery/running of python tests. --------- Co-authored-by: Karthik Nadig --- pythonFiles/unittestadapter/discovery.py | 15 ++-- pythonFiles/unittestadapter/execution.py | 15 ++-- .../testing/testController/common/server.ts | 72 +++++++++++-------- .../testing/testController/common/utils.ts | 45 ++++++++++++ .../testController/server.unit.test.ts | 48 ++++++------- .../testing/testController/utils.unit.test.ts | 67 +++++++++++++++++ 6 files changed, 193 insertions(+), 69 deletions(-) create mode 100644 src/test/testing/testController/utils.unit.test.ts diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index 0be09e986ca8..5c31219fa52b 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -121,13 +121,16 @@ def discover_tests( # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) - with socket_manager.SocketManager(addr) as s: - data = json.dumps(payload) - request = f"""POST / HTTP/1.1 -Host: localhost:{port} -Content-Length: {len(data)} + data = json.dumps(payload) + request = f"""Content-Length: {len(data)} Content-Type: application/json Request-uuid: {uuid} {data}""" - result = s.socket.sendall(request.encode("utf-8")) # type: ignore + try: + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) + except Exception as e: + print(f"Error sending response: {e}") + print(f"Request data: {request}") diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index a926bcdcc09e..b556181f57d6 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -222,13 +222,16 @@ def run_tests( # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) - with socket_manager.SocketManager(addr) as s: - data = json.dumps(payload) - request = f"""POST / HTTP/1.1 -Host: localhost:{port} -Content-Length: {len(data)} + data = json.dumps(payload) + request = f"""Content-Length: {len(data)} Content-Type: application/json Request-uuid: {uuid} {data}""" - result = s.socket.sendall(request.encode("utf-8")) # type: ignore + try: + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) + except Exception as e: + print(f"Error sending response: {e}") + print(f"Request data: {request}") diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 305e18f373ba..6c6bd5b0d970 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as http from 'http'; import * as net from 'net'; import * as crypto from 'crypto'; import { Disposable, Event, EventEmitter } from 'vscode'; @@ -14,53 +13,66 @@ import { traceLog } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; +import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); private uuids: Map; - private server: http.Server; + private server: net.Server; private ready: Promise; constructor(private executionFactory: IPythonExecutionFactory, private debugLauncher: ITestDebugLauncher) { this.uuids = new Map(); - const requestListener: http.RequestListener = async (request, response) => { - const buffers = []; - - try { - for await (const chunk of request) { - buffers.push(chunk); - } - - const data = Buffer.concat(buffers).toString(); - // grab the uuid from the header - const indexRequestuuid = request.rawHeaders.indexOf('Request-uuid'); - const uuid = request.rawHeaders[indexRequestuuid + 1]; - response.end(); - - JSON.parse(data); - // Check if the uuid we received exists in the list of active ones. - // If yes, process the response, if not, ignore it. - const cwd = this.uuids.get(uuid); - if (cwd) { - this._onDataReceived.fire({ uuid, data }); - this.uuids.delete(uuid); + this.server = net.createServer((socket: net.Socket) => { + socket.on('data', (data: Buffer) => { + try { + let rawData: string = data.toString(); + + while (rawData.length > 0) { + const rpcHeaders = jsonRPCHeaders(rawData); + const uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); + rawData = rpcHeaders.remainingRawData; + if (uuid) { + const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); + rawData = rpcContent.remainingRawData; + const cwd = this.uuids.get(uuid); + if (cwd) { + this._onDataReceived.fire({ uuid, data: rpcContent.extractedJSON }); + this.uuids.delete(uuid); + } + } else { + traceLog(`Error processing test server request: uuid not found`); + this._onDataReceived.fire({ uuid: '', data: '' }); + return; + } + } + } catch (ex) { + traceLog(`Error processing test server request: ${ex} observe`); + this._onDataReceived.fire({ uuid: '', data: '' }); } - } catch (ex) { - traceLog(`Error processing test server request: ${ex} observe`); - this._onDataReceived.fire({ uuid: '', data: '' }); - } - }; - - this.server = http.createServer(requestListener); + }); + }); this.ready = new Promise((resolve, _reject) => { this.server.listen(undefined, 'localhost', () => { resolve(); }); }); + this.server.on('error', (ex) => { + traceLog(`Error starting test server: ${ex}`); + }); + this.server.on('close', () => { + traceLog('Test server closed.'); + }); + this.server.on('listening', () => { + traceLog('Test server listening.'); + }); + this.server.on('connection', () => { + traceLog('Test server connected to a client.'); + }); } public serverReady(): Promise { diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 13fc76a37199..e0bad383d695 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -5,3 +5,48 @@ export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}\r\n`; } +export interface IJSONRPCContent { + extractedJSON: string; + remainingRawData: string; +} + +export interface IJSONRPCHeaders { + headers: Map; + remainingRawData: string; +} + +export const JSONRPC_UUID_HEADER = 'Request-uuid'; +export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length'; +export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type'; + +export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { + const lines = rawData.split('\n'); + let remainingRawData = ''; + const headerMap = new Map(); + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + if (line === '') { + remainingRawData = lines.slice(i + 1).join('\n'); + break; + } + const [key, value] = line.split(':'); + if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { + headerMap.set(key.trim(), value.trim()); + } + } + + return { + headers: headerMap, + remainingRawData, + }; +} + +export function jsonRPCContent(headers: Map, rawData: string): IJSONRPCContent { + const length = parseInt(headers.get('Content-Length') ?? '0', 10); + const data = rawData.slice(0, length); + const remainingRawData = rawData.slice(length); + return { + extractedJSON: data, + remainingRawData, + }; +} diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 4af3fd551007..d7b3a242ee9a 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -2,15 +2,14 @@ // Licensed under the MIT License. import * as assert from 'assert'; -import * as http from 'http'; +import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; import { OutputChannel, Uri } from 'vscode'; import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; -import { createDeferred } from '../../../client/common/utils/async'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; -import * as logging from '../../../client/logging'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { createDeferred } from '../../../client/common/utils/async'; suite('Python Test Server', () => { const fakeUuid = 'fake-uuid'; @@ -21,13 +20,11 @@ suite('Python Test Server', () => { let sandbox: sinon.SinonSandbox; let execArgs: string[]; let v4Stub: sinon.SinonStub; - let traceLogStub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; setup(() => { sandbox = sinon.createSandbox(); v4Stub = sandbox.stub(crypto, 'randomUUID'); - traceLogStub = sandbox.stub(logging, 'traceLog'); v4Stub.returns(fakeUuid); stubExecutionService = ({ @@ -120,7 +117,10 @@ suite('Python Test Server', () => { }); test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { + let eventData: string | undefined; + const client = new net.Socket(); const deferred = createDeferred(); + const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -128,37 +128,31 @@ suite('Python Test Server', () => { uuid: fakeUuid, }; - let response; + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); - server.onDataReceived(({ data }) => { - response = data; + eventData = data; deferred.resolve(); }); - await server.sendCommand(options); - - // Send data back. - const port = server.getPort(); - const requestOptions = { - hostname: 'localhost', - method: 'POST', - port, - headers: { 'Request-uuid': fakeUuid }, - }; - - const request = http.request(requestOptions, (res) => { - res.setEncoding('utf8'); + client.on('connect', () => { + console.log('Socket connected, local port:', client.localPort); + client.write('malformed data'); + client.end(); + }); + client.on('error', (error) => { + console.log('Socket connection error:', error); }); - const postData = '[test'; - request.write(postData); - request.end(); + await server.sendCommand(options); await deferred.promise; - - sinon.assert.calledOnce(traceLogStub); - assert.deepStrictEqual(response, ''); + assert.deepStrictEqual(eventData, ''); }); }); diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts new file mode 100644 index 000000000000..d971c7d37c9f --- /dev/null +++ b/src/test/testing/testController/utils.unit.test.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { + JSONRPC_CONTENT_LENGTH_HEADER, + JSONRPC_CONTENT_TYPE_HEADER, + JSONRPC_UUID_HEADER, + jsonRPCContent, + jsonRPCHeaders, +} from '../../../client/testing/testController/common/utils'; + +suite('Test Controller Utils: JSON RPC', () => { + test('Empty raw data string', async () => { + const rawDataString = ''; + + const output = jsonRPCHeaders(rawDataString); + assert.deepStrictEqual(output.headers.size, 0); + assert.deepStrictEqual(output.remainingRawData, ''); + }); + + test('Valid data empty JSON', async () => { + const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 2\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n{}`; + + const rpcHeaders = jsonRPCHeaders(rawDataString); + assert.deepStrictEqual(rpcHeaders.headers.size, 3); + assert.deepStrictEqual(rpcHeaders.remainingRawData, '{}'); + const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, '{}'); + }); + + test('Valid data NO JSON', async () => { + const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 0\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n`; + + const rpcHeaders = jsonRPCHeaders(rawDataString); + assert.deepStrictEqual(rpcHeaders.headers.size, 3); + assert.deepStrictEqual(rpcHeaders.remainingRawData, ''); + const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, ''); + }); + + test('Valid data with full JSON', async () => { + // this is just some random JSON + const json = + '{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}'; + const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; + + const rpcHeaders = jsonRPCHeaders(rawDataString); + assert.deepStrictEqual(rpcHeaders.headers.size, 3); + assert.deepStrictEqual(rpcHeaders.remainingRawData, json); + const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, json); + }); + + test('Valid data with multiple JSON', async () => { + const json = + '{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}'; + const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; + const rawDataString2 = rawDataString + rawDataString; + + const rpcHeaders = jsonRPCHeaders(rawDataString2); + assert.deepStrictEqual(rpcHeaders.headers.size, 3); + const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, json); + assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString); + }); +}); From d844f7d2c8951b541d371ecafe0663e3b1a0490f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 15:47:58 -0700 Subject: [PATCH 032/336] Bump mheap/github-action-required-labels from 3 to 4 (#20975) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mheap/github-action-required-labels](https://github.com/mheap/github-action-required-labels) from 3 to 4.
Release notes

Sourced from mheap/github-action-required-labels's releases.

v4

Tag that always points to the latest commit in the v4.x.x series of releases

v4.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/mheap/github-action-required-labels/compare/v3.0.1...v4.0.0

v3.0.1

What's Changed

Full Changelog: https://github.com/mheap/github-action-required-labels/compare/v3.0.0...v3.0.1

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mheap/github-action-required-labels&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 7563d4d44dcc..e953f62d2011 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'PR impact specified' - uses: mheap/github-action-required-labels@v3 + uses: mheap/github-action-required-labels@v4 with: mode: exactly count: 1 From 6fbfde9d200268480794ae9a2c84c179d074b6b8 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 7 Apr 2023 09:17:27 -0700 Subject: [PATCH 033/336] fix typing to be 3.7 compatible (#21012) --- pythonFiles/unittestadapter/discovery.py | 4 ++-- pythonFiles/unittestadapter/execution.py | 27 ++++++++++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index 5c31219fa52b..dc0a139ed5a2 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -8,7 +8,7 @@ import sys import traceback import unittest -from typing import List, Literal, Optional, Tuple, TypedDict, Union +from typing import List, Literal, Optional, Tuple, Union # Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -22,7 +22,7 @@ # Add the lib path to sys.path to find the typing_extensions module. sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) -from typing_extensions import NotRequired +from typing_extensions import NotRequired, TypedDict DEFAULT_PORT = "45454" diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index b556181f57d6..6075a1480acb 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -9,7 +9,7 @@ import traceback import unittest from types import TracebackType -from typing import Dict, List, Optional, Tuple, Type, TypeAlias, TypedDict +from typing import Dict, List, Optional, Tuple, Type, Union # Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -17,13 +17,15 @@ # Add the lib path to sys.path to find the typing_extensions module. sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) from testing_tools import socket_manager -from typing_extensions import NotRequired +from typing_extensions import NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args DEFAULT_PORT = "45454" -def parse_execution_cli_args(args: List[str]) -> Tuple[int, str | None, List[str]]: +def parse_execution_cli_args( + args: List[str], +) -> Tuple[int, Union[str, None], List[str]]: """Parse command-line arguments that should be processed by the script. So far this includes the port number that it needs to connect to, the uuid passed by the TS side, @@ -43,9 +45,9 @@ def parse_execution_cli_args(args: List[str]) -> Tuple[int, str | None, List[str return (int(parsed_args.port), parsed_args.uuid, parsed_args.testids) -ErrorType = ( - Tuple[Type[BaseException], BaseException, TracebackType] | Tuple[None, None, None] -) +ErrorType = Union[ + Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] +] class TestOutcomeEnum(str, enum.Enum): @@ -60,7 +62,7 @@ class TestOutcomeEnum(str, enum.Enum): class UnittestTestResult(unittest.TextTestResult): - formatted: Dict[str, Dict[str, str | None]] = dict() + formatted: Dict[str, Dict[str, Union[str, None]]] = dict() def startTest(self, test: unittest.TestCase): super(UnittestTestResult, self).startTest(test) @@ -98,7 +100,10 @@ def addUnexpectedSuccess(self, test: unittest.TestCase): self.formatResult(test, TestOutcomeEnum.unexpected_success) def addSubTest( - self, test: unittest.TestCase, subtest: unittest.TestCase, err: ErrorType | None + self, + test: unittest.TestCase, + subtest: unittest.TestCase, + err: Union[ErrorType, None], ): super(UnittestTestResult, self).addSubTest(test, subtest, err) self.formatResult( @@ -112,8 +117,8 @@ def formatResult( self, test: unittest.TestCase, outcome: str, - error: ErrorType | None = None, - subtest: unittest.TestCase | None = None, + error: Union[ErrorType, None] = None, + subtest: Union[unittest.TestCase, None] = None, ): tb = None if error and error[2] is not None: @@ -141,7 +146,7 @@ class TestExecutionStatus(str, enum.Enum): success = "success" -TestResultTypeAlias: TypeAlias = Dict[str, Dict[str, str | None]] +TestResultTypeAlias: TypeAlias = Dict[str, Dict[str, Union[str, None]]] class PayloadDict(TypedDict): From b771c0e91e754cd075ba9acc54daa128870030c6 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 7 Apr 2023 12:49:09 -0700 Subject: [PATCH 034/336] add tests for unittest execution adapter (#21017) --- .../testExecutionAdapter.unit.test.ts | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts new file mode 100644 index 000000000000..f8648c8963fa --- /dev/null +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IConfigurationService } from '../../../../client/common/types'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; +import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; + +suite('Unittest test execution adapter', () => { + let stubConfigSettings: IConfigurationService; + + setup(() => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + } as unknown) as IConfigurationService; + }); + + test('runTests should send the run command to the test server', async () => { + let options: TestCommandOptions | undefined; + + const stubTestServer = ({ + sendCommand(opt: TestCommandOptions): Promise { + options = opt; + return Promise.resolve(); + }, + onDataReceived: () => { + // no body + }, + createUUID: () => '123456789', + } as unknown) as ITestServer; + + const uri = Uri.file('/foo/bar'); + const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + adapter.runTests(uri, [], false); + + const expectedOptions: TestCommandOptions = { + workspaceFolder: uri, + command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, + cwd: uri.fsPath, + uuid: '123456789', + debugBool: false, + testIds: [], + }; + + assert.deepStrictEqual(options, expectedOptions); + }); + test("onDataReceivedHandler should parse the data if the cwd from the payload matches the test adapter's cwd", async () => { + const stubTestServer = ({ + sendCommand(): Promise { + return Promise.resolve(); + }, + onDataReceived: () => { + // no body + }, + createUUID: () => '123456789', + } as unknown) as ITestServer; + + const uri = Uri.file('/foo/bar'); + const data = { status: 'success' }; + const uuid = '123456789'; + + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + + // triggers runTests flow which will run onDataReceivedHandler and the + // promise resolves into the parsed data. + const promise = adapter.runTests(uri, [], false); + + adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); + + const result = await promise; + + assert.deepStrictEqual(result, data); + }); + test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { + const correctUuid = '123456789'; + const incorrectUuid = '987654321'; + const stubTestServer = ({ + sendCommand(): Promise { + return Promise.resolve(); + }, + onDataReceived: () => { + // no body + }, + createUUID: () => correctUuid, + } as unknown) as ITestServer; + + const uri = Uri.file('/foo/bar'); + + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + + // triggers runTests flow which will run onDataReceivedHandler and the + // promise resolves into the parsed data. + const promise = adapter.runTests(uri, [], false); + + const data = { status: 'success' }; + // will not resolve due to incorrect UUID + adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); + + const nextData = { status: 'error' }; + // will resolve and nextData will be returned as result + adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); + + const result = await promise; + + assert.deepStrictEqual(result, nextData); + }); +}); From e288f8b8190b9ec8509c5c69a34bd938172c67ad Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 7 Apr 2023 14:25:10 -0700 Subject: [PATCH 035/336] Switch to List Data Structure for Checking Known UUIDs in Test Server (#21018) continue off of the work done related to switching from checking UUIDS in the server file to now doing so in the adapter file. --- .../testing/testController/common/server.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 6c6bd5b0d970..1b7d46c995e7 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -18,15 +18,13 @@ import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); - private uuids: Map; + private uuids: Array = []; private server: net.Server; private ready: Promise; constructor(private executionFactory: IPythonExecutionFactory, private debugLauncher: ITestDebugLauncher) { - this.uuids = new Map(); - this.server = net.createServer((socket: net.Socket) => { socket.on('data', (data: Buffer) => { try { @@ -36,14 +34,11 @@ export class PythonTestServer implements ITestServer, Disposable { const rpcHeaders = jsonRPCHeaders(rawData); const uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); rawData = rpcHeaders.remainingRawData; - if (uuid) { + if (uuid && this.uuids.includes(uuid)) { const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); rawData = rpcContent.remainingRawData; - const cwd = this.uuids.get(uuid); - if (cwd) { - this._onDataReceived.fire({ uuid, data: rpcContent.extractedJSON }); - this.uuids.delete(uuid); - } + this._onDataReceived.fire({ uuid, data: rpcContent.extractedJSON }); + this.uuids = this.uuids.filter((u) => u !== uuid); } else { traceLog(`Error processing test server request: uuid not found`); this._onDataReceived.fire({ uuid: '', data: '' }); @@ -83,9 +78,9 @@ export class PythonTestServer implements ITestServer, Disposable { return (this.server.address() as net.AddressInfo).port; } - public createUUID(cwd: string): string { + public createUUID(): string { const uuid = crypto.randomUUID(); - this.uuids.set(uuid, cwd); + this.uuids.push(uuid); return uuid; } @@ -152,7 +147,7 @@ export class PythonTestServer implements ITestServer, Disposable { await execService.exec(args, spawnOptions); } } catch (ex) { - this.uuids.delete(uuid); + this.uuids = this.uuids.filter((u) => u !== uuid); this._onDataReceived.fire({ uuid, data: JSON.stringify({ From f4f883c6bf6503ad00c8664ed161ac17fc79e6d4 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 7 Apr 2023 17:33:44 -0700 Subject: [PATCH 036/336] Fix info needed workflow (#21021) --- .github/workflows/triage-info-needed.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index 3b4165c74c55..51c5b610f03f 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -14,14 +14,12 @@ jobs: env: KEYWORDS: '["\\?", "please", "kindly", "let me know", "try", "can you", "could you", "would you", "may I", "provide", "let us know", "tell me", "give me", "send me", "what", "when", "where", "why", "how"]' steps: - - name: Checkout code - uses: actions/checkout@v3 - name: Check for author uses: actions/github-script@v6 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const issue = await github.issues.get({ + const issue = await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number @@ -36,7 +34,7 @@ jobs: const shouldAddLabel = isTeamMember && commentAuthor !== issue.data.user.login && isRequestForInfo; if (shouldAddLabel) { - await github.issues.addLabels({ + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, @@ -48,14 +46,12 @@ jobs: if: contains(github.event.issue.labels.*.name, 'info-needed') && contains(github.event.issue.labels.*.name, 'triage-needed') runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v3 - name: Check for author uses: actions/github-script@v6 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const issue = await github.issues.get({ + const issue = await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number @@ -63,7 +59,7 @@ jobs: const commentAuthor = context.payload.comment.user.login; const issueAuthor = issue.data.user.login; if (commentAuthor === issueAuthor) { - await github.issues.removeLabel({ + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, @@ -77,7 +73,7 @@ jobs: } // Loop through all the comments on the issue in reverse order and find the last username that a TRIAGER mentioned // If the comment author is the last mentioned username, remove the "info-needed" label - const comments = await github.issues.listComments({ + const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number @@ -89,7 +85,7 @@ jobs: const matches = comment.body.match(/@\w+/g) || []; const mentionedUsernames = matches.map(match => match.replace('@', '')); if (mentionedUsernames.includes(commentAuthor)) { - await github.issues.removeLabel({ + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, From c64bb0e5403c41b951501f786460fe7b92d65511 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 10 Apr 2023 10:00:02 -0700 Subject: [PATCH 037/336] Add Testing for PytestExecutionAdapter (#21019) --- .../pytestExecutionAdapter.unit.test.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts new file mode 100644 index 000000000000..ac6c6bd274a4 --- /dev/null +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -0,0 +1,90 @@ +// /* eslint-disable @typescript-eslint/no-explicit-any */ +// // Copyright (c) Microsoft Corporation. All rights reserved. +// // Licensed under the MIT License. +// import * as assert from 'assert'; +// import { Uri } from 'vscode'; +// import * as typeMoq from 'typemoq'; +// import { IConfigurationService } from '../../../../client/common/types'; +// import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; +// import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; +// import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +// import { PytestTestExecutionAdapter } from '../../../../client/testing/testController/pytest/pytestExecutionAdapter'; + +// suite('pytest test execution adapter', () => { +// let testServer: typeMoq.IMock; +// let configService: IConfigurationService; +// let execFactory = typeMoq.Mock.ofType(); +// let adapter: PytestTestExecutionAdapter; +// let execService: typeMoq.IMock; +// let deferred: Deferred; +// setup(() => { +// testServer = typeMoq.Mock.ofType(); +// testServer.setup((t) => t.getPort()).returns(() => 12345); +// testServer +// .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) +// .returns(() => ({ +// dispose: () => { +// /* no-body */ +// }, +// })); +// configService = ({ +// getSettings: () => ({ +// testing: { pytestArgs: ['.'] }, +// }), +// isTestExecution: () => false, +// } as unknown) as IConfigurationService; +// execFactory = typeMoq.Mock.ofType(); +// execService = typeMoq.Mock.ofType(); +// execFactory +// .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) +// .returns(() => Promise.resolve(execService.object)); +// deferred = createDeferred(); +// execService +// .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) +// .returns(() => { +// deferred.resolve(); +// return Promise.resolve({ stdout: '{}' }); +// }); +// execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); +// execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); +// }); +// test('onDataReceivedHandler should parse only if known UUID', async () => { +// const uri = Uri.file('/my/test/path/'); +// const uuid = 'uuid123'; +// const data = { status: 'success' }; +// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); +// const eventData: DataReceivedEvent = { +// uuid, +// data: JSON.stringify(data), +// }; + +// adapter = new PytestTestExecutionAdapter(testServer.object, configService); +// const promise = adapter.runTests(uri, [], false); +// await deferred.promise; +// adapter.onDataReceivedHandler(eventData); +// const result = await promise; +// assert.deepStrictEqual(result, data); +// }); +// test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { +// const uri = Uri.file('/my/test/path/'); +// const uuid = 'uuid456'; +// let data = { status: 'error' }; +// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); +// const wrongUriEventData: DataReceivedEvent = { +// uuid: 'incorrect-uuid456', +// data: JSON.stringify(data), +// }; +// adapter = new PytestTestExecutionAdapter(testServer.object, configService); +// const promise = adapter.runTests(uri, [], false); +// adapter.onDataReceivedHandler(wrongUriEventData); + +// data = { status: 'success' }; +// const correctUriEventData: DataReceivedEvent = { +// uuid, +// data: JSON.stringify(data), +// }; +// adapter.onDataReceivedHandler(correctUriEventData); +// const result = await promise; +// assert.deepStrictEqual(result, data); +// }); +// }); From 983f05aa34226432191cf8b019668d6d16441184 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 10 Apr 2023 14:17:36 -0700 Subject: [PATCH 038/336] Add testing for unittest execution python logic (#21022) these tests cover: - parsing execution args for unittest - test run with no test_ids attached - test run with single test_id and test is a success. --------- Co-authored-by: Karthik Nadig --- .../tests/unittestadapter/test_execution.py | 94 +++++++++++++++++++ pythonFiles/unittestadapter/execution.py | 4 +- 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 pythonFiles/tests/unittestadapter/test_execution.py diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py new file mode 100644 index 000000000000..14ddefa48d52 --- /dev/null +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +from typing import List + +import pytest +from unittestadapter.execution import parse_execution_cli_args, run_tests + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" + + +@pytest.mark.parametrize( + "args, expected", + [ + ( + [ + "--port", + "111", + "--uuid", + "fake-uuid", + "--testids", + "test_file.test_class.test_method", + ], + (111, "fake-uuid", ["test_file.test_class.test_method"]), + ), + ( + ["--port", "111", "--uuid", "fake-uuid", "--testids", ""], + (111, "fake-uuid", [""]), + ), + ( + [ + "--port", + "111", + "--uuid", + "fake-uuid", + "--testids", + "test_file.test_class.test_method", + "-v", + "-s", + ], + (111, "fake-uuid", ["test_file.test_class.test_method"]), + ), + ], +) +def test_parse_execution_cli_args(args: List[str], expected: List[str]) -> None: + """The parse_execution_cli_args function should return values for the port, uuid, and testids arguments + when passed as command-line options, and ignore unrecognized arguments. + """ + actual = parse_execution_cli_args(args) + assert actual == expected + + +def test_no_ids_run() -> None: + """This test runs on an empty array of test_ids, therefore it should return + an empty dict for the result. + """ + start_dir: str = os.fspath(TEST_DATA_PATH) + testids = [] + pattern = "discovery_simple*" + actual = run_tests(start_dir, testids, pattern, None, "fake-uuid") + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + if "result" in actual: + assert len(actual["result"]) == 0 + else: + raise AssertionError("actual['result'] is None") + + +def test_single_ids_run() -> None: + """This test runs on a single test_id, therefore it should return + a dict with a single key-value pair for the result. + + This single test passes so the outcome should be 'success'. + """ + id = "discovery_simple.DiscoverySimple.test_one" + actual = run_tests( + os.fspath(TEST_DATA_PATH), [id], "discovery_simple*", None, "fake-uuid" + ) + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert "result" in actual + result = actual["result"] + assert len(result) == 1 + assert id in result + id_result = result[id] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == "success" diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 6075a1480acb..569ad7fdf298 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -206,11 +206,11 @@ def run_tests( if error is not None: payload["error"] = error + else: + status = TestExecutionStatus.success payload["status"] = status - # print(f"payload: \n{json.dumps(payload, indent=4)}") - return payload From 92744f7d52d1c3eb4268177270050917650882a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 18:49:57 -0700 Subject: [PATCH 039/336] Bump xml2js from 0.4.23 to 0.5.0 (#21032) Bumps [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) from 0.4.23 to 0.5.0.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=xml2js&package-manager=npm_and_yarn&previous-version=0.4.23&new-version=0.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 80 ++++++++++++++++++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index f445bf7f42e9..9b537745c9bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", - "xml2js": "^0.4.19" + "xml2js": "^0.5.0" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", @@ -221,6 +221,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, + "node_modules/@azure/core-http/node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/@azure/core-tracing": { "version": "1.0.0-preview.13", "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", @@ -11900,6 +11912,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "node_modules/schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -14270,6 +14287,19 @@ "node": ">=8.17.0" } }, + "node_modules/vsce/node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/vscode-debugadapter": { "version": "1.35.0", "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.35.0.tgz", @@ -14890,9 +14920,9 @@ "dev": true }, "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -14901,11 +14931,6 @@ "node": ">=4.0.0" } }, - "node_modules/xml2js/node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, "node_modules/xmlbuilder": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", @@ -15249,6 +15274,15 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } } } }, @@ -24487,6 +24521,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -26361,6 +26400,16 @@ "requires": { "rimraf": "^3.0.0" } + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } } } }, @@ -26828,19 +26877,12 @@ "dev": true }, "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "requires": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" - }, - "dependencies": { - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - } } }, "xmlbuilder": { diff --git a/package.json b/package.json index 57d969221a86..08d56bdf3bc0 100644 --- a/package.json +++ b/package.json @@ -1875,7 +1875,7 @@ "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", - "xml2js": "^0.4.19" + "xml2js": "^0.5.0" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", From d61377653e8b6f01c2efebb97ab33860484216e2 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 11 Apr 2023 08:33:11 -0700 Subject: [PATCH 040/336] Fix debugging when using "internalConsole" (#21033) Closes https://github.com/microsoft/vscode-python/issues/20828 Do case-insensitive merge of environment variables, always make sure to return the standard env key for an OS, similar to `process.env`. --- src/client/common/platform/fs-paths.ts | 6 +- src/client/common/variables/environment.ts | 63 ++++++++++++++++--- .../configuration/resolvers/helper.ts | 5 +- .../variables/envVarsService.unit.test.ts | 25 ++++---- src/test/debugger/envVars.test.ts | 7 ++- 5 files changed, 83 insertions(+), 23 deletions(-) diff --git a/src/client/common/platform/fs-paths.ts b/src/client/common/platform/fs-paths.ts index 18a1fea363b7..2d46fca98526 100644 --- a/src/client/common/platform/fs-paths.ts +++ b/src/client/common/platform/fs-paths.ts @@ -145,7 +145,11 @@ export class FileSystemPathUtils implements IFileSystemPathUtils { } export function normCasePath(filePath: string): string { - return getOSType() === OSType.Windows ? nodepath.normalize(filePath).toUpperCase() : nodepath.normalize(filePath); + return normCase(nodepath.normalize(filePath)); +} + +export function normCase(s: string): string { + return getOSType() === OSType.Windows ? s.toUpperCase() : s; } /** diff --git a/src/client/common/variables/environment.ts b/src/client/common/variables/environment.ts index 06d3ef3af414..4b3652e5021f 100644 --- a/src/client/common/variables/environment.ts +++ b/src/client/common/variables/environment.ts @@ -10,6 +10,7 @@ import { EventName } from '../../telemetry/constants'; import { IFileSystem } from '../platform/types'; import { IPathUtils } from '../types'; import { EnvironmentVariables, IEnvironmentVariablesService } from './types'; +import { normCase } from '../platform/fs-paths'; @injectable() export class EnvironmentVariablesService implements IEnvironmentVariablesService { @@ -61,6 +62,9 @@ export class EnvironmentVariablesService implements IEnvironmentVariablesService if (!target) { return; } + const reference = target; + target = normCaseKeys(target); + source = normCaseKeys(source); const settingsNotToMerge = ['PYTHONPATH', this.pathVariable]; Object.keys(source).forEach((setting) => { if (settingsNotToMerge.indexOf(setting) >= 0) { @@ -70,6 +74,8 @@ export class EnvironmentVariablesService implements IEnvironmentVariablesService target[setting] = source[setting]; } }); + restoreKeys(target); + matchTarget(reference, target); } public appendPythonPath(vars: EnvironmentVariables, ...pythonPaths: string[]) { @@ -80,18 +86,24 @@ export class EnvironmentVariablesService implements IEnvironmentVariablesService return this.appendPaths(vars, this.pathVariable, ...paths); } - private get pathVariable(): 'Path' | 'PATH' { + private get pathVariable(): string { if (!this._pathVariable) { this._pathVariable = this.pathUtils.getPathVariableName(); } - return this._pathVariable!; + return normCase(this._pathVariable)!; } - private appendPaths( - vars: EnvironmentVariables, - variableName: 'PATH' | 'Path' | 'PYTHONPATH', - ...pathsToAppend: string[] - ) { + private appendPaths(vars: EnvironmentVariables, variableName: string, ...pathsToAppend: string[]) { + const reference = vars; + vars = normCaseKeys(vars); + variableName = normCase(variableName); + vars = this._appendPaths(vars, variableName, ...pathsToAppend); + restoreKeys(vars); + matchTarget(reference, vars); + return vars; + } + + private _appendPaths(vars: EnvironmentVariables, variableName: string, ...pathsToAppend: string[]) { const valueToAppend = pathsToAppend .filter((item) => typeof item === 'string' && item.trim().length > 0) .map((item) => item.trim()) @@ -183,3 +195,40 @@ function substituteEnvVars( return value.replace(/\\\$/g, '$'); } + +export function normCaseKeys(env: EnvironmentVariables): EnvironmentVariables { + const normalizedEnv: EnvironmentVariables = {}; + Object.keys(env).forEach((key) => { + const normalizedKey = normCase(key); + normalizedEnv[normalizedKey] = env[key]; + }); + return normalizedEnv; +} + +export function restoreKeys(env: EnvironmentVariables) { + const processEnvKeys = Object.keys(process.env); + processEnvKeys.forEach((processEnvKey) => { + const originalKey = normCase(processEnvKey); + if (originalKey !== processEnvKey && env[originalKey] !== undefined) { + env[processEnvKey] = env[originalKey]; + delete env[originalKey]; + } + }); +} + +export function matchTarget(reference: EnvironmentVariables, target: EnvironmentVariables): void { + Object.keys(reference).forEach((key) => { + if (target.hasOwnProperty(key)) { + reference[key] = target[key]; + } else { + delete reference[key]; + } + }); + + // Add any new keys from target to reference + Object.keys(target).forEach((key) => { + if (!reference.hasOwnProperty(key)) { + reference[key] = target[key]; + } + }); +} diff --git a/src/client/debugger/extension/configuration/resolvers/helper.ts b/src/client/debugger/extension/configuration/resolvers/helper.ts index 0ccaa9964054..96cdf65bfac3 100644 --- a/src/client/debugger/extension/configuration/resolvers/helper.ts +++ b/src/client/debugger/extension/configuration/resolvers/helper.ts @@ -48,7 +48,10 @@ export class DebugEnvironmentVariablesHelper implements IDebugEnvironmentVariabl } // Append the PYTHONPATH and PATH variables. - this.envParser.appendPath(env, debugLaunchEnvVars[pathVariableName]); + this.envParser.appendPath( + env, + debugLaunchEnvVars[pathVariableName] ?? debugLaunchEnvVars[pathVariableName.toUpperCase()], + ); this.envParser.appendPythonPath(env, debugLaunchEnvVars.PYTHONPATH); if (typeof env[pathVariableName] === 'string' && env[pathVariableName]!.length > 0) { diff --git a/src/test/common/variables/envVarsService.unit.test.ts b/src/test/common/variables/envVarsService.unit.test.ts index 590803ea135e..8b1c402e634d 100644 --- a/src/test/common/variables/envVarsService.unit.test.ts +++ b/src/test/common/variables/envVarsService.unit.test.ts @@ -10,17 +10,16 @@ import * as TypeMoq from 'typemoq'; import { IFileSystem } from '../../../client/common/platform/types'; import { IPathUtils } from '../../../client/common/types'; import { EnvironmentVariablesService, parseEnvFile } from '../../../client/common/variables/environment'; +import { getSearchPathEnvVarNames } from '../../../client/common/utils/exec'; use(chaiAsPromised); type PathVar = 'Path' | 'PATH'; -const PATHS = [ - 'Path', // Windows - 'PATH', // non-Windows -]; +const PATHS = getSearchPathEnvVarNames(); -suite('Environment Variables Service', () => { +suite('xEnvironment Variables Service', () => { const filename = 'x/y/z/.env'; + const processEnvPath = getSearchPathEnvVarNames()[0]; let pathUtils: TypeMoq.IMock; let fs: TypeMoq.IMock; let variablesService: EnvironmentVariablesService; @@ -208,7 +207,7 @@ PYTHON=${BINDIR}/python3\n\ expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); expect(vars2).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); - expect(vars2).to.have.property(pathVariable, 'PATH', 'Incorrect value'); + expect(vars2).to.have.property(processEnvPath, 'PATH', 'Incorrect value'); verifyAll(); }); @@ -226,7 +225,7 @@ PYTHON=${BINDIR}/python3\n\ expect(target).to.have.property('TWO', 'TWO', 'Incorrect value'); expect(target).to.have.property('THREE', '3', 'Variable not merged'); expect(target).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); - expect(target).to.have.property(pathVariable, 'PATH', 'Incorrect value'); + expect(target).to.have.property(processEnvPath, 'PATH', 'Incorrect value'); verifyAll(); }); }); @@ -266,17 +265,17 @@ PYTHON=${BINDIR}/python3\n\ variablesService.appendPath(vars); expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); + expect(vars).to.have.property(processEnvPath, 'PATH', 'Incorrect value'); variablesService.appendPath(vars, ''); expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); + expect(vars).to.have.property(processEnvPath, 'PATH', 'Incorrect value'); variablesService.appendPath(vars, ' ', ''); expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); + expect(vars).to.have.property(processEnvPath, 'PATH', 'Incorrect value'); verifyAll(); }); @@ -291,7 +290,11 @@ PYTHON=${BINDIR}/python3\n\ expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, `PATH${path.delimiter}${pathToAppend}`, 'Incorrect value'); + expect(vars).to.have.property( + processEnvPath, + `PATH${path.delimiter}${pathToAppend}`, + 'Incorrect value', + ); verifyAll(); }); }); diff --git a/src/test/debugger/envVars.test.ts b/src/test/debugger/envVars.test.ts index 6aa0dea4d8c6..c043146fe53d 100644 --- a/src/test/debugger/envVars.test.ts +++ b/src/test/debugger/envVars.test.ts @@ -15,6 +15,7 @@ import { ConsoleType, LaunchRequestArguments } from '../../client/debugger/types import { isOs, OSType } from '../common'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; import { UnitTestIocContainer } from '../testing/serviceRegistry'; +import { normCase } from '../../client/common/platform/fs-paths'; use(chaiAsPromised); @@ -109,9 +110,9 @@ suite('Resolving Environment Variables when Debugging', () => { }); async function testJsonEnvVariables(console: ConsoleType, expectedNumberOfVariables: number) { - const prop1 = shortid.generate(); - const prop2 = shortid.generate(); - const prop3 = shortid.generate(); + const prop1 = normCase(shortid.generate()); + const prop2 = normCase(shortid.generate()); + const prop3 = normCase(shortid.generate()); const env: Record = {}; env[prop1] = prop1; env[prop2] = prop2; From f058f5efeab5b18e01170f2d76286cfa255b304f Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 11 Apr 2023 10:24:28 -0700 Subject: [PATCH 041/336] Undo unwanted change in env var tests (#21035) https://github.com/microsoft/vscode-python/pull/21033#discussion_r1163009663 --- src/test/common/variables/envVarsService.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/common/variables/envVarsService.unit.test.ts b/src/test/common/variables/envVarsService.unit.test.ts index 8b1c402e634d..0c978b2f9e86 100644 --- a/src/test/common/variables/envVarsService.unit.test.ts +++ b/src/test/common/variables/envVarsService.unit.test.ts @@ -17,7 +17,7 @@ use(chaiAsPromised); type PathVar = 'Path' | 'PATH'; const PATHS = getSearchPathEnvVarNames(); -suite('xEnvironment Variables Service', () => { +suite('Environment Variables Service', () => { const filename = 'x/y/z/.env'; const processEnvPath = getSearchPathEnvVarNames()[0]; let pathUtils: TypeMoq.IMock; From 7d2cd36d2c4089cc23cd48b7ff7e04e054d39cea Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 11 Apr 2023 12:34:06 -0700 Subject: [PATCH 042/336] Update packages for Component Governance (#21037) --- package-lock.json | 1401 ++++++++++++++++++++++----------------------- package.json | 4 +- 2 files changed, 685 insertions(+), 720 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b537745c9bd..eb01e68b0929 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", - "@vscode/extension-telemetry": "^0.7.4-preview", + "@vscode/extension-telemetry": "^0.7.7", "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", "diff-match-patch": "^1.0.0", @@ -74,6 +74,7 @@ "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", "@vscode/test-electron": "^2.1.3", + "@vscode/vsce": "^2.18.0", "bent": "^7.3.12", "chai": "^4.1.2", "chai-arrays": "^2.0.0", @@ -116,7 +117,6 @@ "typemoq": "^2.1.0", "typescript": "4.5.5", "uuid": "^8.3.2", - "vsce": "^2.6.6", "vscode-debugadapter-testsupport": "^1.27.0", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", @@ -143,9 +143,9 @@ } }, "node_modules/@azure/abort-controller/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, "node_modules/@azure/core-auth": { "version": "1.4.0", @@ -160,85 +160,76 @@ } }, "node_modules/@azure/core-auth/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, - "node_modules/@azure/core-http": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.3.1.tgz", - "integrity": "sha512-cur03BUwV0Tbv81bQBOLafFB02B6G++K6F2O3IMl8pSE2QlXm3cu11bfyBNlDUKi5U+xnB3GC63ae3athhkx6Q==", + "node_modules/@azure/core-rest-pipeline": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.3.tgz", + "integrity": "sha512-AMQb0ttiGJ0MIV/r+4TVra6U4+90mPeOveehFnrqKlo7dknPJYdJ61wOzYJXJjDxF8LcCtSogfRelkq+fCGFTw==", "dependencies": { "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.13", - "@azure/core-util": "^1.1.1", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.3.0", "@azure/logger": "^1.0.0", - "@types/node-fetch": "^2.5.0", - "@types/tunnel": "^0.0.3", "form-data": "^4.0.0", - "node-fetch": "^2.6.7", - "process": "^0.11.10", - "tough-cookie": "^4.0.0", - "tslib": "^2.2.0", - "tunnel": "^0.0.6", - "uuid": "^8.3.0", - "xml2js": "^0.4.19" + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "tslib": "^2.2.0" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@azure/core-http/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, + "node_modules/@azure/core-rest-pipeline/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "engines": { - "node": ">= 6" + "node": ">= 10" } }, - "node_modules/@azure/core-http/node_modules/tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "node_modules/@azure/core-rest-pipeline/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "ms": "2.1.2" }, "engines": { - "node": ">=6" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@azure/core-http/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "node_modules/@azure/core-http/node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" }, "engines": { - "node": ">=4.0.0" + "node": ">= 6" } }, + "node_modules/@azure/core-rest-pipeline/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/@azure/core-tracing": { - "version": "1.0.0-preview.13", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", - "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", + "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", "dependencies": { - "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" }, "engines": { @@ -246,42 +237,42 @@ } }, "node_modules/@azure/core-tracing/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, "node_modules/@azure/core-util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.1.1.tgz", - "integrity": "sha512-A4TBYVQCtHOigFb2ETiiKFDocBoI1Zk2Ui1KpI42aJSIDexF7DHQFpnjonltXAIU/ceH+1fsZAWWgvX6/AKzog==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.0.tgz", + "integrity": "sha512-ANP0Er7R2KHHHjwmKzPF9wbd0gXvOX7yRRHeYL1eNd/OaNrMLyfZH/FQasHRVAf6rMXX+EAUpvYwLMFDHDI5Gw==", "dependencies": { "@azure/abort-controller": "^1.0.0", "tslib": "^2.2.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/@azure/core-util/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, "node_modules/@azure/logger": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz", - "integrity": "sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", + "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", "dependencies": { "tslib": "^2.2.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/@azure/logger/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, "node_modules/@babel/code-frame": { "version": "7.12.11", @@ -604,32 +595,44 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.8.tgz", - "integrity": "sha512-9o9SUAamJiTXIYwpkQDuueYt83uZfXp8zp8YFix1IwVPwC9RmE36T2CX9gXOeq1nDckOuOduYpA8qHvdh5BGfQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.9.tgz", + "integrity": "sha512-3pCfM2TzHn3gU9pxHztduKcVRdb/nzruvPFfHPZD0IM0mb0h6TGo2isELF3CTMahTx50RAC51ojNIw2/7VRkOg==", "dependencies": { - "@microsoft/applicationinsights-core-js": "2.8.9", + "@microsoft/applicationinsights-core-js": "2.8.10", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.8.tgz", - "integrity": "sha512-SjlRoNcXcXBH6WQD/5SkkaCHIVqldH3gDu+bI7YagrOVJ5APxwT1Duw9gm3L1FjFa9S2i81fvJ3EVSKpp9wULA==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.9.tgz", + "integrity": "sha512-D/RtqkQ2Nr4cuoGqmhi5QTmi3cBlxehIThJ1u3BaH9H/YkLNTKEcHZRWTXy14bXheCefNHciLuadg37G2Kekcg==", "dependencies": { - "@microsoft/1ds-core-js": "3.2.8", + "@microsoft/1ds-core-js": "3.2.9", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.9.tgz", - "integrity": "sha512-fMBsAEB7pWtPn43y72q9Xy5E5y55r6gMuDQqRRccccVoQDPXyS57VCj5IdATblctru0C6A8XpL2vRyNmEsu0Vg==", + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.11.tgz", + "integrity": "sha512-DGDNzT4DMlSvUzWjA4y3tDg47+QYOPV+W07vlfdPwGgLwrl4n6Q4crrW8Y/IOpthHAKDU8rolSAUvP3NqxPi4Q==", + "dependencies": { + "@microsoft/applicationinsights-common": "2.8.11", + "@microsoft/applicationinsights-core-js": "2.8.11", + "@microsoft/applicationinsights-shims": "2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/applicationinsights-core-js": { + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", + "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", "dependencies": { - "@microsoft/applicationinsights-common": "2.8.9", - "@microsoft/applicationinsights-core-js": "2.8.9", "@microsoft/applicationinsights-shims": "2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" }, @@ -638,11 +641,23 @@ } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.9.tgz", - "integrity": "sha512-mObn1moElyxZaGIRF/IU3cOaeKMgxghXnYEoHNUCA2e+rNwBIgxjyKkblFIpmGuHf4X7Oz3o3yBWpaC6AoMpig==", + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.11.tgz", + "integrity": "sha512-Cxu4gRajkYv9buEtrcLGHK97AqGK62feN9jH9/JSjUSiSFhbnWtYvEg1EMqMI/P4pneu53yLJloITB+TKwmK7A==", + "dependencies": { + "@microsoft/applicationinsights-core-js": "2.8.11", + "@microsoft/applicationinsights-shims": "2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/applicationinsights-core-js": { + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", + "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", "dependencies": { - "@microsoft/applicationinsights-core-js": "2.8.9", "@microsoft/applicationinsights-shims": "2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" }, @@ -651,9 +666,9 @@ } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.9.tgz", - "integrity": "sha512-HRuIuZ6aOWezcg/G5VyFDDWGL8hDNe/ljPP01J7ImH2kRPEgbtcfPSUMjkamGMefgdq81GZsSoC/NNGTP4pp2w==", + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.10.tgz", + "integrity": "sha512-jQrufDW0+sV8fBhRvzIPNGiCC6dELH+Ug0DM5CfN9757TBqZJz8CSWyDjex39as8+jD0F/8HRU9QdmrVgq5vFg==", "dependencies": { "@microsoft/applicationinsights-shims": "2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" @@ -668,13 +683,25 @@ "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.9.tgz", - "integrity": "sha512-CH0J8JFOy7MjK8JO4pXXU+EML+Ilix+94PMZTX5EJlBU1in+mrik74/8qSg3UC4ekPi12KwrXaHCQSVC3WseXQ==", + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.11.tgz", + "integrity": "sha512-11T7bbP4ifIBg95E9mYZv1g/vcWvw/KaWKRcGMREP3+vBTLBwMB8r2e9Zd583bOVx+9/gRvfIg+Z/lInQqAfbA==", + "dependencies": { + "@microsoft/applicationinsights-channel-js": "2.8.11", + "@microsoft/applicationinsights-common": "2.8.11", + "@microsoft/applicationinsights-core-js": "2.8.11", + "@microsoft/applicationinsights-shims": "2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/applicationinsights-core-js": { + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", + "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", "dependencies": { - "@microsoft/applicationinsights-channel-js": "2.8.9", - "@microsoft/applicationinsights-common": "2.8.9", - "@microsoft/applicationinsights-core-js": "2.8.9", "@microsoft/applicationinsights-shims": "2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" }, @@ -688,9 +715,9 @@ "integrity": "sha512-2IHAOaLauc8qaAitvWS+U931T+ze+7MNWrDHY47IENP5y2UA0vqJDu67kWZDdpCN1fFC77sfgfB+HV7SrKshnQ==" }, "node_modules/@microsoft/dynamicproto-js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.7.tgz", - "integrity": "sha512-SK3D3aVt+5vOOccKPnGaJWB5gQ8FuKfjboUJHedMP7gu54HqSCXX5iFXhktGD8nfJb0Go30eDvs/UDoTnR2kOA==" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", + "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -728,62 +755,62 @@ } }, "node_modules/@opentelemetry/api": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.3.0.tgz", - "integrity": "sha512-YveTnGNsFFixTKJz09Oi4zYkiLT5af3WpZDu4aIUM7xX+2bHAkOJayFTVQd6zB8kkWPpbua4Ha6Ql00grdLlJQ==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", + "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==", "engines": { "node": ">=8.0.0" } }, "node_modules/@opentelemetry/core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.8.0.tgz", - "integrity": "sha512-6SDjwBML4Am0AQmy7z1j6HGrWDgeK8awBRUvl1PGw6HayViMk4QpnUXvv4HTHisecgVBy43NE/cstWprm8tIfw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.11.0.tgz", + "integrity": "sha512-aP1wHSb+YfU0pM63UAkizYPuS4lZxzavHHw5KJfFNN2oWQ79HSm6JR3CzwFKHwKhSzHN8RE9fgP1IdVJ8zmo1w==", "dependencies": { - "@opentelemetry/semantic-conventions": "1.8.0" + "@opentelemetry/semantic-conventions": "1.11.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.4.0" + "@opentelemetry/api": ">=1.0.0 <1.5.0" } }, "node_modules/@opentelemetry/resources": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.8.0.tgz", - "integrity": "sha512-KSyMH6Jvss/PFDy16z5qkCK0ERlpyqixb1xwb73wLMvVq+j7i89lobDjw3JkpCcd1Ws0J6jAI4fw28Zufj2ssg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.11.0.tgz", + "integrity": "sha512-y0z2YJTqk0ag+hGT4EXbxH/qPhDe8PfwltYb4tXIEsozgEFfut/bqW7H7pDvylmCjBRMG4NjtLp57V1Ev++brA==", "dependencies": { - "@opentelemetry/core": "1.8.0", - "@opentelemetry/semantic-conventions": "1.8.0" + "@opentelemetry/core": "1.11.0", + "@opentelemetry/semantic-conventions": "1.11.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.4.0" + "@opentelemetry/api": ">=1.0.0 <1.5.0" } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.8.0.tgz", - "integrity": "sha512-iH41m0UTddnCKJzZx3M85vlhKzRcmT48pUeBbnzsGrq4nIay1oWVHKM5nhB5r8qRDGvd/n7f/YLCXClxwM0tvA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.11.0.tgz", + "integrity": "sha512-DV8e5/Qo42V8FMBlQ0Y0Liv6Hl/Pp5bAZ73s7r1euX8w4bpRes1B7ACiA4yujADbWMJxBgSo4fGbi4yjmTMG2A==", "dependencies": { - "@opentelemetry/core": "1.8.0", - "@opentelemetry/resources": "1.8.0", - "@opentelemetry/semantic-conventions": "1.8.0" + "@opentelemetry/core": "1.11.0", + "@opentelemetry/resources": "1.11.0", + "@opentelemetry/semantic-conventions": "1.11.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.4.0" + "@opentelemetry/api": ">=1.0.0 <1.5.0" } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.8.0.tgz", - "integrity": "sha512-TYh1MRcm4JnvpqtqOwT9WYaBYY4KERHdToxs/suDTLviGRsQkIjS5yYROTYTSJQUnYLOn/TuOh5GoMwfLSU+Ew==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.11.0.tgz", + "integrity": "sha512-fG4D0AktoHyHwGhFGv+PzKrZjxbKJfckJauTJdq2A+ej5cTazmNYjJVAODXXkYyrsI10muMl+B1iO2q1R6Lp+w==", "engines": { "node": ">=14" } @@ -1044,29 +1071,8 @@ "node_modules/@types/node": { "version": "14.18.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz", - "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==" - }, - "node_modules/@types/node-fetch": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", - "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", - "dependencies": { - "@types/node": "*", - "form-data": "^3.0.0" - } - }, - "node_modules/@types/node-fetch/node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } + "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==", + "dev": true }, "node_modules/@types/semver": { "version": "5.5.0", @@ -1107,14 +1113,6 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, - "node_modules/@types/tunnel": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", - "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -1357,14 +1355,14 @@ "dev": true }, "node_modules/@vscode/extension-telemetry": { - "version": "0.7.4-preview", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.7.4-preview.tgz", - "integrity": "sha512-6OkvjCc+DaC9B26t3hj7vuAxf1ONm/p4LrVvFrapa+jBCKxXXUaV1Asz6+QxYaPfd4Ws/MlnFfCvlgvv3uYRwQ==", + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.7.7.tgz", + "integrity": "sha512-uW508BPjkWDBOKvvvSym3ZmGb7kHIiWaAfB/1PHzLz2x9TrC33CfjmFEI+CywIL/jBv4bqZxxjN4tfefB61F+g==", "dependencies": { - "@microsoft/1ds-core-js": "^3.2.8", - "@microsoft/1ds-post-js": "^3.2.8", - "@microsoft/applicationinsights-web-basic": "^2.8.9", - "applicationinsights": "2.3.6" + "@microsoft/1ds-core-js": "^3.2.9", + "@microsoft/1ds-post-js": "^3.2.9", + "@microsoft/applicationinsights-web-basic": "^2.8.11", + "applicationinsights": "2.5.0" }, "engines": { "vscode": "^1.75.0" @@ -1411,6 +1409,101 @@ "node": ">=8.9.3" } }, + "node_modules/@vscode/vsce": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.18.0.tgz", + "integrity": "sha512-tUA3XoKx5xjoi3EDcngk0VUYMhvfXLhS4s7CntpLPh1qtLYtgSCexTIMUHkCy6MqyozRW98bdW3a2yHPEADRnQ==", + "dev": true, + "dependencies": { + "azure-devops-node-api": "^11.0.1", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "commander": "^6.1.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^5.1.0", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.4.23", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 14" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@vscode/vsce/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vscode/vsce/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@vscode/vsce/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/@vscode/vsce/node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -1644,6 +1737,33 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/aggregate-error": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", @@ -1792,11 +1912,12 @@ } }, "node_modules/applicationinsights": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.3.6.tgz", - "integrity": "sha512-ZzXXpZpDRGcy6Pp5V319nDF9/+Ey7jNknEXZyaBajtC5onN0dcBem6ng5jcb3MPH2AjYWRI8XgyNEuzP/6Y5/A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.5.0.tgz", + "integrity": "sha512-6kIFmpANRok+6FhCOmO7ZZ/mh7fdNKn17BaT13cg/RV5roLPJlA6q8srWexayHd3MPcwMb9072e8Zp0P47s/pw==", "dependencies": { - "@azure/core-http": "^2.2.3", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.10.0", "@microsoft/applicationinsights-web-snippet": "^1.0.1", "@opentelemetry/api": "^1.0.4", "@opentelemetry/core": "^1.0.1", @@ -1823,7 +1944,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "dev": true, + "optional": true }, "node_modules/arch": { "version": "2.2.0", @@ -1876,6 +1998,7 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", "dev": true, + "optional": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -2236,7 +2359,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/atob": { "version": "2.1.2", @@ -3294,7 +3417,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", - "dev": true + "dev": true, + "optional": true }, "node_modules/chrome-trace-event": { "version": "1.0.2", @@ -3613,7 +3737,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "dev": true, + "optional": true }, "node_modules/constants-browserify": { "version": "1.0.0", @@ -4090,6 +4215,7 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, + "optional": true, "engines": { "node": ">=4.0.0" } @@ -4239,7 +4365,7 @@ "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "engines": { "node": ">=0.4.0" } @@ -4248,7 +4374,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true + "dev": true, + "optional": true }, "node_modules/des.js": { "version": "1.0.0", @@ -4274,6 +4401,7 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, + "optional": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -5681,6 +5809,7 @@ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, + "optional": true, "engines": { "node": ">=6" } @@ -6291,6 +6420,19 @@ "node": ">=8" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -6442,6 +6584,7 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, + "optional": true, "dependencies": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -6458,6 +6601,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, + "optional": true, "engines": { "node": ">=0.10.0" } @@ -6467,6 +6611,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, + "optional": true, "dependencies": { "ansi-regex": "^2.0.0" }, @@ -6562,7 +6707,8 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", - "dev": true + "dev": true, + "optional": true }, "node_modules/glob": { "version": "7.2.0", @@ -7143,7 +7289,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true + "dev": true, + "optional": true }, "node_modules/has-value": { "version": "1.0.0", @@ -7292,18 +7439,6 @@ "node": ">= 6" } }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/http-proxy-agent/node_modules/debug": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", @@ -7326,37 +7461,23 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true - }, - "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "dependencies": { + "agent-base": "6", "debug": "4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 6" } }, "node_modules/https-proxy-agent/node_modules/debug": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -8585,9 +8706,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "node_modules/jsx-ast-utils": { "version": "3.2.1", @@ -8620,6 +8741,7 @@ "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", "dev": true, "hasInstallScript": true, + "optional": true, "dependencies": { "node-addon-api": "^3.0.0", "prebuild-install": "^6.0.0" @@ -9338,7 +9460,8 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true + "dev": true, + "optional": true }, "node_modules/mocha": { "version": "9.2.2", @@ -9759,8 +9882,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mute-stdout": { "version": "1.0.1", @@ -9821,7 +9943,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true + "dev": true, + "optional": true }, "node_modules/natural-compare": { "version": "1.4.0", @@ -9889,6 +10012,7 @@ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", "dev": true, + "optional": true, "dependencies": { "semver": "^5.4.1" } @@ -9897,26 +10021,8 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true - }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } + "dev": true, + "optional": true }, "node_modules/node-has-native-dependencies": { "version": "1.0.2", @@ -10262,6 +10368,7 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, + "optional": true, "dependencies": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -11141,6 +11248,7 @@ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", "dev": true, + "optional": true, "dependencies": { "detect-libc": "^1.0.3", "expand-template": "^2.0.3", @@ -11168,6 +11276,7 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -11207,6 +11316,7 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true, "engines": { "node": ">= 0.6.0" } @@ -11249,11 +11359,6 @@ "node >= 0.8.1" ] }, - "node_modules/psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -11293,6 +11398,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, "engines": { "node": ">=6" } @@ -11339,11 +11445,6 @@ "node": ">=0.4.x" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11388,6 +11489,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "optional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -11403,6 +11505,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, + "optional": true, "engines": { "node": ">=0.10.0" } @@ -11710,11 +11813,6 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "node_modules/resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -12125,13 +12223,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true }, "node_modules/simple-get": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", "dev": true, + "optional": true, "dependencies": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -12143,6 +12243,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", "dev": true, + "optional": true, "dependencies": { "mimic-response": "^2.0.0" }, @@ -12155,6 +12256,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", "dev": true, + "optional": true, "engines": { "node": ">=8" }, @@ -12943,6 +13045,7 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", "dev": true, + "optional": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -12955,6 +13058,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, + "optional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -12966,6 +13070,7 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -12976,6 +13081,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, + "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -12990,6 +13096,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, + "optional": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -13285,11 +13392,6 @@ "node": ">=6" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", @@ -13669,6 +13771,7 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, "engines": { "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } @@ -13678,6 +13781,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "dev": true, + "optional": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -13892,14 +13996,6 @@ "through2-filter": "^3.0.0" } }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -14022,15 +14118,6 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", @@ -14092,6 +14179,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, "bin": { "uuid": "dist/bin/uuid" } @@ -14208,98 +14296,6 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, - "node_modules/vsce": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.7.0.tgz", - "integrity": "sha512-CKU34wrQlbKDeJCRBkd1a8iwF9EvNxcYMg9hAUH6AxFGR6Wo2IKWwt3cJIcusHxx6XdjDHWlfAS/fJN30uvVnA==", - "dev": true, - "dependencies": { - "azure-devops-node-api": "^11.0.1", - "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", - "glob": "^7.0.6", - "hosted-git-info": "^4.0.2", - "keytar": "^7.7.0", - "leven": "^3.1.0", - "markdown-it": "^12.3.2", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^5.1.0", - "tmp": "^0.2.1", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.4.23", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" - }, - "bin": { - "vsce": "vsce" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/vsce/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/vsce/node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/vsce/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/vsce/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/vsce/node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/vscode-debugadapter": { "version": "1.35.0", "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.35.0.tgz", @@ -14426,11 +14422,6 @@ "node": ">=10.13.0" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, "node_modules/webpack": { "version": "5.76.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", @@ -14699,15 +14690,6 @@ "node": ">=10.13.0" } }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -14796,6 +14778,7 @@ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, + "optional": true, "dependencies": { "string-width": "^1.0.2 || 2" } @@ -15205,9 +15188,9 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" } } }, @@ -15221,115 +15204,101 @@ }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" } } }, - "@azure/core-http": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.3.1.tgz", - "integrity": "sha512-cur03BUwV0Tbv81bQBOLafFB02B6G++K6F2O3IMl8pSE2QlXm3cu11bfyBNlDUKi5U+xnB3GC63ae3athhkx6Q==", + "@azure/core-rest-pipeline": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.3.tgz", + "integrity": "sha512-AMQb0ttiGJ0MIV/r+4TVra6U4+90mPeOveehFnrqKlo7dknPJYdJ61wOzYJXJjDxF8LcCtSogfRelkq+fCGFTw==", "requires": { "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.13", - "@azure/core-util": "^1.1.1", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.3.0", "@azure/logger": "^1.0.0", - "@types/node-fetch": "^2.5.0", - "@types/tunnel": "^0.0.3", "form-data": "^4.0.0", - "node-fetch": "^2.6.7", - "process": "^0.11.10", - "tough-cookie": "^4.0.0", - "tslib": "^2.2.0", - "tunnel": "^0.0.6", - "uuid": "^8.3.0", - "xml2js": "^0.4.19" + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "tslib": "^2.2.0" }, "dependencies": { - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "ms": "2.1.2" } }, - "tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" } }, "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" } } }, "@azure/core-tracing": { - "version": "1.0.0-preview.13", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", - "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", + "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", "requires": { - "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" } } }, "@azure/core-util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.1.1.tgz", - "integrity": "sha512-A4TBYVQCtHOigFb2ETiiKFDocBoI1Zk2Ui1KpI42aJSIDexF7DHQFpnjonltXAIU/ceH+1fsZAWWgvX6/AKzog==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.0.tgz", + "integrity": "sha512-ANP0Er7R2KHHHjwmKzPF9wbd0gXvOX7yRRHeYL1eNd/OaNrMLyfZH/FQasHRVAf6rMXX+EAUpvYwLMFDHDI5Gw==", "requires": { "@azure/abort-controller": "^1.0.0", "tslib": "^2.2.0" }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" } } }, "@azure/logger": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz", - "integrity": "sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", + "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", "requires": { "tslib": "^2.2.0" }, "dependencies": { "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" } } }, @@ -15576,50 +15545,72 @@ } }, "@microsoft/1ds-core-js": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.8.tgz", - "integrity": "sha512-9o9SUAamJiTXIYwpkQDuueYt83uZfXp8zp8YFix1IwVPwC9RmE36T2CX9gXOeq1nDckOuOduYpA8qHvdh5BGfQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.9.tgz", + "integrity": "sha512-3pCfM2TzHn3gU9pxHztduKcVRdb/nzruvPFfHPZD0IM0mb0h6TGo2isELF3CTMahTx50RAC51ojNIw2/7VRkOg==", "requires": { - "@microsoft/applicationinsights-core-js": "2.8.9", + "@microsoft/applicationinsights-core-js": "2.8.10", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "@microsoft/1ds-post-js": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.8.tgz", - "integrity": "sha512-SjlRoNcXcXBH6WQD/5SkkaCHIVqldH3gDu+bI7YagrOVJ5APxwT1Duw9gm3L1FjFa9S2i81fvJ3EVSKpp9wULA==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.9.tgz", + "integrity": "sha512-D/RtqkQ2Nr4cuoGqmhi5QTmi3cBlxehIThJ1u3BaH9H/YkLNTKEcHZRWTXy14bXheCefNHciLuadg37G2Kekcg==", "requires": { - "@microsoft/1ds-core-js": "3.2.8", + "@microsoft/1ds-core-js": "3.2.9", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "@microsoft/applicationinsights-channel-js": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.9.tgz", - "integrity": "sha512-fMBsAEB7pWtPn43y72q9Xy5E5y55r6gMuDQqRRccccVoQDPXyS57VCj5IdATblctru0C6A8XpL2vRyNmEsu0Vg==", + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.11.tgz", + "integrity": "sha512-DGDNzT4DMlSvUzWjA4y3tDg47+QYOPV+W07vlfdPwGgLwrl4n6Q4crrW8Y/IOpthHAKDU8rolSAUvP3NqxPi4Q==", "requires": { - "@microsoft/applicationinsights-common": "2.8.9", - "@microsoft/applicationinsights-core-js": "2.8.9", + "@microsoft/applicationinsights-common": "2.8.11", + "@microsoft/applicationinsights-core-js": "2.8.11", "@microsoft/applicationinsights-shims": "2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" + }, + "dependencies": { + "@microsoft/applicationinsights-core-js": { + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", + "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "requires": { + "@microsoft/applicationinsights-shims": "2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" + } + } } }, "@microsoft/applicationinsights-common": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.9.tgz", - "integrity": "sha512-mObn1moElyxZaGIRF/IU3cOaeKMgxghXnYEoHNUCA2e+rNwBIgxjyKkblFIpmGuHf4X7Oz3o3yBWpaC6AoMpig==", + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.11.tgz", + "integrity": "sha512-Cxu4gRajkYv9buEtrcLGHK97AqGK62feN9jH9/JSjUSiSFhbnWtYvEg1EMqMI/P4pneu53yLJloITB+TKwmK7A==", "requires": { - "@microsoft/applicationinsights-core-js": "2.8.9", + "@microsoft/applicationinsights-core-js": "2.8.11", "@microsoft/applicationinsights-shims": "2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" + }, + "dependencies": { + "@microsoft/applicationinsights-core-js": { + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", + "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "requires": { + "@microsoft/applicationinsights-shims": "2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" + } + } } }, "@microsoft/applicationinsights-core-js": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.9.tgz", - "integrity": "sha512-HRuIuZ6aOWezcg/G5VyFDDWGL8hDNe/ljPP01J7ImH2kRPEgbtcfPSUMjkamGMefgdq81GZsSoC/NNGTP4pp2w==", + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.10.tgz", + "integrity": "sha512-jQrufDW0+sV8fBhRvzIPNGiCC6dELH+Ug0DM5CfN9757TBqZJz8CSWyDjex39as8+jD0F/8HRU9QdmrVgq5vFg==", "requires": { "@microsoft/applicationinsights-shims": "2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" @@ -15631,15 +15622,26 @@ "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" }, "@microsoft/applicationinsights-web-basic": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.9.tgz", - "integrity": "sha512-CH0J8JFOy7MjK8JO4pXXU+EML+Ilix+94PMZTX5EJlBU1in+mrik74/8qSg3UC4ekPi12KwrXaHCQSVC3WseXQ==", + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.11.tgz", + "integrity": "sha512-11T7bbP4ifIBg95E9mYZv1g/vcWvw/KaWKRcGMREP3+vBTLBwMB8r2e9Zd583bOVx+9/gRvfIg+Z/lInQqAfbA==", "requires": { - "@microsoft/applicationinsights-channel-js": "2.8.9", - "@microsoft/applicationinsights-common": "2.8.9", - "@microsoft/applicationinsights-core-js": "2.8.9", + "@microsoft/applicationinsights-channel-js": "2.8.11", + "@microsoft/applicationinsights-common": "2.8.11", + "@microsoft/applicationinsights-core-js": "2.8.11", "@microsoft/applicationinsights-shims": "2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" + }, + "dependencies": { + "@microsoft/applicationinsights-core-js": { + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", + "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "requires": { + "@microsoft/applicationinsights-shims": "2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" + } + } } }, "@microsoft/applicationinsights-web-snippet": { @@ -15648,9 +15650,9 @@ "integrity": "sha512-2IHAOaLauc8qaAitvWS+U931T+ze+7MNWrDHY47IENP5y2UA0vqJDu67kWZDdpCN1fFC77sfgfB+HV7SrKshnQ==" }, "@microsoft/dynamicproto-js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.7.tgz", - "integrity": "sha512-SK3D3aVt+5vOOccKPnGaJWB5gQ8FuKfjboUJHedMP7gu54HqSCXX5iFXhktGD8nfJb0Go30eDvs/UDoTnR2kOA==" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", + "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -15679,41 +15681,41 @@ } }, "@opentelemetry/api": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.3.0.tgz", - "integrity": "sha512-YveTnGNsFFixTKJz09Oi4zYkiLT5af3WpZDu4aIUM7xX+2bHAkOJayFTVQd6zB8kkWPpbua4Ha6Ql00grdLlJQ==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", + "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==" }, "@opentelemetry/core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.8.0.tgz", - "integrity": "sha512-6SDjwBML4Am0AQmy7z1j6HGrWDgeK8awBRUvl1PGw6HayViMk4QpnUXvv4HTHisecgVBy43NE/cstWprm8tIfw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.11.0.tgz", + "integrity": "sha512-aP1wHSb+YfU0pM63UAkizYPuS4lZxzavHHw5KJfFNN2oWQ79HSm6JR3CzwFKHwKhSzHN8RE9fgP1IdVJ8zmo1w==", "requires": { - "@opentelemetry/semantic-conventions": "1.8.0" + "@opentelemetry/semantic-conventions": "1.11.0" } }, "@opentelemetry/resources": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.8.0.tgz", - "integrity": "sha512-KSyMH6Jvss/PFDy16z5qkCK0ERlpyqixb1xwb73wLMvVq+j7i89lobDjw3JkpCcd1Ws0J6jAI4fw28Zufj2ssg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.11.0.tgz", + "integrity": "sha512-y0z2YJTqk0ag+hGT4EXbxH/qPhDe8PfwltYb4tXIEsozgEFfut/bqW7H7pDvylmCjBRMG4NjtLp57V1Ev++brA==", "requires": { - "@opentelemetry/core": "1.8.0", - "@opentelemetry/semantic-conventions": "1.8.0" + "@opentelemetry/core": "1.11.0", + "@opentelemetry/semantic-conventions": "1.11.0" } }, "@opentelemetry/sdk-trace-base": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.8.0.tgz", - "integrity": "sha512-iH41m0UTddnCKJzZx3M85vlhKzRcmT48pUeBbnzsGrq4nIay1oWVHKM5nhB5r8qRDGvd/n7f/YLCXClxwM0tvA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.11.0.tgz", + "integrity": "sha512-DV8e5/Qo42V8FMBlQ0Y0Liv6Hl/Pp5bAZ73s7r1euX8w4bpRes1B7ACiA4yujADbWMJxBgSo4fGbi4yjmTMG2A==", "requires": { - "@opentelemetry/core": "1.8.0", - "@opentelemetry/resources": "1.8.0", - "@opentelemetry/semantic-conventions": "1.8.0" + "@opentelemetry/core": "1.11.0", + "@opentelemetry/resources": "1.11.0", + "@opentelemetry/semantic-conventions": "1.11.0" } }, "@opentelemetry/semantic-conventions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.8.0.tgz", - "integrity": "sha512-TYh1MRcm4JnvpqtqOwT9WYaBYY4KERHdToxs/suDTLviGRsQkIjS5yYROTYTSJQUnYLOn/TuOh5GoMwfLSU+Ew==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.11.0.tgz", + "integrity": "sha512-fG4D0AktoHyHwGhFGv+PzKrZjxbKJfckJauTJdq2A+ej5cTazmNYjJVAODXXkYyrsI10muMl+B1iO2q1R6Lp+w==" }, "@polka/url": { "version": "1.0.0-next.21", @@ -15965,28 +15967,8 @@ "@types/node": { "version": "14.18.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz", - "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==" - }, - "@types/node-fetch": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", - "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - }, - "dependencies": { - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } + "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==", + "dev": true }, "@types/semver": { "version": "5.5.0", @@ -16027,14 +16009,6 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, - "@types/tunnel": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", - "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", - "requires": { - "@types/node": "*" - } - }, "@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -16186,14 +16160,14 @@ "dev": true }, "@vscode/extension-telemetry": { - "version": "0.7.4-preview", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.7.4-preview.tgz", - "integrity": "sha512-6OkvjCc+DaC9B26t3hj7vuAxf1ONm/p4LrVvFrapa+jBCKxXXUaV1Asz6+QxYaPfd4Ws/MlnFfCvlgvv3uYRwQ==", + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.7.7.tgz", + "integrity": "sha512-uW508BPjkWDBOKvvvSym3ZmGb7kHIiWaAfB/1PHzLz2x9TrC33CfjmFEI+CywIL/jBv4bqZxxjN4tfefB61F+g==", "requires": { - "@microsoft/1ds-core-js": "^3.2.8", - "@microsoft/1ds-post-js": "^3.2.8", - "@microsoft/applicationinsights-web-basic": "^2.8.9", - "applicationinsights": "2.3.6" + "@microsoft/1ds-core-js": "^3.2.9", + "@microsoft/1ds-post-js": "^3.2.9", + "@microsoft/applicationinsights-web-basic": "^2.8.11", + "applicationinsights": "2.5.0" } }, "@vscode/jupyter-lsp-middleware": { @@ -16231,6 +16205,80 @@ "unzipper": "^0.10.11" } }, + "@vscode/vsce": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.18.0.tgz", + "integrity": "sha512-tUA3XoKx5xjoi3EDcngk0VUYMhvfXLhS4s7CntpLPh1qtLYtgSCexTIMUHkCy6MqyozRW98bdW3a2yHPEADRnQ==", + "dev": true, + "requires": { + "azure-devops-node-api": "^11.0.1", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "commander": "^6.1.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "keytar": "^7.7.0", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^5.1.0", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.4.23", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "dependencies": { + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true + }, + "hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + } + } + }, "@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -16438,6 +16486,24 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, "aggregate-error": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", @@ -16554,11 +16620,12 @@ } }, "applicationinsights": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.3.6.tgz", - "integrity": "sha512-ZzXXpZpDRGcy6Pp5V319nDF9/+Ey7jNknEXZyaBajtC5onN0dcBem6ng5jcb3MPH2AjYWRI8XgyNEuzP/6Y5/A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.5.0.tgz", + "integrity": "sha512-6kIFmpANRok+6FhCOmO7ZZ/mh7fdNKn17BaT13cg/RV5roLPJlA6q8srWexayHd3MPcwMb9072e8Zp0P47s/pw==", "requires": { - "@azure/core-http": "^2.2.3", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.10.0", "@microsoft/applicationinsights-web-snippet": "^1.0.1", "@opentelemetry/api": "^1.0.4", "@opentelemetry/core": "^1.0.1", @@ -16574,7 +16641,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "dev": true, + "optional": true }, "arch": { "version": "2.2.0", @@ -16609,6 +16677,7 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", "dev": true, + "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -16893,7 +16962,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "atob": { "version": "2.1.2", @@ -17715,7 +17784,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", - "dev": true + "dev": true, + "optional": true }, "chrome-trace-event": { "version": "1.0.2", @@ -17988,7 +18058,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "dev": true, + "optional": true }, "constants-browserify": { "version": "1.0.0", @@ -18392,7 +18463,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true + "dev": true, + "optional": true }, "deep-is": { "version": "0.1.3", @@ -18509,13 +18581,14 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true + "dev": true, + "optional": true }, "des.js": { "version": "1.0.0", @@ -18537,7 +18610,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true + "dev": true, + "optional": true }, "diagnostic-channel": { "version": "1.1.0", @@ -19645,7 +19719,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true + "dev": true, + "optional": true }, "expand-tilde": { "version": "2.0.2", @@ -20127,6 +20202,16 @@ } } }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -20255,6 +20340,7 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, + "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -20270,13 +20356,15 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "dev": true, + "optional": true }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -20344,7 +20432,8 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", - "dev": true + "dev": true, + "optional": true }, "glob": { "version": "7.2.0", @@ -20805,7 +20894,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true + "dev": true, + "optional": true }, "has-value": { "version": "1.0.0", @@ -20931,15 +21021,6 @@ "debug": "4" }, "dependencies": { - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, "debug": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", @@ -20961,26 +21042,15 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, "requires": { "agent-base": "6", "debug": "4" }, "dependencies": { - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, "debug": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -21905,9 +21975,9 @@ "dev": true }, "jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "jsx-ast-utils": { "version": "3.2.1", @@ -21936,6 +22006,7 @@ "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", "dev": true, + "optional": true, "requires": { "node-addon-api": "^3.0.0", "prebuild-install": "^6.0.0" @@ -22524,7 +22595,8 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true + "dev": true, + "optional": true }, "mocha": { "version": "9.2.2", @@ -22822,8 +22894,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mute-stdout": { "version": "1.0.1", @@ -22878,7 +22949,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true + "dev": true, + "optional": true }, "natural-compare": { "version": "1.4.0", @@ -22944,6 +23016,7 @@ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", "dev": true, + "optional": true, "requires": { "semver": "^5.4.1" } @@ -22952,15 +23025,8 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } + "dev": true, + "optional": true }, "node-has-native-dependencies": { "version": "1.0.2", @@ -23243,6 +23309,7 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, + "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -23921,6 +23988,7 @@ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", "dev": true, + "optional": true, "requires": { "detect-libc": "^1.0.3", "expand-template": "^2.0.3", @@ -23942,6 +24010,7 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -23970,7 +24039,8 @@ "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true }, "process-nextick-args": { "version": "2.0.1", @@ -24004,11 +24074,6 @@ "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", "dev": true }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, "public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -24047,7 +24112,8 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true }, "qs": { "version": "6.5.3", @@ -24078,11 +24144,6 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -24113,6 +24174,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -24124,7 +24186,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true + "dev": true, + "optional": true } } }, @@ -24368,11 +24431,6 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -24683,13 +24741,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true + "dev": true, + "optional": true }, "simple-get": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", "dev": true, + "optional": true, "requires": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -24701,6 +24761,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", "dev": true, + "optional": true, "requires": { "mimic-response": "^2.0.0" } @@ -24709,7 +24770,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "dev": true + "dev": true, + "optional": true } } }, @@ -25335,6 +25397,7 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", "dev": true, + "optional": true, "requires": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -25347,6 +25410,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, + "optional": true, "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -25358,6 +25422,7 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -25368,6 +25433,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, + "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -25379,6 +25445,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, + "optional": true, "requires": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -25605,11 +25672,6 @@ "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", "dev": true }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", @@ -25884,13 +25946,15 @@ "tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -26062,11 +26126,6 @@ "through2-filter": "^3.0.0" } }, - "universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" - }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -26183,15 +26242,6 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true }, - "url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", @@ -26239,7 +26289,8 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true }, "v8-compile-cache": { "version": "2.3.0", @@ -26340,79 +26391,6 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, - "vsce": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.7.0.tgz", - "integrity": "sha512-CKU34wrQlbKDeJCRBkd1a8iwF9EvNxcYMg9hAUH6AxFGR6Wo2IKWwt3cJIcusHxx6XdjDHWlfAS/fJN30uvVnA==", - "dev": true, - "requires": { - "azure-devops-node-api": "^11.0.1", - "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", - "glob": "^7.0.6", - "hosted-git-info": "^4.0.2", - "keytar": "^7.7.0", - "leven": "^3.1.0", - "markdown-it": "^12.3.2", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^5.1.0", - "tmp": "^0.2.1", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.4.23", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" - }, - "dependencies": { - "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true - }, - "hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - } - } - }, "vscode-debugadapter": { "version": "1.35.0", "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.35.0.tgz", @@ -26516,11 +26494,6 @@ "graceful-fs": "^4.1.2" } }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, "webpack": { "version": "5.76.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", @@ -26705,15 +26678,6 @@ "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -26777,6 +26741,7 @@ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, + "optional": true, "requires": { "string-width": "^1.0.2 || 2" } diff --git a/package.json b/package.json index 08d56bdf3bc0..94b007ef8b8e 100644 --- a/package.json +++ b/package.json @@ -1841,7 +1841,7 @@ }, "dependencies": { "@iarna/toml": "^2.2.5", - "@vscode/extension-telemetry": "^0.7.4-preview", + "@vscode/extension-telemetry": "^0.7.7", "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", "diff-match-patch": "^1.0.0", @@ -1899,6 +1899,7 @@ "@types/tmp": "^0.0.33", "@types/uuid": "^8.3.4", "@types/vscode": "^1.75.0", + "@vscode/vsce": "^2.18.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", @@ -1947,7 +1948,6 @@ "typemoq": "^2.1.0", "typescript": "4.5.5", "uuid": "^8.3.2", - "vsce": "^2.6.6", "vscode-debugadapter-testsupport": "^1.27.0", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", From 81debacf1618f3f2aaaf509073064caefc9f4a55 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 11 Apr 2023 13:09:36 -0700 Subject: [PATCH 043/336] Make fixes to `pythonTerminalEnvVarActivation` experiment (#21036) For https://github.com/microsoft/vscode-python/issues/20822 --- src/client/common/terminal/activator/index.ts | 7 +++++-- src/client/common/variables/environment.ts | 4 ++-- src/client/common/variables/types.ts | 6 +++++- .../extension/configuration/resolvers/helper.ts | 2 +- .../extension/configuration/resolvers/launch.ts | 6 ++++-- .../terminalEnvVarCollectionService.ts | 14 +++++++++++++- .../terminals/activator/index.unit.test.ts | 17 +++++++++++++++-- ...terminalEnvVarCollectionService.unit.test.ts | 13 ++++++++++++- 8 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/client/common/terminal/activator/index.ts b/src/client/common/terminal/activator/index.ts index 5bc76c0cb0f8..1c2cf4041585 100644 --- a/src/client/common/terminal/activator/index.ts +++ b/src/client/common/terminal/activator/index.ts @@ -5,9 +5,10 @@ import { inject, injectable, multiInject } from 'inversify'; import { Terminal } from 'vscode'; -import { IConfigurationService } from '../../types'; +import { IConfigurationService, IExperimentService } from '../../types'; import { ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, TerminalActivationOptions } from '../types'; import { BaseTerminalActivator } from './base'; +import { inTerminalEnvVarExperiment } from '../../experiments/helpers'; @injectable() export class TerminalActivator implements ITerminalActivator { @@ -17,6 +18,7 @@ export class TerminalActivator implements ITerminalActivator { @inject(ITerminalHelper) readonly helper: ITerminalHelper, @multiInject(ITerminalActivationHandler) private readonly handlers: ITerminalActivationHandler[], @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, ) { this.initialize(); } @@ -37,7 +39,8 @@ export class TerminalActivator implements ITerminalActivator { options?: TerminalActivationOptions, ): Promise { const settings = this.configurationService.getSettings(options?.resource); - const activateEnvironment = settings.terminal.activateEnvironment; + const activateEnvironment = + settings.terminal.activateEnvironment && !inTerminalEnvVarExperiment(this.experimentService); if (!activateEnvironment || options?.hideFromUser) { return false; } diff --git a/src/client/common/variables/environment.ts b/src/client/common/variables/environment.ts index 4b3652e5021f..81e6b8b2cfc9 100644 --- a/src/client/common/variables/environment.ts +++ b/src/client/common/variables/environment.ts @@ -57,7 +57,7 @@ export class EnvironmentVariablesService implements IEnvironmentVariablesService public mergeVariables( source: EnvironmentVariables, target: EnvironmentVariables, - options?: { overwrite?: boolean }, + options?: { overwrite?: boolean; mergeAll?: boolean }, ) { if (!target) { return; @@ -67,7 +67,7 @@ export class EnvironmentVariablesService implements IEnvironmentVariablesService source = normCaseKeys(source); const settingsNotToMerge = ['PYTHONPATH', this.pathVariable]; Object.keys(source).forEach((setting) => { - if (settingsNotToMerge.indexOf(setting) >= 0) { + if (!options?.mergeAll && settingsNotToMerge.indexOf(setting) >= 0) { return; } if (target[setting] === undefined || options?.overwrite) { diff --git a/src/client/common/variables/types.ts b/src/client/common/variables/types.ts index e4c301db7dd7..252a0d48038f 100644 --- a/src/client/common/variables/types.ts +++ b/src/client/common/variables/types.ts @@ -10,7 +10,11 @@ export const IEnvironmentVariablesService = Symbol('IEnvironmentVariablesService export interface IEnvironmentVariablesService { parseFile(filePath?: string, baseVars?: EnvironmentVariables): Promise; parseFileSync(filePath?: string, baseVars?: EnvironmentVariables): EnvironmentVariables | undefined; - mergeVariables(source: EnvironmentVariables, target: EnvironmentVariables, options?: { overwrite?: boolean }): void; + mergeVariables( + source: EnvironmentVariables, + target: EnvironmentVariables, + options?: { overwrite?: boolean; mergeAll?: boolean }, + ): void; appendPythonPath(vars: EnvironmentVariables, ...pythonPaths: string[]): void; appendPath(vars: EnvironmentVariables, ...paths: string[]): void; } diff --git a/src/client/debugger/extension/configuration/resolvers/helper.ts b/src/client/debugger/extension/configuration/resolvers/helper.ts index 96cdf65bfac3..15be5f97538e 100644 --- a/src/client/debugger/extension/configuration/resolvers/helper.ts +++ b/src/client/debugger/extension/configuration/resolvers/helper.ts @@ -44,7 +44,7 @@ export class DebugEnvironmentVariablesHelper implements IDebugEnvironmentVariabl // take precedence over env file. this.envParser.mergeVariables(debugLaunchEnvVars, env, { overwrite: true }); if (baseVars) { - this.envParser.mergeVariables(baseVars, env); + this.envParser.mergeVariables(baseVars, env, { mergeAll: true }); } // Append the PYTHONPATH and PATH variables. diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index 6a28075d4353..f48b2c19aaff 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -19,6 +19,8 @@ import { getProgram, IDebugEnvironmentVariablesService } from './helper'; @injectable() export class LaunchConfigurationResolver extends BaseConfigurationResolver { + private isPythonSet = false; + constructor( @inject(IDiagnosticsService) @named(InvalidPythonPathInDebuggerServiceId) @@ -36,6 +38,7 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { + this.isPythonSet = debugConfiguration.python !== undefined; if ( debugConfiguration.name === undefined && debugConfiguration.type === undefined && @@ -84,7 +87,6 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { - const isPythonSet = debugConfiguration.python !== undefined; if (debugConfiguration.python === undefined) { debugConfiguration.python = debugConfiguration.pythonPath; } @@ -104,7 +106,7 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { @@ -68,6 +75,11 @@ export class TerminalEnvVarCollectionService implements IExtensionSingleActivati } public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { + const settings = this.configurationService.getSettings(resource); + if (!settings.terminal.activateEnvironment) { + traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); + return; + } const env = await this.environmentActivationService.getActivatedEnvironmentVariables( resource, undefined, diff --git a/src/test/common/terminals/activator/index.unit.test.ts b/src/test/common/terminals/activator/index.unit.test.ts index 9dff5a800cad..a50b946c391f 100644 --- a/src/test/common/terminals/activator/index.unit.test.ts +++ b/src/test/common/terminals/activator/index.unit.test.ts @@ -12,7 +12,12 @@ import { ITerminalActivator, ITerminalHelper, } from '../../../../client/common/terminal/types'; -import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../../client/common/types'; +import { + IConfigurationService, + IExperimentService, + IPythonSettings, + ITerminalSettings, +} from '../../../../client/common/types'; suite('Terminal Activator', () => { let activator: TerminalActivator; @@ -20,9 +25,12 @@ suite('Terminal Activator', () => { let handler1: TypeMoq.IMock; let handler2: TypeMoq.IMock; let terminalSettings: TypeMoq.IMock; + let experimentService: TypeMoq.IMock; setup(() => { baseActivator = TypeMoq.Mock.ofType(); terminalSettings = TypeMoq.Mock.ofType(); + experimentService = TypeMoq.Mock.ofType(); + experimentService.setup((e) => e.inExperimentSync(TypeMoq.It.isAny())).returns(() => false); handler1 = TypeMoq.Mock.ofType(); handler2 = TypeMoq.Mock.ofType(); const configService = TypeMoq.Mock.ofType(); @@ -37,7 +45,12 @@ suite('Terminal Activator', () => { protected initialize() { this.baseActivator = baseActivator.object; } - })(TypeMoq.Mock.ofType().object, [handler1.object, handler2.object], configService.object); + })( + TypeMoq.Mock.ofType().object, + [handler1.object, handler2.object], + configService.object, + experimentService.object, + ); }); async function testActivationAndHandlers( activationSuccessful: boolean, diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 4ac04cf1ee22..324de1174584 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -11,7 +11,13 @@ import { EnvironmentVariableCollection, ProgressLocation, Uri } from 'vscode'; import { IApplicationShell, IApplicationEnvironment } from '../../../client/common/application/types'; import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; import { IPlatformService } from '../../../client/common/platform/types'; -import { IExtensionContext, IExperimentService, Resource } from '../../../client/common/types'; +import { + IExtensionContext, + IExperimentService, + Resource, + IConfigurationService, + IPythonSettings, +} from '../../../client/common/types'; import { Interpreters } from '../../../client/common/utils/localize'; import { getOSType } from '../../../client/common/utils/platform'; import { defaultShells } from '../../../client/interpreter/activation/service'; @@ -57,6 +63,10 @@ suite('Terminal Environment Variable Collection Service', () => { }) .thenResolve(); environmentActivationService = mock(); + const configService = mock(); + when(configService.getSettings(anything())).thenReturn(({ + terminal: { activateEnvironment: true }, + } as unknown) as IPythonSettings); terminalEnvVarCollectionService = new TerminalEnvVarCollectionService( instance(platform), instance(interpreterService), @@ -66,6 +76,7 @@ suite('Terminal Environment Variable Collection Service', () => { instance(applicationEnvironment), [], instance(environmentActivationService), + instance(configService), ); }); From b4f0ad1345c4323320a5fe4281affe62ceea8dcc Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 11 Apr 2023 13:25:37 -0700 Subject: [PATCH 044/336] Expand Testing for Unittest Run in PythonFiles (#21028) closes https://github.com/microsoft/vscode-python/issues/21031 --- .../unittestadapter/.data/test_fail_simple.py | 21 +++ .../unittestadapter/.data/test_two_classes.py | 20 +++ .../.data/two_patterns/pattern_a_test.py | 22 +++ .../.data/two_patterns/test_pattern_b.py | 15 ++ .../.data/unittest_folder/test_add.py | 22 +++ .../.data/unittest_folder/test_subtract.py | 21 +++ .../tests/unittestadapter/test_execution.py | 160 ++++++++++++++++++ pythonFiles/unittestadapter/execution.py | 4 +- 8 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 pythonFiles/tests/unittestadapter/.data/test_fail_simple.py create mode 100644 pythonFiles/tests/unittestadapter/.data/test_two_classes.py create mode 100644 pythonFiles/tests/unittestadapter/.data/two_patterns/pattern_a_test.py create mode 100644 pythonFiles/tests/unittestadapter/.data/two_patterns/test_pattern_b.py create mode 100644 pythonFiles/tests/unittestadapter/.data/unittest_folder/test_add.py create mode 100644 pythonFiles/tests/unittestadapter/.data/unittest_folder/test_subtract.py diff --git a/pythonFiles/tests/unittestadapter/.data/test_fail_simple.py b/pythonFiles/tests/unittestadapter/.data/test_fail_simple.py new file mode 100644 index 000000000000..e329c3fd7003 --- /dev/null +++ b/pythonFiles/tests/unittestadapter/.data/test_fail_simple.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + +# Test class for the test_fail_simple test. +# The test_failed_tests function should return a dictionary with a "success" status +# and the two tests with their outcome as "failed". + +class RunFailSimple(unittest.TestCase): + """Test class for the test_fail_simple test. + + The test_failed_tests function should return a dictionary with a "success" status + and the two tests with their outcome as "failed". + """ + + def test_one_fail(self) -> None: + self.assertGreater(2, 3) + + def test_two_fail(self) -> None: + self.assertNotEqual(1, 1) diff --git a/pythonFiles/tests/unittestadapter/.data/test_two_classes.py b/pythonFiles/tests/unittestadapter/.data/test_two_classes.py new file mode 100644 index 000000000000..60b26706ad42 --- /dev/null +++ b/pythonFiles/tests/unittestadapter/.data/test_two_classes.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + +# Test class which runs for the test_multiple_ids_run test with the two class parameters. +# Both test functions will be returned in a dictionary with a "success" status, +# and the two tests with their outcome as "success". + + +class ClassOne(unittest.TestCase): + + def test_one(self) -> None: + self.assertGreater(2, 1) + +class ClassTwo(unittest.TestCase): + + def test_two(self) -> None: + self.assertGreater(2, 1) + diff --git a/pythonFiles/tests/unittestadapter/.data/two_patterns/pattern_a_test.py b/pythonFiles/tests/unittestadapter/.data/two_patterns/pattern_a_test.py new file mode 100644 index 000000000000..4f3f77e1056e --- /dev/null +++ b/pythonFiles/tests/unittestadapter/.data/two_patterns/pattern_a_test.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +# Test class for the two file pattern test. It is pattern *test.py. +# The test_ids_multiple_runs function should return a dictionary with a "success" status, +# and the two tests with their outcome as "success". + + + +class DiscoveryA(unittest.TestCase): + """Test class for the two file pattern test. It is pattern *test.py + + The test_ids_multiple_runs function should return a dictionary with a "success" status, + and the two tests with their outcome as "success". + """ + + def test_one_a(self) -> None: + self.assertGreater(2, 1) + + def test_two_a(self) -> None: + self.assertNotEqual(2, 1) \ No newline at end of file diff --git a/pythonFiles/tests/unittestadapter/.data/two_patterns/test_pattern_b.py b/pythonFiles/tests/unittestadapter/.data/two_patterns/test_pattern_b.py new file mode 100644 index 000000000000..a912699383ca --- /dev/null +++ b/pythonFiles/tests/unittestadapter/.data/two_patterns/test_pattern_b.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +# Test class for the two file pattern test. This file is pattern test*.py. +# The test_ids_multiple_runs function should return a dictionary with a "success" status, +# and the two tests with their outcome as "success". + +class DiscoveryB(unittest.TestCase): + + def test_one_b(self) -> None: + self.assertGreater(2, 1) + + def test_two_b(self) -> None: + self.assertNotEqual(2, 1) \ No newline at end of file diff --git a/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_add.py b/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_add.py new file mode 100644 index 000000000000..2e616077ec40 --- /dev/null +++ b/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_add.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +# Test class which runs for the test_multiple_ids_run test with the two test +# files in the same folder. The cwd is set to the parent folder. This should return +# a dictionary with a "success" status and the two tests with their outcome as "success". + +def add(a, b): + return a + b + + +class TestAddFunction(unittest.TestCase): + + def test_add_positive_numbers(self): + result = add(2, 3) + self.assertEqual(result, 5) + + + def test_add_negative_numbers(self): + result = add(-2, -3) + self.assertEqual(result, -5) \ No newline at end of file diff --git a/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_subtract.py b/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_subtract.py new file mode 100644 index 000000000000..4028e25825d1 --- /dev/null +++ b/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_subtract.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +# Test class which runs for the test_multiple_ids_run test with the two test +# files in the same folder. The cwd is set to the parent folder. This should return +# a dictionary with a "success" status and the two tests with their outcome as "success". + +def subtract(a, b): + return a - b + + +class TestSubtractFunction(unittest.TestCase): + def test_subtract_positive_numbers(self): + result = subtract(5, 3) + self.assertEqual(result, 2) + + + def test_subtract_negative_numbers(self): + result = subtract(-2, -3) + self.assertEqual(result, 1) \ No newline at end of file diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index 14ddefa48d52..5353703c59f2 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -92,3 +92,163 @@ def test_single_ids_run() -> None: assert id_result is not None assert "outcome" in id_result assert id_result["outcome"] == "success" + + +@pytest.mark.parametrize( + "test_ids, pattern, cwd, expected_outcome", + [ + ( + [ + "test_add.TestAddFunction.test_add_negative_numbers", + "test_add.TestAddFunction.test_add_positive_numbers", + ], + "test_add.py", + os.fspath(TEST_DATA_PATH / "unittest_folder"), + "success", + ), + ( + [ + "test_add.TestAddFunction.test_add_negative_numbers", + "test_add.TestAddFunction.test_add_positive_numbers", + "test_subtract.TestSubtractFunction.test_subtract_negative_numbers", + "test_subtract.TestSubtractFunction.test_subtract_positive_numbers", + ], + "test*", + os.fspath(TEST_DATA_PATH / "unittest_folder"), + "success", + ), + ( + [ + "pattern_a_test.DiscoveryA.test_one_a", + "pattern_a_test.DiscoveryA.test_two_a", + ], + "*test", + os.fspath(TEST_DATA_PATH / "two_patterns"), + "success", + ), + ( + [ + "test_pattern_b.DiscoveryB.test_one_b", + "test_pattern_b.DiscoveryB.test_two_b", + ], + "test_*", + os.fspath(TEST_DATA_PATH / "two_patterns"), + "success", + ), + ( + [ + "file_one.CaseTwoFileOne.test_one", + "file_one.CaseTwoFileOne.test_two", + "folder.file_two.CaseTwoFileTwo.test_one", + "folder.file_two.CaseTwoFileTwo.test_two", + ], + "*", + os.fspath(TEST_DATA_PATH / "utils_nested_cases"), + "success", + ), + ( + [ + "test_two_classes.ClassOne.test_one", + "test_two_classes.ClassTwo.test_two", + ], + "test_two_classes.py", + os.fspath(TEST_DATA_PATH), + "success", + ), + ], +) +def test_multiple_ids_run(test_ids, pattern, cwd, expected_outcome) -> None: + """ + The following are all successful tests of different formats. + + # 1. Two tests with the `pattern` specified as a file + # 2. Two test files in the same folder called `unittest_folder` + # 3. A folder with two different test file patterns, this test gathers pattern `*test` + # 4. A folder with two different test file patterns, this test gathers pattern `test_*` + # 5. A nested structure where a test file is on the same level as a folder containing a test file + # 6. Test file with two test classes + + All tests should have the outcome of `success`. + """ + actual = run_tests(cwd, test_ids, pattern, None, "fake-uuid") + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == cwd + assert "result" in actual + result = actual["result"] + assert len(result) == len(test_ids) + for test_id in test_ids: + assert test_id in result + id_result = result[test_id] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == expected_outcome + assert True + + +def test_failed_tests(): + """This test runs on a single file `test_fail` with two tests that fail.""" + test_ids = [ + "test_fail_simple.RunFailSimple.test_one_fail", + "test_fail_simple.RunFailSimple.test_two_fail", + ] + actual = run_tests( + os.fspath(TEST_DATA_PATH), test_ids, "test_fail_simple*", None, "fake-uuid" + ) + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert "result" in actual + result = actual["result"] + assert len(result) == len(test_ids) + for test_id in test_ids: + assert test_id in result + id_result = result[test_id] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == "failure" + assert "message" and "traceback" in id_result + assert True + + +def test_unknown_id(): + """This test runs on a unknown test_id, therefore it should return + an error as the outcome as it attempts to find the given test. + """ + test_ids = ["unknown_id"] + actual = run_tests( + os.fspath(TEST_DATA_PATH), test_ids, "test_fail_simple*", None, "fake-uuid" + ) + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert "result" in actual + result = actual["result"] + assert len(result) == len(test_ids) + assert "unittest.loader._FailedTest.unknown_id" in result + id_result = result["unittest.loader._FailedTest.unknown_id"] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == "error" + assert "message" and "traceback" in id_result + + +def test_incorrect_path(): + """This test runs on a non existent path, therefore it should return + an error as the outcome as it attempts to find the given folder. + """ + test_ids = ["unknown_id"] + actual = run_tests( + os.fspath(TEST_DATA_PATH / "unknown_folder"), + test_ids, + "test_fail_simple*", + None, + "fake-uuid", + ) + assert actual + assert all(item in actual for item in ("cwd", "status", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "unknown_folder") diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 569ad7fdf298..5acb11b2609f 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -62,7 +62,9 @@ class TestOutcomeEnum(str, enum.Enum): class UnittestTestResult(unittest.TextTestResult): - formatted: Dict[str, Dict[str, Union[str, None]]] = dict() + def __init__(self, *args, **kwargs): + self.formatted: Dict[str, Dict[str, Union[str, None]]] = dict() + super(UnittestTestResult, self).__init__(*args, **kwargs) def startTest(self, test: unittest.TestCase): super(UnittestTestResult, self).startTest(test) From 586d18f1dcb312612b32f6b30b90492f60e30f30 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 11 Apr 2023 15:36:30 -0700 Subject: [PATCH 045/336] Fix for missing `pip` in codespaces with `venv` (#21041) no change logs because this feature is not released yet. Fixes https://github.com/microsoft/vscode-python/issues/21039 --- pythonFiles/create_microvenv.py | 37 ------------ pythonFiles/create_venv.py | 60 ++++++++++++++++--- pythonFiles/tests/test_create_microvenv.py | 31 ---------- pythonFiles/tests/test_create_venv.py | 50 ++++++++++------ .../provider/venvProgressAndTelemetry.ts | 10 ++-- 5 files changed, 89 insertions(+), 99 deletions(-) diff --git a/pythonFiles/create_microvenv.py b/pythonFiles/create_microvenv.py index 3dfd554a3012..10eae38ab977 100644 --- a/pythonFiles/create_microvenv.py +++ b/pythonFiles/create_microvenv.py @@ -6,7 +6,6 @@ import pathlib import subprocess import sys -import urllib.request as url_lib from typing import Optional, Sequence VENV_NAME = ".venv" @@ -29,13 +28,6 @@ def run_process(args: Sequence[str], error_message: str) -> None: def parse_args(argv: Sequence[str]) -> argparse.Namespace: parser = argparse.ArgumentParser() - parser.add_argument( - "--install-pip", - action="store_true", - default=False, - help="Install pip into the virtual environment.", - ) - parser.add_argument( "--name", default=VENV_NAME, @@ -54,31 +46,6 @@ def create_microvenv(name: str): ) -def download_pip_pyz(name: str): - url = "https://bootstrap.pypa.io/pip/pip.pyz" - print("CREATE_MICROVENV.DOWNLOADING_PIP") - - try: - with url_lib.urlopen(url) as response: - pip_pyz_path = os.fspath(CWD / name / "pip.pyz") - with open(pip_pyz_path, "wb") as out_file: - data = response.read() - out_file.write(data) - out_file.flush() - except Exception: - raise MicroVenvError("CREATE_MICROVENV.DOWNLOAD_PIP_FAILED") - - -def install_pip(name: str): - pip_pyz_path = os.fspath(CWD / name / "pip.pyz") - executable = os.fspath(CWD / name / "bin" / "python") - print("CREATE_MICROVENV.INSTALLING_PIP") - run_process( - [executable, pip_pyz_path, "install", "pip"], - "CREATE_MICROVENV.INSTALL_PIP_FAILED", - ) - - def main(argv: Optional[Sequence[str]] = None) -> None: if argv is None: argv = [] @@ -88,10 +55,6 @@ def main(argv: Optional[Sequence[str]] = None) -> None: create_microvenv(args.name) print("CREATE_MICROVENV.CREATED_MICROVENV") - if args.install_pip: - download_pip_pyz(args.name) - install_pip(args.name) - if __name__ == "__main__": main(sys.argv[1:]) diff --git a/pythonFiles/create_venv.py b/pythonFiles/create_venv.py index 8ebafdfca32c..749c36f8088b 100644 --- a/pythonFiles/create_venv.py +++ b/pythonFiles/create_venv.py @@ -7,6 +7,7 @@ import pathlib import subprocess import sys +import urllib.request as url_lib from typing import List, Optional, Sequence, Union VENV_NAME = ".venv" @@ -126,51 +127,92 @@ def add_gitignore(name: str) -> None: f.write("*") +def download_pip_pyz(name: str): + url = "https://bootstrap.pypa.io/pip/pip.pyz" + print("CREATE_VENV.DOWNLOADING_PIP") + + try: + with url_lib.urlopen(url) as response: + pip_pyz_path = os.fspath(CWD / name / "pip.pyz") + with open(pip_pyz_path, "wb") as out_file: + data = response.read() + out_file.write(data) + out_file.flush() + except Exception: + raise VenvError("CREATE_VENV.DOWNLOAD_PIP_FAILED") + + +def install_pip(name: str): + pip_pyz_path = os.fspath(CWD / name / "pip.pyz") + executable = get_venv_path(name) + print("CREATE_VENV.INSTALLING_PIP") + run_process( + [executable, pip_pyz_path, "install", "pip"], + "CREATE_VENV.INSTALL_PIP_FAILED", + ) + + def main(argv: Optional[Sequence[str]] = None) -> None: if argv is None: argv = [] args = parse_args(argv) use_micro_venv = False - if not is_installed("venv"): + venv_installed = is_installed("venv") + pip_installed = is_installed("pip") + ensure_pip_installed = is_installed("ensurepip") + + if not venv_installed: if sys.platform == "win32": raise VenvError("CREATE_VENV.VENV_NOT_FOUND") else: use_micro_venv = True - pip_installed = is_installed("pip") - deps_needed = args.requirements or args.extras or args.toml - if deps_needed and not pip_installed and not use_micro_venv: - raise VenvError("CREATE_VENV.PIP_NOT_FOUND") - if venv_exists(args.name): + # A virtual environment with same name exists. + # We will use the existing virtual environment. venv_path = get_venv_path(args.name) print(f"EXISTING_VENV:{venv_path}") else: if use_micro_venv: + # `venv` was not found but on this platform we can use `microvenv` run_process( [ sys.executable, os.fspath(MICROVENV_SCRIPT_PATH), - "--install-pip", "--name", args.name, ], "CREATE_VENV.MICROVENV_FAILED_CREATION", ) - pip_installed = True + elif not pip_installed or not ensure_pip_installed: + # `venv` was found but `pip` or `ensurepip` was not found. + # We create a venv without `pip` in it. We will later install `pip`. + run_process( + [sys.executable, "-m", "venv", "--without-pip", args.name], + "CREATE_VENV.VENV_FAILED_CREATION", + ) else: + # Both `venv` and `pip` were found. So create a .venv normally run_process( [sys.executable, "-m", "venv", args.name], "CREATE_VENV.VENV_FAILED_CREATION", ) + venv_path = get_venv_path(args.name) print(f"CREATED_VENV:{venv_path}") + if args.git_ignore: add_gitignore(args.name) - if pip_installed: + # At this point we have a .venv. Now we handle installing `pip`. + if pip_installed and ensure_pip_installed: + # We upgrade pip if it is already installed. upgrade_pip(venv_path) + else: + # `pip` was not found, so we download it and install it. + download_pip_pyz(args.name) + install_pip(args.name) if args.toml: print(f"VENV_INSTALLING_PYPROJECT: {args.toml}") diff --git a/pythonFiles/tests/test_create_microvenv.py b/pythonFiles/tests/test_create_microvenv.py index 26a57dda26a1..f123052c491c 100644 --- a/pythonFiles/tests/test_create_microvenv.py +++ b/pythonFiles/tests/test_create_microvenv.py @@ -27,34 +27,3 @@ def run_process(args, error_message): create_microvenv.main() assert run_process_called == True - - -def test_create_microvenv_with_pip(): - importlib.reload(create_microvenv) - - download_pip_pyz_called = False - - def download_pip_pyz(name): - nonlocal download_pip_pyz_called - download_pip_pyz_called = True - assert name == create_microvenv.VENV_NAME - - create_microvenv.download_pip_pyz = download_pip_pyz - - run_process_called = False - - def run_process(args, error_message): - if "install" in args and "pip" in args: - nonlocal run_process_called - run_process_called = True - pip_pyz_path = os.fspath( - create_microvenv.CWD / create_microvenv.VENV_NAME / "pip.pyz" - ) - executable = os.fspath( - create_microvenv.CWD / create_microvenv.VENV_NAME / "bin" / "python" - ) - assert args == [executable, pip_pyz_path, "install", "pip"] - assert error_message == "CREATE_MICROVENV.INSTALL_PIP_FAILED" - - create_microvenv.run_process = run_process - create_microvenv.main(["--install-pip"]) diff --git a/pythonFiles/tests/test_create_venv.py b/pythonFiles/tests/test_create_venv.py index 2949a18ebd40..bebe304c13c3 100644 --- a/pythonFiles/tests/test_create_venv.py +++ b/pythonFiles/tests/test_create_venv.py @@ -19,12 +19,12 @@ def test_venv_not_installed_unix(): def run_process(args, error_message): nonlocal run_process_called - if "--install-pip" in args: + microvenv_path = os.fspath(create_venv.MICROVENV_SCRIPT_PATH) + if microvenv_path in args: run_process_called = True assert args == [ sys.executable, - os.fspath(create_venv.MICROVENV_SCRIPT_PATH), - "--install-pip", + microvenv_path, "--name", ".test_venv", ] @@ -49,20 +49,6 @@ def test_venv_not_installed_windows(): assert str(e.value) == "CREATE_VENV.VENV_NOT_FOUND" -@pytest.mark.parametrize("install", ["requirements", "toml"]) -def test_pip_not_installed(install): - importlib.reload(create_venv) - create_venv.venv_exists = lambda _n: True - create_venv.is_installed = lambda module: module != "pip" - create_venv.run_process = lambda _args, _error_message: None - with pytest.raises(create_venv.VenvError) as e: - if install == "requirements": - create_venv.main(["--requirements", "requirements-for-test.txt"]) - elif install == "toml": - create_venv.main(["--toml", "pyproject.toml", "--extras", "test"]) - assert str(e.value) == "CREATE_VENV.PIP_NOT_FOUND" - - @pytest.mark.parametrize("env_exists", ["hasEnv", "noEnv"]) @pytest.mark.parametrize("git_ignore", ["useGitIgnore", "skipGitIgnore"]) @pytest.mark.parametrize("install", ["requirements", "toml", "skipInstall"]) @@ -207,3 +193,33 @@ def run_process(args, error_message): create_venv.install_requirements(sys.executable, extras) assert actual == expected + + +def test_create_venv_missing_pip(): + importlib.reload(create_venv) + create_venv.venv_exists = lambda _n: True + create_venv.is_installed = lambda module: module != "pip" + + download_pip_pyz_called = False + + def download_pip_pyz(name): + nonlocal download_pip_pyz_called + download_pip_pyz_called = True + assert name == create_venv.VENV_NAME + + create_venv.download_pip_pyz = download_pip_pyz + + run_process_called = False + + def run_process(args, error_message): + if "install" in args and "pip" in args: + nonlocal run_process_called + run_process_called = True + pip_pyz_path = os.fspath( + create_venv.CWD / create_venv.VENV_NAME / "pip.pyz" + ) + assert args[1:] == [pip_pyz_path, "install", "pip"] + assert error_message == "CREATE_VENV.INSTALL_PIP_FAILED" + + create_venv.run_process = run_process + create_venv.main([]) diff --git a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts index b8efff3afbd8..818e06c3f630 100644 --- a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts +++ b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts @@ -25,10 +25,10 @@ const CREATING_MICROVENV_MARKER = 'CREATE_MICROVENV.CREATING_MICROVENV'; const CREATE_MICROVENV_FAILED_MARKER = 'CREATE_VENV.MICROVENV_FAILED_CREATION'; const CREATE_MICROVENV_FAILED_MARKER2 = 'CREATE_MICROVENV.MICROVENV_FAILED_CREATION'; const MICROVENV_CREATED_MARKER = 'CREATE_MICROVENV.CREATED_MICROVENV'; -const INSTALLING_PIP_MARKER = 'CREATE_MICROVENV.INSTALLING_PIP'; -const INSTALL_PIP_FAILED_MARKER = 'CREATE_MICROVENV.INSTALL_PIP_FAILED'; -const DOWNLOADING_PIP_MARKER = 'CREATE_MICROVENV.DOWNLOADING_PIP'; -const DOWNLOAD_PIP_FAILED_MARKER = 'CREATE_MICROVENV.DOWNLOAD_PIP_FAILED'; +const INSTALLING_PIP_MARKER = 'CREATE_VENV.INSTALLING_PIP'; +const INSTALL_PIP_FAILED_MARKER = 'CREATE_VENV.INSTALL_PIP_FAILED'; +const DOWNLOADING_PIP_MARKER = 'CREATE_VENV.DOWNLOADING_PIP'; +const DOWNLOAD_PIP_FAILED_MARKER = 'CREATE_VENV.DOWNLOAD_PIP_FAILED'; export class VenvProgressAndTelemetry { private readonly processed = new Set(); @@ -266,7 +266,7 @@ export class VenvProgressAndTelemetry { (progress: CreateEnvironmentProgress) => { progress.report({ message: CreateEnv.Venv.upgradingPip }); sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { - environmentType: 'microvenv', + environmentType: 'venv', using: 'pipUpgrade', }); return undefined; From b68facc42f684f26c5f83f3cfe895e7aa4bb2bee Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 12 Apr 2023 17:16:05 -0700 Subject: [PATCH 046/336] Remove unused bits entries from package.json (#21046) --- package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/package.json b/package.json index 94b007ef8b8e..ac7c40eee442 100644 --- a/package.json +++ b/package.json @@ -1957,10 +1957,5 @@ "webpack-node-externals": "^3.0.0", "webpack-require-from": "^1.8.6", "yargs": "^15.3.1" - }, - "__metadata": { - "id": "f1f59ae4-9318-4f3c-a9b5-81b2eaa5f8a5", - "publisherDisplayName": "Microsoft", - "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" } } From 7ac230c8a09f4c9e2c51a207652f5c1fe6f14ab2 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 17 Apr 2023 09:22:10 -0700 Subject: [PATCH 047/336] Localize walkthroughs (#21045) Closes https://github.com/microsoft/vscode-python/issues/20906 --------- Co-authored-by: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> --- package.json | 74 ++++++++++++++++++++++++------------------------ package.nls.json | 39 ++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index ac7c40eee442..893eb2299abd 100644 --- a/package.json +++ b/package.json @@ -83,24 +83,24 @@ "walkthroughs": [ { "id": "pythonWelcome", - "title": "Get Started with Python Development", - "description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", + "title": "%walkthrough.pythonWelcome.title%", + "description": "%walkthrough.pythonWelcome.description%", "when": "workspacePlatform != webworker", "steps": [ { "id": "python.createPythonFile", - "title": "Create a Python file", - "description": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", + "title": "%walkthrough.step.python.createPythonFile.title%", + "description": "%walkthrough.step.python.createPythonFile.description%", "media": { "svg": "resources/walkthrough/open-folder.svg", - "altText": "Open a Python file or a folder with a Python project." + "altText": "%walkthrough.step.python.createPythonFile.altText%" }, "when": "" }, { "id": "python.installPythonWin8", - "title": "Install Python", - "description": "The Python Extension requires Python to be installed. Install Python [from python.org](https://www.python.org/downloads).\n\n[Install Python](https://www.python.org/downloads)\n", + "title": "%walkthrough.step.python.installPythonWin8.title%", + "description": "%walkthrough.step.python.installPythonWin8.description%", "media": { "markdown": "resources/walkthrough/install-python-windows-8.md" }, @@ -108,8 +108,8 @@ }, { "id": "python.installPythonMac", - "title": "Install Python", - "description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via Brew](command:python.installPythonOnMac)\n", + "title": "%walkthrough.step.python.installPythonMac.title%", + "description": "%walkthrough.step.python.installPythonMac.description%", "media": { "markdown": "resources/walkthrough/install-python-macos.md" }, @@ -118,8 +118,8 @@ }, { "id": "python.installPythonLinux", - "title": "Install Python", - "description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via terminal](command:python.installPythonOnLinux)\n", + "title": "%walkthrough.step.python.installPythonLinux.title%", + "description": "%walkthrough.step.python.installPythonLinux.description%", "media": { "markdown": "resources/walkthrough/install-python-linux.md" }, @@ -128,40 +128,40 @@ }, { "id": "python.selectInterpreter", - "title": "Select a Python Interpreter", - "description": "Choose which Python interpreter/environment you want to use for your Python project.\n[Select Python Interpreter](command:python.setInterpreter)\n**Tip**: Run the ``Python: Select Interpreter`` command in the [Command Palette](command:workbench.action.showCommands).", + "title": "%walkthrough.step.python.selectInterpreter.title%", + "description": "%walkthrough.step.python.selectInterpreter.description%", "media": { "svg": "resources/walkthrough/python-interpreter.svg", - "altText": "Selecting a python interpreter from the status bar" + "altText": "%walkthrough.step.python.selectInterpreter.altText%" }, "when": "workspaceFolderCount == 0" }, { "id": "python.createEnvironment", - "title": "Create a Python Environment ", - "description": "Create an environment for your Python project.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).\n 🔍 Check out our [docs](https://aka.ms/pythonenvs) to learn more.", + "title": "%walkthrough.step.python.createEnvironment.title%", + "description": "%walkthrough.step.python.createEnvironment.description%", "media": { "svg": "resources/walkthrough/create-environment.svg", - "altText": "Creating a Python environment from the Command Palette" + "altText": "%walkthrough.step.python.createEnvironment.altText%" }, "when": "workspaceFolderCount > 0" }, { "id": "python.runAndDebug", - "title": "Run and debug your Python file", - "description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", + "title": "%walkthrough.step.python.runAndDebug.title%", + "description": "%walkthrough.step.python.runAndDebug.description%", "media": { "svg": "resources/walkthrough/rundebug2.svg", - "altText": "How to run and debug in VS Code with F5 or the play button on the top right." + "altText": "%walkthrough.step.python.runAndDebug.altText%" }, "when": "" }, { "id": "python.learnMoreWithDS", - "title": "Explore more resources", - "description": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Learn More](https://aka.ms/AA8dqti)", + "title": "%walkthrough.step.python.learnMoreWithDS.title%", + "description": "%walkthrough.step.python.learnMoreWithDS.description%", "media": { - "altText": "Image representing our documentation page and mailing list resources.", + "altText": "%walkthrough.step.python.learnMoreWithDS.altText%", "svg": "resources/walkthrough/learnmore.svg" }, "when": "" @@ -170,26 +170,26 @@ }, { "id": "pythonDataScienceWelcome", - "title": "Get Started with Python for Data Science", - "description": "Your first steps to getting started with a Data Science project with Python!", + "title": "%walkthrough.pythonDataScienceWelcome.title%", + "description": "%walkthrough.pythonDataScienceWelcome.description%", "when": "false", "steps": [ { "id": "python.installJupyterExt", - "title": "Install Jupyter extension", - "description": "If you haven't already, install the [Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\") to take full advantage of notebooks experiences in VS Code!\n \n[Search Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\")", + "title": "%walkthrough.step.python.installJupyterExt.title%", + "description": "%walkthrough.step.python.installJupyterExt.description%", "media": { "svg": "resources/walkthrough/data-science.svg", - "altText": "Creating a new Jupyter notebook" + "altText": "%walkthrough.step.python.installJupyterExt.altText%" } }, { "id": "python.createNewNotebook", - "title": "Create or open a Jupyter Notebook", - "description": "Right click in the file explorer and create a new file with an .ipynb extension. Or, open the [Command Palette](command:workbench.action.showCommands) and run the command \n``Jupyter: Create New Blank Notebook``.\n[Create new Jupyter Notebook](command:toSide:jupyter.createnewnotebook)\n If you have an existing project, you can also [open a folder](command:workbench.action.files.openFolder) and/or clone a project from GitHub: [clone a Git repository](command:git.clone).", + "title": "%walkthrough.step.python.createNewNotebook.title%", + "description": "%walkthrough.step.python.createNewNotebook.description%", "media": { "svg": "resources/walkthrough/create-notebook.svg", - "altText": "Creating a new Jupyter notebook" + "altText": "%walkthrough.step.python.createNewNotebook.altText%" }, "completionEvents": [ "onCommand:jupyter.createnewnotebook", @@ -199,11 +199,11 @@ }, { "id": "python.openInteractiveWindow", - "title": "Open the Python Interactive Window", - "description": "The Python Interactive Window is a Python shell where you can execute and view the results of your Python code. You can create cells on a Python file by typing ``#%%``.\n \nTo open the interactive window anytime, open the [Command Palette](command:workbench.action.showCommands) and run the command \n``Jupyter: Create Interactive Window``.\n[Open Interactive Window](command:jupyter.createnewinteractive)", + "title": "%walkthrough.step.python.openInteractiveWindow.title%", + "description": "%walkthrough.step.python.openInteractiveWindow.description%", "media": { "svg": "resources/walkthrough/interactive-window.svg", - "altText": "Opening python interactive window" + "altText": "%walkthrough.step.python.openInteractiveWindow.altText%" }, "completionEvents": [ "onCommand:jupyter.createnewinteractive" @@ -211,11 +211,11 @@ }, { "id": "python.dataScienceLearnMore", - "title": "Find out more!", - "description": "📒 Take a look into the [Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\") features, by looking for \"Jupyter\" in the [Command Palette](command:workbench.action.showCommands). \n 🏃🏻 Find out more features in our [Tutorials](https://aka.ms/AAdjzpd). \n[Learn more](https://aka.ms/AAdar6q)", + "title": "%walkthrough.step.python.dataScienceLearnMore.title%", + "description": "%walkthrough.step.python.dataScienceLearnMore.description%", "media": { "svg": "resources/walkthrough/learnmore.svg", - "altText": "Image representing our documentation page and mailing list resources." + "altText": "%walkthrough.step.python.dataScienceLearnMore.altText%" } } ] diff --git a/package.nls.json b/package.nls.json index 95cb54a1b509..cb777d95c214 100644 --- a/package.nls.json +++ b/package.nls.json @@ -120,5 +120,42 @@ "python.venvFolders.description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).", "python.venvPath.description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", "python.sortImports.args.deprecationMessage": "This setting will be removed soon. Use 'isort.args' instead.", - "python.sortImports.path.deprecationMessage": "This setting will be removed soon. Use 'isort.path' instead." + "python.sortImports.path.deprecationMessage": "This setting will be removed soon. Use 'isort.path' instead.", + "walkthrough.pythonWelcome.title": "Get Started with Python Development", + "walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", + "walkthrough.step.python.createPythonFile.title": "Create a Python file", + "walkthrough.step.python.createPythonFile.description": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", + "walkthrough.step.python.installPythonWin8.title": "Install Python", + "walkthrough.step.python.installPythonWin8.description": "The Python Extension requires Python to be installed. Install Python [from python.org](https://www.python.org/downloads).\n\n[Install Python](https://www.python.org/downloads)\n", + "walkthrough.step.python.installPythonMac.title": "Install Python", + "walkthrough.step.python.installPythonMac.description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via Brew](command:python.installPythonOnMac)\n", + "walkthrough.step.python.installPythonLinux.title": "Install Python", + "walkthrough.step.python.installPythonLinux.description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via terminal](command:python.installPythonOnLinux)\n", + "walkthrough.step.python.selectInterpreter.title": "Select a Python Interpreter", + "walkthrough.step.python.selectInterpreter.description": "Choose which Python interpreter/environment you want to use for your Python project.\n[Select Python Interpreter](command:python.setInterpreter)\n**Tip**: Run the ``Python: Select Interpreter`` command in the [Command Palette](command:workbench.action.showCommands).", + "walkthrough.step.python.createEnvironment.title": "Create a Python Environment ", + "walkthrough.step.python.createEnvironment.description": "Create an environment for your Python project.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).\n 🔍 Check out our [docs](https://aka.ms/pythonenvs) to learn more.", + "walkthrough.step.python.runAndDebug.title": "Run and debug your Python file", + "walkthrough.step.python.runAndDebug.description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", + "walkthrough.step.python.learnMoreWithDS.title": "Explore more resources", + "walkthrough.step.python.learnMoreWithDS.description": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Learn More](https://aka.ms/AA8dqti)", + "walkthrough.pythonDataScienceWelcome.title": "Get Started with Python for Data Science", + "walkthrough.pythonDataScienceWelcome.description": "Your first steps to getting started with a Data Science project with Python!", + "walkthrough.step.python.installJupyterExt.title": "Install Jupyter extension", + "walkthrough.step.python.installJupyterExt.description": "If you haven't already, install the [Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\") to take full advantage of notebooks experiences in VS Code!\n \n[Search Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\")", + "walkthrough.step.python.createNewNotebook.title": "Create or open a Jupyter Notebook", + "walkthrough.step.python.createNewNotebook.description": "Right click in the file explorer and create a new file with an .ipynb extension. Or, open the [Command Palette](command:workbench.action.showCommands) and run the command \n``Jupyter: Create New Blank Notebook``.\n[Create new Jupyter Notebook](command:toSide:jupyter.createnewnotebook)\n If you have an existing project, you can also [open a folder](command:workbench.action.files.openFolder) and/or clone a project from GitHub: [clone a Git repository](command:git.clone).", + "walkthrough.step.python.openInteractiveWindow.title": "Open the Python Interactive Window", + "walkthrough.step.python.openInteractiveWindow.description": "The Python Interactive Window is a Python shell where you can execute and view the results of your Python code. You can create cells on a Python file by typing ``#%%``.\n \nTo open the interactive window anytime, open the [Command Palette](command:workbench.action.showCommands) and run the command \n``Jupyter: Create Interactive Window``.\n[Open Interactive Window](command:jupyter.createnewinteractive)", + "walkthrough.step.python.dataScienceLearnMore.title": "Find out more!", + "walkthrough.step.python.dataScienceLearnMore.description": "📒 Take a look into the [Jupyter extension](command:workbench.extensions.search?\"ms-toolsai.jupyter\") features, by looking for \"Jupyter\" in the [Command Palette](command:workbench.action.showCommands). \n 🏃🏻 Find out more features in our [Tutorials](https://aka.ms/AAdjzpd). \n[Learn more](https://aka.ms/AAdar6q)", + "walkthrough.step.python.createPythonFile.altText": "Open a Python file or a folder with a Python project.", + "walkthrough.step.python.selectInterpreter.altText": "Selecting a Python interpreter from the status bar", + "walkthrough.step.python.createEnvironment.altText": "Creating a Python environment from the Command Palette", + "walkthrough.step.python.runAndDebug.altText": "How to run and debug in VS Code with F5 or the play button on the top right.", + "walkthrough.step.python.learnMoreWithDS.altText": "Image representing our documentation page and mailing list resources.", + "walkthrough.step.python.installJupyterExt.altText": "Creating a new Jupyter notebook", + "walkthrough.step.python.createNewNotebook.altText": "Creating a new Jupyter notebook", + "walkthrough.step.python.openInteractiveWindow.altText": "Opening Python interactive window", + "walkthrough.step.python.dataScienceLearnMore.altText": "Image representing our documentation page and mailing list resources." } From ed06e550226ea1ae2c2c11139b02089e0186bf1e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 17 Apr 2023 10:03:43 -0700 Subject: [PATCH 048/336] Edited Discovery Logic (#20631) closes https://github.com/microsoft/vscode-python/issues/20078 and closes https://github.com/microsoft/vscode-python/issues/20085 (which is about the testing work to support this code) This logic now successfully works to discover the pytest repo tests. This branch takes into account all the previous comments made to the python discovery logic in the previous PR (located on my personal fork). Therefore this is a second round of edits on this code. It now works for the pytest library (discovers all the tests in the pytest library). --------- Co-authored-by: Brett Cannon Co-authored-by: Karthik Nadig --- .vscode/launch.json | 9 + .../nested_folder_two/test_nest.py | 8 + .../nested_folder_one/test_bottom_folder.py | 14 + .../test_top_folder.py | 14 + .../pytestadapter/.data/empty_discovery.py | 7 + .../.data/error_parametrize_discovery.py | 10 + .../.data/error_syntax_discovery.txt | 7 + .../pytestadapter/.data/parametrize_tests.py | 10 + .../pytestadapter/.data/simple_pytest.py | 7 + .../pytestadapter/.data/text_docstring.txt | 4 + .../.data/unittest_folder/test_add.py | 21 + .../.data/unittest_folder/test_subtract.py | 25 + .../.data/unittest_pytest_same_file.py | 17 + pythonFiles/tests/pytestadapter/__init__.py | 2 + .../expected_discovery_test_output.py | 473 ++++++++++++++++++ pythonFiles/tests/pytestadapter/helpers.py | 161 ++++++ .../tests/pytestadapter/test_discovery.py | 112 +++++ pythonFiles/vscode_pytest/__init__.py | 334 +++++++++++++ .../pytest/pytestDiscoveryAdapter.ts | 4 +- .../pytest/pytestExecutionAdapter.ts | 99 ++-- .../unittest/testExecutionAdapter.ts | 6 +- .../pytestDiscoveryAdapter.unit.test.ts | 91 ++++ 22 files changed, 1380 insertions(+), 55 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py create mode 100644 pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py create mode 100644 pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py create mode 100644 pythonFiles/tests/pytestadapter/.data/empty_discovery.py create mode 100644 pythonFiles/tests/pytestadapter/.data/error_parametrize_discovery.py create mode 100644 pythonFiles/tests/pytestadapter/.data/error_syntax_discovery.txt create mode 100644 pythonFiles/tests/pytestadapter/.data/parametrize_tests.py create mode 100644 pythonFiles/tests/pytestadapter/.data/simple_pytest.py create mode 100644 pythonFiles/tests/pytestadapter/.data/text_docstring.txt create mode 100644 pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py create mode 100644 pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py create mode 100644 pythonFiles/tests/pytestadapter/.data/unittest_pytest_same_file.py create mode 100644 pythonFiles/tests/pytestadapter/__init__.py create mode 100644 pythonFiles/tests/pytestadapter/expected_discovery_test_output.py create mode 100644 pythonFiles/tests/pytestadapter/helpers.py create mode 100644 pythonFiles/tests/pytestadapter/test_discovery.py create mode 100644 pythonFiles/vscode_pytest/__init__.py create mode 100644 src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 7b654703dbd8..82981a93305d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -253,6 +253,15 @@ "request": "attach", "listen": { "host": "localhost", "port": 5678 }, "justMyCode": true + }, + { + "name": "Debug pytest plugin tests", + + "type": "python", + "request": "launch", + "module": "pytest", + "args": ["${workspaceFolder}/pythonFiles/tests/pytestadapter"], + "justMyCode": true } ], "compounds": [ diff --git a/pythonFiles/tests/pytestadapter/.data/double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py b/pythonFiles/tests/pytestadapter/.data/double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py new file mode 100644 index 000000000000..9ac9f7017f87 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test's id is double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function. +# This test passes. +def test_function(): # test_marker--test_function + assert 1 == 1 diff --git a/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py b/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py new file mode 100644 index 000000000000..59738aeba37f --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test's id is dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t. +# This test passes. +def test_bottom_function_t(): # test_marker--test_bottom_function_t + assert True + + +# This test's id is dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f. +# This test fails. +def test_bottom_function_f(): # test_marker--test_bottom_function_f + assert False diff --git a/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py b/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py new file mode 100644 index 000000000000..010c54cf4461 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test's id is dual_level_nested_folder/test_top_folder.py::test_top_function_t. +# This test passes. +def test_top_function_t(): # test_marker--test_top_function_t + assert True + + +# This test's id is dual_level_nested_folder/test_top_folder.py::test_top_function_f. +# This test fails. +def test_top_function_f(): # test_marker--test_top_function_f + assert False diff --git a/pythonFiles/tests/pytestadapter/.data/empty_discovery.py b/pythonFiles/tests/pytestadapter/.data/empty_discovery.py new file mode 100644 index 000000000000..5f4ea27aec7f --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/empty_discovery.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This file has no tests in it; the discovery will return an empty list of tests. +def function_function(string): + return string diff --git a/pythonFiles/tests/pytestadapter/.data/error_parametrize_discovery.py b/pythonFiles/tests/pytestadapter/.data/error_parametrize_discovery.py new file mode 100644 index 000000000000..8e48224edf3b --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/error_parametrize_discovery.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +# This test has an error which will appear on pytest discovery. +# This error is intentional and is meant to test pytest discovery error handling. +@pytest.mark.parametrize("actual,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) +def test_function(): + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/error_syntax_discovery.txt b/pythonFiles/tests/pytestadapter/.data/error_syntax_discovery.txt new file mode 100644 index 000000000000..78627fffb351 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/error_syntax_discovery.txt @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# This test has a syntax error. +# This error is intentional and is meant to test pytest discovery error handling. +def test_function() + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py new file mode 100644 index 000000000000..9421e0cc0691 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py @@ -0,0 +1,10 @@ +import pytest + + +# Testing pytest with parametrized tests. The first two pass, the third fails. +# The tests ids are parametrize_tests.py::test_adding[3+5-8] and so on. +@pytest.mark.parametrize( # test_marker--test_adding + "actual, expected", [("3+5", 8), ("2+4", 6), ("6+9", 16)] +) +def test_adding(actual, expected): + assert eval(actual) == expected diff --git a/pythonFiles/tests/pytestadapter/.data/simple_pytest.py b/pythonFiles/tests/pytestadapter/.data/simple_pytest.py new file mode 100644 index 000000000000..9f9bfb014f3d --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/simple_pytest.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test passes. +def test_function(): # test_marker--test_function + assert 1 == 1 diff --git a/pythonFiles/tests/pytestadapter/.data/text_docstring.txt b/pythonFiles/tests/pytestadapter/.data/text_docstring.txt new file mode 100644 index 000000000000..b29132c10b57 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/text_docstring.txt @@ -0,0 +1,4 @@ +This is a doctest test which passes #test_marker--text_docstring.txt +>>> x = 3 +>>> x +3 diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py new file mode 100644 index 000000000000..a96c7f2fa392 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + + +def add(a, b): + return a + b + + +class TestAddFunction(unittest.TestCase): + # This test's id is unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers. + # This test passes. + def test_add_positive_numbers(self): # test_marker--test_add_positive_numbers + result = add(2, 3) + self.assertEqual(result, 5) + + # This test's id is unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers. + # This test passes. + def test_add_negative_numbers(self): # test_marker--test_add_negative_numbers + result = add(-2, -3) + self.assertEqual(result, -5) diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py new file mode 100644 index 000000000000..80087fed0f3c --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + + +def subtract(a, b): + return a - b + + +class TestSubtractFunction(unittest.TestCase): + # This test's id is unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers. + # This test passes. + def test_subtract_positive_numbers( # test_marker--test_subtract_positive_numbers + self, + ): + result = subtract(5, 3) + self.assertEqual(result, 2) + + # This test's id is unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers. + # This test passes. + def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers + self, + ): + result = subtract(-2, -3) + self.assertEqual(result, 1) diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_pytest_same_file.py b/pythonFiles/tests/pytestadapter/.data/unittest_pytest_same_file.py new file mode 100644 index 000000000000..ac66779b9cbe --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/unittest_pytest_same_file.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +class TestExample(unittest.TestCase): + # This test's id is unittest_pytest_same_file.py::TestExample::test_true_unittest. + # Test type is unittest and this test passes. + def test_true_unittest(self): # test_marker--test_true_unittest + assert True + + +# This test's id is unittest_pytest_same_file.py::test_true_pytest. +# Test type is pytest and this test passes. +def test_true_pytest(): # test_marker--test_true_pytest + assert True diff --git a/pythonFiles/tests/pytestadapter/__init__.py b/pythonFiles/tests/pytestadapter/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py new file mode 100644 index 000000000000..e1422a81c979 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -0,0 +1,473 @@ +import os +import pathlib + +from .helpers import TEST_DATA_PATH, find_test_line_number + +# This is the expected output for the empty_discovery.py file. +# └── +TEST_DATA_PATH_STR = os.fspath(TEST_DATA_PATH) +empty_discovery_pytest_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the simple_pytest.py file. +# └── simple_pytest.py +# └── test_function +simple_test_file_path = os.fspath(TEST_DATA_PATH / "simple_pytest.py") +simple_discovery_pytest_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "simple_pytest.py", + "path": simple_test_file_path, + "type_": "file", + "id_": simple_test_file_path, + "children": [ + { + "name": "test_function", + "path": simple_test_file_path, + "lineno": find_test_line_number( + "test_function", + simple_test_file_path, + ), + "type_": "test", + "id_": "simple_pytest.py::test_function", + "runID": "simple_pytest.py::test_function", + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the unittest_pytest_same_file.py file. +# ├── unittest_pytest_same_file.py +# ├── TestExample +# │ └── test_true_unittest +# └── test_true_pytest +unit_pytest_same_file_path = os.fspath(TEST_DATA_PATH / "unittest_pytest_same_file.py") +unit_pytest_same_file_discovery_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "unittest_pytest_same_file.py", + "path": unit_pytest_same_file_path, + "type_": "file", + "id_": unit_pytest_same_file_path, + "children": [ + { + "name": "TestExample", + "path": unit_pytest_same_file_path, + "type_": "class", + "children": [ + { + "name": "test_true_unittest", + "path": unit_pytest_same_file_path, + "lineno": find_test_line_number( + "test_true_unittest", + unit_pytest_same_file_path, + ), + "type_": "test", + "id_": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "runID": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + } + ], + "id_": "unittest_pytest_same_file.py::TestExample", + }, + { + "name": "test_true_pytest", + "path": unit_pytest_same_file_path, + "lineno": find_test_line_number( + "test_true_pytest", + unit_pytest_same_file_path, + ), + "type_": "test", + "id_": "unittest_pytest_same_file.py::test_true_pytest", + "runID": "unittest_pytest_same_file.py::test_true_pytest", + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the unittest_folder tests +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ ├── test_add_negative_numbers +# │ └── test_add_positive_numbers +# └── test_subtract.py +# └── TestSubtractFunction +# ├── test_subtract_negative_numbers +# └── test_subtract_positive_numbers +unittest_folder_path = os.fspath(TEST_DATA_PATH / "unittest_folder") +test_add_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_add.py") +test_subtract_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_subtract.py") +unittest_folder_discovery_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "unittest_folder", + "path": unittest_folder_path, + "type_": "folder", + "id_": unittest_folder_path, + "children": [ + { + "name": "test_add.py", + "path": test_add_path, + "type_": "file", + "id_": test_add_path, + "children": [ + { + "name": "TestAddFunction", + "path": test_add_path, + "type_": "class", + "children": [ + { + "name": "test_add_negative_numbers", + "path": test_add_path, + "lineno": find_test_line_number( + "test_add_negative_numbers", + test_add_path, + ), + "type_": "test", + "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + }, + { + "name": "test_add_positive_numbers", + "path": test_add_path, + "lineno": find_test_line_number( + "test_add_positive_numbers", + test_add_path, + ), + "type_": "test", + "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + }, + ], + "id_": "unittest_folder/test_add.py::TestAddFunction", + } + ], + }, + { + "name": "test_subtract.py", + "path": test_subtract_path, + "type_": "file", + "id_": test_subtract_path, + "children": [ + { + "name": "TestSubtractFunction", + "path": test_subtract_path, + "type_": "class", + "children": [ + { + "name": "test_subtract_negative_numbers", + "path": test_subtract_path, + "lineno": find_test_line_number( + "test_subtract_negative_numbers", + test_subtract_path, + ), + "type_": "test", + "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + }, + { + "name": "test_subtract_positive_numbers", + "path": test_subtract_path, + "lineno": find_test_line_number( + "test_subtract_positive_numbers", + test_subtract_path, + ), + "type_": "test", + "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + }, + ], + "id_": "unittest_folder/test_subtract.py::TestSubtractFunction", + } + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the dual_level_nested_folder tests +# └── dual_level_nested_folder +# └── test_top_folder.py +# └── test_top_function_t +# └── test_top_function_f +# └── nested_folder_one +# └── test_bottom_folder.py +# └── test_bottom_function_t +# └── test_bottom_function_f +dual_level_nested_folder_path = os.fspath(TEST_DATA_PATH / "dual_level_nested_folder") +test_top_folder_path = os.fspath( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +test_nested_folder_one_path = os.fspath( + TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" +) +test_bottom_folder_path = os.fspath( + TEST_DATA_PATH + / "dual_level_nested_folder" + / "nested_folder_one" + / "test_bottom_folder.py" +) + +dual_level_nested_folder_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "dual_level_nested_folder", + "path": dual_level_nested_folder_path, + "type_": "folder", + "id_": dual_level_nested_folder_path, + "children": [ + { + "name": "test_top_folder.py", + "path": test_top_folder_path, + "type_": "file", + "id_": test_top_folder_path, + "children": [ + { + "name": "test_top_function_t", + "path": test_top_folder_path, + "lineno": find_test_line_number( + "test_top_function_t", + test_top_folder_path, + ), + "type_": "test", + "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + }, + { + "name": "test_top_function_f", + "path": test_top_folder_path, + "lineno": find_test_line_number( + "test_top_function_f", + test_top_folder_path, + ), + "type_": "test", + "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + }, + ], + }, + { + "name": "nested_folder_one", + "path": test_nested_folder_one_path, + "type_": "folder", + "id_": test_nested_folder_one_path, + "children": [ + { + "name": "test_bottom_folder.py", + "path": test_bottom_folder_path, + "type_": "file", + "id_": test_bottom_folder_path, + "children": [ + { + "name": "test_bottom_function_t", + "path": test_bottom_folder_path, + "lineno": find_test_line_number( + "test_bottom_function_t", + test_bottom_folder_path, + ), + "type_": "test", + "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + }, + { + "name": "test_bottom_function_f", + "path": test_bottom_folder_path, + "lineno": find_test_line_number( + "test_bottom_function_f", + test_bottom_folder_path, + ), + "type_": "test", + "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + }, + ], + } + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the double_nested_folder tests. +# └── double_nested_folder +# └── nested_folder_one +# └── nested_folder_two +# └── test_nest.py +# └── test_function +double_nested_folder_path = os.fspath(TEST_DATA_PATH / "double_nested_folder") +double_nested_folder_one_path = os.fspath( + TEST_DATA_PATH / "double_nested_folder" / "nested_folder_one" +) +double_nested_folder_two_path = os.fspath( + TEST_DATA_PATH / "double_nested_folder" / "nested_folder_one" / "nested_folder_two" +) +double_nested_test_nest_path = os.fspath( + TEST_DATA_PATH + / "double_nested_folder" + / "nested_folder_one" + / "nested_folder_two" + / "test_nest.py" +) +double_nested_folder_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "double_nested_folder", + "path": double_nested_folder_path, + "type_": "folder", + "id_": double_nested_folder_path, + "children": [ + { + "name": "nested_folder_one", + "path": double_nested_folder_one_path, + "type_": "folder", + "id_": double_nested_folder_one_path, + "children": [ + { + "name": "nested_folder_two", + "path": double_nested_folder_two_path, + "type_": "folder", + "id_": double_nested_folder_two_path, + "children": [ + { + "name": "test_nest.py", + "path": double_nested_test_nest_path, + "type_": "file", + "id_": double_nested_test_nest_path, + "children": [ + { + "name": "test_function", + "path": double_nested_test_nest_path, + "lineno": find_test_line_number( + "test_function", + double_nested_test_nest_path, + ), + "type_": "test", + "id_": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + "runID": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + } + ], + } + ], + } + ], + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the nested_folder tests. +# └── parametrize_tests.py +# └── test_adding[3+5-8] +# └── test_adding[2+4-6] +# └── test_adding[6+9-16] +parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") +parametrize_tests_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "parametrize_tests.py", + "path": parameterize_tests_path, + "type_": "file", + "id_": parameterize_tests_path, + "children": [ + { + "name": "test_adding[3+5-8]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[3+5-8]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[3+5-8]", + "runID": "parametrize_tests.py::test_adding[3+5-8]", + }, + { + "name": "test_adding[2+4-6]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[2+4-6]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[2+4-6]", + "runID": "parametrize_tests.py::test_adding[2+4-6]", + }, + { + "name": "test_adding[6+9-16]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[6+9-16]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[6+9-16]", + "runID": "parametrize_tests.py::test_adding[6+9-16]", + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the text_docstring.txt tests. +# └── text_docstring.txt +text_docstring_path = os.fspath(TEST_DATA_PATH / "text_docstring.txt") +doctest_pytest_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "text_docstring.txt", + "path": text_docstring_path, + "type_": "file", + "id_": text_docstring_path, + "children": [ + { + "name": "text_docstring.txt", + "path": text_docstring_path, + "lineno": find_test_line_number( + "text_docstring.txt", + text_docstring_path, + ), + "type_": "test", + "id_": "text_docstring.txt::text_docstring.txt", + "runID": "text_docstring.txt::text_docstring.txt", + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py new file mode 100644 index 000000000000..8d485456c145 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -0,0 +1,161 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import contextlib +import io +import json +import os +import pathlib +import random +import socket +import subprocess +import sys +import uuid +from typing import Dict, List, Union + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" +from typing_extensions import TypedDict + + +@contextlib.contextmanager +def test_output_file(root: pathlib.Path, ext: str = ".txt"): + """Creates a temporary python file with a random name.""" + basename = ( + "".join(random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(9)) + ext + ) + fullpath = root / basename + try: + fullpath.write_text("", encoding="utf-8") + yield fullpath + finally: + os.unlink(str(fullpath)) + + +def create_server( + host: str = "127.0.0.1", + port: int = 0, + backlog: int = socket.SOMAXCONN, + timeout: int = 1000, +) -> socket.socket: + """Return a local server socket listening on the given port.""" + server: socket.socket = _new_sock() + if port: + # If binding to a specific port, make sure that the user doesn't have + # to wait until the OS times out waiting for socket in order to use + # that port again if the server or the adapter crash or are force-killed. + if sys.platform == "win32": + server.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) + else: + try: + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + except (AttributeError, OSError): + pass # Not available everywhere + server.bind((host, port)) + if timeout: + server.settimeout(timeout) + server.listen(backlog) + return server + + +def _new_sock() -> socket.socket: + sock: socket.socket = socket.socket( + socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP + ) + options = [ + ("SOL_SOCKET", "SO_KEEPALIVE", 1), + ("IPPROTO_TCP", "TCP_KEEPIDLE", 1), + ("IPPROTO_TCP", "TCP_KEEPINTVL", 3), + ("IPPROTO_TCP", "TCP_KEEPCNT", 5), + ] + + for level, name, value in options: + try: + sock.setsockopt(getattr(socket, level), getattr(socket, name), value) + except (AttributeError, OSError): + pass # May not be available everywhere. + + return sock + + +CONTENT_LENGTH: str = "Content-Length:" +Env_Dict = TypedDict( + "Env_Dict", {"TEST_UUID": str, "TEST_PORT": str, "PYTHONPATH": str} +) + + +def process_rpc_json(data: str) -> Dict[str, str]: + """Process the JSON data which comes from the server which runs the pytest discovery.""" + str_stream: io.StringIO = io.StringIO(data) + + length: int = 0 + + while True: + line: str = str_stream.readline() + if CONTENT_LENGTH.lower() in line.lower(): + length = int(line[len(CONTENT_LENGTH) :]) + break + + if not line or line.isspace(): + raise ValueError("Header does not contain Content-Length") + + while True: + line: str = str_stream.readline() + if not line or line.isspace(): + break + + raw_json: str = str_stream.read(length) + return json.loads(raw_json) + + +def runner(args: List[str]) -> Union[Dict[str, str], None]: + """Run the pytest discovery and return the JSON data from the server.""" + process_args: List[str] = [ + sys.executable, + "-m", + "pytest", + "-p", + "vscode_pytest", + ] + args + + with test_output_file(TEST_DATA_PATH) as output_path: + env = os.environ.copy() + env.update( + { + "TEST_UUID": str(uuid.uuid4()), + "TEST_PORT": str(12345), # port is not used for tests + "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), + "TEST_OUTPUT_FILE": os.fspath(output_path), + } + ) + + result = subprocess.run( + process_args, + env=env, + cwd=os.fspath(TEST_DATA_PATH), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if result.returncode != 0: + print("Subprocess Run failed with:") + print(result.stdout.decode(encoding="utf-8")) + print(result.stderr.decode(encoding="utf-8")) + + return process_rpc_json(output_path.read_text(encoding="utf-8")) + + +def find_test_line_number(test_name: str, test_file_path) -> str: + """Function which finds the correct line number for a test by looking for the "test_marker--[test_name]" string. + + The test_name is split on the "[" character to remove the parameterization information. + + Args: + test_name: The name of the test to find the line number for, will be unique per file. + test_file_path: The path to the test file where the test is located. + """ + test_file_unique_id: str = "test_marker--" + test_name.split("[")[0] + with open(test_file_path) as f: + for i, line in enumerate(f): + if test_file_unique_id in line: + return str(i + 1) + error_str: str = f"Test {test_name!r} not found on any line in {test_file_path}" + raise ValueError(error_str) diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py new file mode 100644 index 000000000000..57fa9d624bd6 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -0,0 +1,112 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os +import shutil +import signal + +import pytest + +from . import expected_discovery_test_output +from .helpers import TEST_DATA_PATH, runner + + +def test_syntax_error(tmp_path): + """Test pytest discovery on a file that has a syntax error. + + Copies the contents of a .txt file to a .py file in the temporary directory + to then run pytest discovery on. + + The json should still be returned but the errors list should be present. + + Keyword arguments: + tmp_path -- pytest fixture that creates a temporary directory. + """ + # Saving some files as .txt to avoid that file displaying a syntax error for + # the extension as a whole. Instead, rename it before running this test + # in order to test the error handling. + file_path = TEST_DATA_PATH / "error_syntax_discovery.txt" + temp_dir = tmp_path / "temp_data" + temp_dir.mkdir() + p = temp_dir / "error_syntax_discovery.py" + shutil.copyfile(file_path, p) + actual = runner(["--collect-only", os.fspath(p)]) + assert actual + assert all(item in actual for item in ("status", "cwd", "errors")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["errors"]) == 2 + + +def test_parameterized_error_collect(): + """Tests pytest discovery on specific file that incorrectly uses parametrize. + + The json should still be returned but the errors list should be present. + """ + file_path_str = "error_parametrize_discovery.py" + actual = runner(["--collect-only", file_path_str]) + assert actual + assert all(item in actual for item in ("status", "cwd", "errors")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["errors"]) == 2 + + +@pytest.mark.parametrize( + "file, expected_const", + [ + ( + "parametrize_tests.py", + expected_discovery_test_output.parametrize_tests_expected_output, + ), + ( + "empty_discovery.py", + expected_discovery_test_output.empty_discovery_pytest_expected_output, + ), + ( + "simple_pytest.py", + expected_discovery_test_output.simple_discovery_pytest_expected_output, + ), + ( + "unittest_pytest_same_file.py", + expected_discovery_test_output.unit_pytest_same_file_discovery_expected_output, + ), + ( + "unittest_folder", + expected_discovery_test_output.unittest_folder_discovery_expected_output, + ), + ( + "dual_level_nested_folder", + expected_discovery_test_output.dual_level_nested_folder_expected_output, + ), + ( + "double_nested_folder", + expected_discovery_test_output.double_nested_folder_expected_output, + ), + ( + "text_docstring.txt", + expected_discovery_test_output.doctest_pytest_expected_output, + ), + ], +) +def test_pytest_collect(file, expected_const): + """ + Test to test pytest discovery on a variety of test files/ folder structures. + Uses variables from expected_discovery_test_output.py to store the expected dictionary return. + Only handles discovery and therefore already contains the arg --collect-only. + All test discovery will succeed, be in the correct cwd, and match expected test output. + + Keyword arguments: + file -- a string with the file or folder to run pytest discovery on. + expected_const -- the expected output from running pytest discovery on the file. + """ + actual = runner( + [ + "--collect-only", + os.fspath(TEST_DATA_PATH / file), + ] + ) + assert actual + assert all(item in actual for item in ("status", "cwd", "tests")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert actual["tests"] == expected_const diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py new file mode 100644 index 000000000000..22acaab57953 --- /dev/null +++ b/pythonFiles/vscode_pytest/__init__.py @@ -0,0 +1,334 @@ +import json +import os +import pathlib +import sys +import traceback + +import pytest + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + +from typing import Any, Dict, List, Optional, Union + +from testing_tools import socket_manager +from typing_extensions import Literal, TypedDict + + +class TestData(TypedDict): + """A general class that all test objects inherit from.""" + + name: str + path: str + type_: Literal["class", "file", "folder", "test", "error"] + id_: str + + +class TestItem(TestData): + """A class defining test items.""" + + lineno: str + runID: str + + +class TestNode(TestData): + """A general class that handles all test data which contains children.""" + + children: "list[Union[TestNode, TestItem, None]]" + + +class VSCodePytestError(Exception): + """A custom exception class for pytest errors.""" + + def __init__(self, message): + super().__init__(message) + + +ERRORS = [] + + +def pytest_internalerror(excrepr, excinfo): + """A pytest hook that is called when an internal error occurs. + + Keyword arguments: + excrepr -- the exception representation. + excinfo -- the exception information of type ExceptionInfo. + """ + # call.excinfo.exconly() returns the exception as a string. + ERRORS.append(excinfo.exconly()) + + +def pytest_exception_interact(node, call, report): + """A pytest hook that is called when an exception is raised which could be handled. + + Keyword arguments: + node -- the node that raised the exception. + call -- the call object. + report -- the report object of either type CollectReport or TestReport. + """ + # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. + # call.excinfo.exconly() returns the exception as a string. + ERRORS.append(call.excinfo.exconly()) + + +def pytest_keyboard_interrupt(excinfo): + """A pytest hook that is called when a keyboard interrupt is raised. + + Keyword arguments: + excinfo -- the exception information of type ExceptionInfo. + """ + # The function execonly() returns the exception as a string. + ERRORS.append(excinfo.exconly()) + + +def pytest_sessionfinish(session, exitstatus): + """A pytest hook that is called after pytest has fulled finished. + + Keyword arguments: + session -- the pytest session object. + exitstatus -- the status code of the session. + """ + cwd = pathlib.Path.cwd() + try: + session_node: Union[TestNode, None] = build_test_tree(session) + if not session_node: + raise VSCodePytestError( + "Something went wrong following pytest finish, \ + no session node was created" + ) + post_response(os.fsdecode(cwd), session_node) + except Exception as e: + ERRORS.append( + f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" + ) + errorNode: TestNode = { + "name": "", + "path": "", + "type_": "error", + "children": [], + "id_": "", + } + post_response(os.fsdecode(cwd), errorNode) + + +def build_test_tree(session: pytest.Session) -> TestNode: + """Builds a tree made up of testing nodes from the pytest session. + + Keyword arguments: + session -- the pytest session object. + """ + session_node = create_session_node(session) + session_children_dict: Dict[str, TestNode] = {} + file_nodes_dict: Dict[Any, TestNode] = {} + class_nodes_dict: Dict[str, TestNode] = {} + + for test_case in session.items: + test_node = create_test_node(test_case) + if isinstance(test_case.parent, pytest.Class): + try: + test_class_node = class_nodes_dict[test_case.parent.name] + except KeyError: + test_class_node = create_class_node(test_case.parent) + class_nodes_dict[test_case.parent.name] = test_class_node + test_class_node["children"].append(test_node) + if test_case.parent.parent: + parent_module = test_case.parent.parent + else: + ERRORS.append(f"Test class {test_case.parent} has no parent") + break + # Create a file node that has the class as a child. + try: + test_file_node: TestNode = file_nodes_dict[parent_module] + except KeyError: + test_file_node = create_file_node(parent_module) + file_nodes_dict[parent_module] = test_file_node + # Check if the class is already a child of the file node. + if test_class_node not in test_file_node["children"]: + test_file_node["children"].append(test_class_node) + else: # This includes test cases that are pytest functions or a doctests. + try: + parent_test_case = file_nodes_dict[test_case.parent] + except KeyError: + parent_test_case = create_file_node(test_case.parent) + file_nodes_dict[test_case.parent] = parent_test_case + parent_test_case["children"].append(test_node) + created_files_folders_dict: Dict[str, TestNode] = {} + for file_module, file_node in file_nodes_dict.items(): + # Iterate through all the files that exist and construct them into nested folders. + root_folder_node: TestNode = build_nested_folders( + file_module, file_node, created_files_folders_dict, session + ) + # The final folder we get to is the highest folder in the path + # and therefore we add this as a child to the session. + root_id = root_folder_node.get("id_") + if root_id and root_id not in session_children_dict: + session_children_dict[root_id] = root_folder_node + session_node["children"] = list(session_children_dict.values()) + return session_node + + +def build_nested_folders( + file_module: Any, + file_node: TestNode, + created_files_folders_dict: Dict[str, TestNode], + session: pytest.Session, +) -> TestNode: + """Takes a file or folder and builds the nested folder structure for it. + + Keyword arguments: + file_module -- the created module for the file we are nesting. + file_node -- the file node that we are building the nested folders for. + created_files_folders_dict -- Dictionary of all the folders and files that have been created. + session -- the pytest session object. + """ + prev_folder_node = file_node + + # Begin the iterator_path one level above the current file. + iterator_path = file_module.path.parent + while iterator_path != session.path: + curr_folder_name = iterator_path.name + try: + curr_folder_node: TestNode = created_files_folders_dict[curr_folder_name] + except KeyError: + curr_folder_node: TestNode = create_folder_node( + curr_folder_name, iterator_path + ) + created_files_folders_dict[curr_folder_name] = curr_folder_node + if prev_folder_node not in curr_folder_node["children"]: + curr_folder_node["children"].append(prev_folder_node) + iterator_path = iterator_path.parent + prev_folder_node = curr_folder_node + return prev_folder_node + + +def create_test_node( + test_case: pytest.Item, +) -> TestItem: + """Creates a test node from a pytest test case. + + Keyword arguments: + test_case -- the pytest test case. + """ + test_case_loc: str = ( + str(test_case.location[1] + 1) if (test_case.location[1] is not None) else "" + ) + return { + "name": test_case.name, + "path": os.fspath(test_case.path), + "lineno": test_case_loc, + "type_": "test", + "id_": test_case.nodeid, + "runID": test_case.nodeid, + } + + +def create_session_node(session: pytest.Session) -> TestNode: + """Creates a session node from a pytest session. + + Keyword arguments: + session -- the pytest session. + """ + return { + "name": session.name, + "path": os.fspath(session.path), + "type_": "folder", + "children": [], + "id_": os.fspath(session.path), + } + + +def create_class_node(class_module: pytest.Class) -> TestNode: + """Creates a class node from a pytest class object. + + Keyword arguments: + class_module -- the pytest object representing a class module. + """ + return { + "name": class_module.name, + "path": os.fspath(class_module.path), + "type_": "class", + "children": [], + "id_": class_module.nodeid, + } + + +def create_file_node(file_module: Any) -> TestNode: + """Creates a file node from a pytest file module. + + Keyword arguments: + file_module -- the pytest file module. + """ + return { + "name": file_module.path.name, + "path": os.fspath(file_module.path), + "type_": "file", + "id_": os.fspath(file_module.path), + "children": [], + } + + +def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode: + """Creates a folder node from a pytest folder name and its path. + + Keyword arguments: + folderName -- the name of the folder. + path_iterator -- the path of the folder. + """ + return { + "name": folderName, + "path": os.fspath(path_iterator), + "type_": "folder", + "id_": os.fspath(path_iterator), + "children": [], + } + + +class PayloadDict(TypedDict): + """A dictionary that is used to send a post request to the server.""" + + cwd: str + status: Literal["success", "error"] + tests: Optional[TestNode] + errors: Optional[List[str]] + + +def post_response(cwd: str, session_node: TestNode) -> None: + """Sends a post request to the server. + + Keyword arguments: + cwd -- the current working directory. + session_node -- the session node, which is the top of the testing tree. + errors -- a list of errors that occurred during test collection. + """ + payload: PayloadDict = { + "cwd": cwd, + "status": "success" if not ERRORS else "error", + "tests": session_node, + "errors": [], + } + if ERRORS: + payload["errors"] = ERRORS + + testPort: Union[str, int] = os.getenv("TEST_PORT", 45454) + testuuid: Union[str, None] = os.getenv("TEST_UUID") + addr = "localhost", int(testPort) + data = json.dumps(payload) + request = f"""Content-Length: {len(data)} +Content-Type: application/json +Request-uuid: {testuuid} + +{data}""" + test_output_file: Optional[str] = os.getenv("TEST_OUTPUT_FILE", None) + if test_output_file == "stdout": + print(request) + elif test_output_file: + pathlib.Path(test_output_file).write_text(request, encoding="utf-8") + else: + try: + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) + except Exception as e: + print(f"Plugin error connection error[vscode-pytest]: {e}") + print(f"[vscode-pytest] data: {request}") diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 2787ea180bdb..379f1ea9d12c 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -45,16 +45,15 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // const { pytestArgs } = settings.testing; // traceVerbose(pytestArgs); - // this.cwd = uri.fsPath; // return this.runPytestDiscovery(uri, executionFactory); // } async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { const deferred = createDeferred(); - this.deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); const uuid = this.testServer.createUUID(uri.fsPath); + this.promiseMap.set(uuid, deferred); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; @@ -86,7 +85,6 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } catch (ex) { console.error(ex); } - return deferred.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 25b7d92b767b..70aa2698c0d1 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -34,60 +34,61 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { this.deferred = createDeferred(); return this.deferred.promise; } +} - // public async runTests( - // uri: Uri, - // testIds: string[], - // debugBool?: boolean, - // executionFactory?: IPythonExecutionFactory, - // ): Promise { - // const deferred = createDeferred(); - // const relativePathToPytest = 'pythonFiles'; - // const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - // this.configSettings.isTestExecution(); - // const uuid = this.testServer.createUUID(uri.fsPath); - // this.promiseMap.set(uuid, deferred); - // const settings = this.configSettings.getSettings(uri); - // const { pytestArgs } = settings.testing; +// public async runTests( +// uri: Uri, +// testIds: string[], +// debugBool?: boolean, +// executionFactory?: IPythonExecutionFactory, +// ): Promise { +// const deferred = createDeferred(); +// const relativePathToPytest = 'pythonFiles'; +// const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); +// this.configSettings.isTestExecution(); +// const uuid = this.testServer.createUUID(uri.fsPath); +// this.promiseMap.set(uuid, deferred); +// const settings = this.configSettings.getSettings(uri); +// const { pytestArgs } = settings.testing; - // const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; - // const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); +// const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; +// const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - // const spawnOptions: SpawnOptions = { - // cwd: uri.fsPath, - // throwOnStdErr: true, - // extraVariables: { - // PYTHONPATH: pythonPathCommand, - // TEST_UUID: uuid.toString(), - // TEST_PORT: this.testServer.getPort().toString(), - // }, - // }; +// const spawnOptions: SpawnOptions = { +// cwd: uri.fsPath, +// throwOnStdErr: true, +// extraVariables: { +// PYTHONPATH: pythonPathCommand, +// TEST_UUID: uuid.toString(), +// TEST_PORT: this.testServer.getPort().toString(), +// }, +// }; - // // Create the Python environment in which to execute the command. - // const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { - // allowEnvironmentFetchExceptions: false, - // resource: uri, - // }; - // // need to check what will happen in the exec service is NOT defined and is null - // const execService = await executionFactory?.createActivatedEnvironment(creationOptions); +// // Create the Python environment in which to execute the command. +// const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { +// allowEnvironmentFetchExceptions: false, +// resource: uri, +// }; +// // need to check what will happen in the exec service is NOT defined and is null +// const execService = await executionFactory?.createActivatedEnvironment(creationOptions); - // const testIdsString = testIds.join(' '); - // console.debug('what to do with debug bool?', debugBool); - // try { - // execService?.exec(['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), spawnOptions); - // } catch (ex) { - // console.error(ex); - // } +// const testIdsString = testIds.join(' '); +// console.debug('what to do with debug bool?', debugBool); +// try { +// execService?.exec(['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), spawnOptions); +// } catch (ex) { +// console.error(ex); +// } - // return deferred.promise; - // } - // } +// return deferred.promise; +// } +// } - // function buildExecutionCommand(args: string[]): TestExecutionCommand { - // const executionScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); +// function buildExecutionCommand(args: string[]): TestExecutionCommand { +// const executionScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); - // return { - // script: executionScript, - // args: ['--udiscovery', ...args], - // }; -} +// return { +// script: executionScript, +// args: ['--udiscovery', ...args], +// }; +// } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 98ba8bfeb937..0bca2778ef75 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -37,7 +37,6 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { } public async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { - const deferred = createDeferred(); const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -54,10 +53,11 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { testIds, }; + const deferred = createDeferred(); this.promiseMap.set(uuid, deferred); - // send test command to server - // server fire onDataReceived event once it gets response + // Send test command to server. + // Server fire onDataReceived event once it gets response. this.testServer.sendCommand(options); return deferred.promise; diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts new file mode 100644 index 000000000000..f5a2203dbd9a --- /dev/null +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -0,0 +1,91 @@ +// /* eslint-disable @typescript-eslint/no-explicit-any */ +// // Copyright (c) Microsoft Corporation. All rights reserved. +// // Licensed under the MIT License. +// import * as assert from 'assert'; +// import { Uri } from 'vscode'; +// import * as typeMoq from 'typemoq'; +// import { IConfigurationService } from '../../../../client/common/types'; +// import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +// import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; +// import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; +// import { createDeferred, Deferred } from '../../../../client/common/utils/async'; + +// suite('pytest test discovery adapter', () => { +// let testServer: typeMoq.IMock; +// let configService: IConfigurationService; +// let execFactory = typeMoq.Mock.ofType(); +// let adapter: PytestTestDiscoveryAdapter; +// let execService: typeMoq.IMock; +// let deferred: Deferred; +// setup(() => { +// testServer = typeMoq.Mock.ofType(); +// testServer.setup((t) => t.getPort()).returns(() => 12345); +// testServer +// .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) +// .returns(() => ({ +// dispose: () => { +// /* no-body */ +// }, +// })); +// configService = ({ +// getSettings: () => ({ +// testing: { pytestArgs: ['.'] }, +// }), +// } as unknown) as IConfigurationService; +// execFactory = typeMoq.Mock.ofType(); +// execService = typeMoq.Mock.ofType(); +// execFactory +// .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) +// .returns(() => Promise.resolve(execService.object)); +// deferred = createDeferred(); +// execService +// .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) +// .returns(() => { +// deferred.resolve(); +// return Promise.resolve({ stdout: '{}' }); +// }); +// execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); +// execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); +// }); +// test('onDataReceivedHandler should parse only if known UUID', async () => { +// const uri = Uri.file('/my/test/path/'); +// const uuid = 'uuid123'; +// const data = { status: 'success' }; +// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); +// const eventData: DataReceivedEvent = { +// uuid, +// data: JSON.stringify(data), +// }; + +// adapter = new PytestTestDiscoveryAdapter(testServer.object, configService); +// // ** const promise = adapter.discoverTests(uri, execFactory.object); +// const promise = adapter.discoverTests(uri); +// await deferred.promise; +// adapter.onDataReceivedHandler(eventData); +// const result = await promise; +// assert.deepStrictEqual(result, data); +// }); +// test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { +// const uri = Uri.file('/my/test/path/'); +// const uuid = 'uuid456'; +// let data = { status: 'error' }; +// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); +// const wrongUriEventData: DataReceivedEvent = { +// uuid: 'incorrect-uuid456', +// data: JSON.stringify(data), +// }; +// adapter = new PytestTestDiscoveryAdapter(testServer.object, configService); +// // ** const promise = adapter.discoverTests(uri, execFactory.object); +// const promise = adapter.discoverTests(uri); +// adapter.onDataReceivedHandler(wrongUriEventData); + +// data = { status: 'success' }; +// const correctUriEventData: DataReceivedEvent = { +// uuid, +// data: JSON.stringify(data), +// }; +// adapter.onDataReceivedHandler(correctUriEventData); +// const result = await promise; +// assert.deepStrictEqual(result, data); +// }); +// }); From ab1eb28dde73e20372777211fb845f026693a140 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 17 Apr 2023 11:41:41 -0700 Subject: [PATCH 049/336] Attempt to fix pre-release build (#21071) ![image](https://user-images.githubusercontent.com/13199757/232574996-0b772fb3-59cf-40f1-a9b7-8e356dc83a81.png) --- build/webpack/webpack.extension.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/webpack/webpack.extension.config.js b/build/webpack/webpack.extension.config.js index b1b3922126d6..f496aa32ee26 100644 --- a/build/webpack/webpack.extension.config.js +++ b/build/webpack/webpack.extension.config.js @@ -61,6 +61,9 @@ const config = { // See: https://github.com/microsoft/vscode-extension-telemetry/issues/41#issuecomment-598852991 'applicationinsights-native-metrics', '@opentelemetry/tracing', + '@azure/opentelemetry-instrumentation-azure-sdk', + '@opentelemetry/instrumentation', + '@azure/functions-core', ], plugins: [...common.getDefaultPlugins('extension')], resolve: { From 48952a3c379ea7a67f84528907e8b2d31be60148 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 17 Apr 2023 15:05:21 -0700 Subject: [PATCH 050/336] Use new logging API for python extension logger and LS logger (#21062) In this PR: 1. Changes the python extension logging to use LogOutputChannel 2. Changes the language server logger with LogOutputChannel 3. Test output channel uses OutputChannel as it needs to show test output and not really logging. Also, using logging test output makes it pretty much unreadable. 4. Simplifies logging channel and output channel registration. We need to do this now to make it easier for new test work to integrate with output logging. For #20844 This still doesn't get rid of the log level setting. --- .../activation/common/analysisOptions.ts | 4 +- src/client/activation/common/outputChannel.ts | 6 +-- src/client/activation/types.ts | 6 +-- .../common/application/applicationShell.ts | 6 +-- src/client/common/application/types.ts | 4 +- src/client/common/constants.ts | 2 - .../common/installer/moduleInstaller.ts | 5 +- src/client/common/process/rawProcessApis.ts | 6 ++- src/client/common/process/types.ts | 3 +- src/client/common/types.ts | 9 ++-- src/client/extensionActivation.ts | 15 ++---- src/client/extensionInit.ts | 15 +++--- src/client/linters/errorHandlers/standard.ts | 5 +- src/client/logging/outputChannelLogger.ts | 17 +++--- src/client/testing/constants.ts | 4 -- .../testing/testController/common/server.ts | 1 + .../testing/testController/controller.ts | 27 ++++++++-- .../pytest/pytestDiscoveryAdapter.ts | 9 +++- .../pytest/pytestExecutionAdapter.ts | 11 +++- .../testing/testController/pytest/runner.ts | 7 ++- .../testing/testController/unittest/runner.ts | 7 ++- .../unittest/testDiscoveryAdapter.ts | 9 +++- .../unittest/testExecutionAdapter.ts | 9 +++- .../node/analysisOptions.unit.test.ts | 6 +-- .../activation/outputChannel.unit.test.ts | 6 +-- .../installer/moduleInstaller.unit.test.ts | 9 ++-- src/test/format/formatter.unit.test.ts | 9 ++-- src/test/linters/common.ts | 8 +-- src/test/linters/lintengine.test.ts | 12 ++--- src/test/mockClasses.ts | 22 +++++++- src/test/mocks/vsc/index.ts | 32 +++++++++++ src/test/serviceRegistry.ts | 15 ++---- .../testConfigurationManager.unit.test.ts | 5 +- src/test/testing/configuration.unit.test.ts | 11 ++-- .../testing/configurationFactory.unit.test.ts | 5 +- .../testDiscoveryAdapter.unit.test.ts | 12 +++-- .../testExecutionAdapter.unit.test.ts | 12 +++-- .../workspaceTestAdapter.unit.test.ts | 53 +++++++++++++++---- src/test/vscode-mock.ts | 1 + 39 files changed, 259 insertions(+), 146 deletions(-) delete mode 100644 src/client/testing/constants.ts diff --git a/src/client/activation/common/analysisOptions.ts b/src/client/activation/common/analysisOptions.ts index 18de19384fbf..75d0aabef9d2 100644 --- a/src/client/activation/common/analysisOptions.ts +++ b/src/client/activation/common/analysisOptions.ts @@ -5,7 +5,7 @@ import { DocumentFilter, LanguageClientOptions, RevealOutputChannelOn } from 'vs import { IWorkspaceService } from '../../common/application/types'; import { PYTHON, PYTHON_LANGUAGE } from '../../common/constants'; -import { IOutputChannel, Resource } from '../../common/types'; +import { ILogOutputChannel, Resource } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; import { traceDecoratorError } from '../../logging'; @@ -14,7 +14,7 @@ import { ILanguageServerAnalysisOptions, ILanguageServerOutputChannel } from '.. export abstract class LanguageServerAnalysisOptionsBase implements ILanguageServerAnalysisOptions { protected readonly didChange = new EventEmitter(); - private readonly output: IOutputChannel; + private readonly output: ILogOutputChannel; protected constructor( lsOutputChannel: ILanguageServerOutputChannel, diff --git a/src/client/activation/common/outputChannel.ts b/src/client/activation/common/outputChannel.ts index 830bfbfdf55b..60a99687793e 100644 --- a/src/client/activation/common/outputChannel.ts +++ b/src/client/activation/common/outputChannel.ts @@ -6,13 +6,13 @@ import { inject, injectable } from 'inversify'; import { IApplicationShell, ICommandManager } from '../../common/application/types'; import '../../common/extensions'; -import { IDisposableRegistry, IOutputChannel } from '../../common/types'; +import { IDisposableRegistry, ILogOutputChannel } from '../../common/types'; import { OutputChannelNames } from '../../common/utils/localize'; import { ILanguageServerOutputChannel } from '../types'; @injectable() export class LanguageServerOutputChannel implements ILanguageServerOutputChannel { - public output: IOutputChannel | undefined; + public output: ILogOutputChannel | undefined; private registered = false; @@ -22,7 +22,7 @@ export class LanguageServerOutputChannel implements ILanguageServerOutputChannel @inject(IDisposableRegistry) private readonly disposable: IDisposableRegistry, ) {} - public get channel(): IOutputChannel { + public get channel(): ILogOutputChannel { if (!this.output) { this.output = this.appShell.createOutputChannel(OutputChannelNames.languageServer); this.disposable.push(this.output); diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 873d608f0bd0..2a177bb570b8 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -5,7 +5,7 @@ import { Event } from 'vscode'; import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; -import type { IDisposable, IOutputChannel, Resource } from '../common/types'; +import type { IDisposable, ILogOutputChannel, Resource } from '../common/types'; import { PythonEnvironment } from '../pythonEnvironments/info'; export const IExtensionActivationManager = Symbol('IExtensionActivationManager'); @@ -110,10 +110,10 @@ export interface ILanguageServerOutputChannel { /** * Creates output channel if necessary and returns it * - * @type {IOutputChannel} + * @type {ILogOutputChannel} * @memberof ILanguageServerOutputChannel */ - readonly channel: IOutputChannel; + readonly channel: ILogOutputChannel; } export const IExtensionSingleActivationService = Symbol('IExtensionSingleActivationService'); diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index c1a5de51b7f6..454662472010 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -14,10 +14,10 @@ import { InputBoxOptions, languages, LanguageStatusItem, + LogOutputChannel, MessageItem, MessageOptions, OpenDialogOptions, - OutputChannel, Progress, ProgressOptions, QuickPick, @@ -166,8 +166,8 @@ export class ApplicationShell implements IApplicationShell { public createTreeView(viewId: string, options: TreeViewOptions): TreeView { return window.createTreeView(viewId, options); } - public createOutputChannel(name: string): OutputChannel { - return window.createOutputChannel(name); + public createOutputChannel(name: string): LogOutputChannel { + return window.createOutputChannel(name, { log: true }); } public createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem { return languages.createLanguageStatusItem(id, selector); diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 1b054eda687c..77d7b5af3279 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -25,10 +25,10 @@ import { InputBox, InputBoxOptions, LanguageStatusItem, + LogOutputChannel, MessageItem, MessageOptions, OpenDialogOptions, - OutputChannel, Progress, ProgressOptions, QuickPick, @@ -429,7 +429,7 @@ export interface IApplicationShell { * * @param name Human-readable string which will be used to represent the channel in the UI. */ - createOutputChannel(name: string): OutputChannel; + createOutputChannel(name: string): LogOutputChannel; createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem; } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index da44a7bfe677..f53923b3e1b4 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -93,8 +93,6 @@ export namespace ThemeIcons { export const DEFAULT_INTERPRETER_SETTING = 'python'; -export const STANDARD_OUTPUT_CHANNEL = 'STANDARD_OUTPUT_CHANNEL'; - export const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; export function isTestExecution(): boolean { diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 4049edb8ec0d..62160b7e25c9 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -12,12 +12,11 @@ import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IApplicationShell } from '../application/types'; import { wrapCancellationTokens } from '../cancellation'; -import { STANDARD_OUTPUT_CHANNEL } from '../constants'; import { IFileSystem } from '../platform/types'; import * as internalPython from '../process/internal/python'; import { IProcessServiceFactory } from '../process/types'; import { ITerminalServiceFactory, TerminalCreationOptions } from '../terminal/types'; -import { ExecutionInfo, IConfigurationService, IOutputChannel, Product } from '../types'; +import { ExecutionInfo, IConfigurationService, ILogOutputChannel, Product } from '../types'; import { isResource } from '../utils/misc'; import { ProductNames } from './productNames'; import { IModuleInstaller, InstallOptions, InterpreterUri, ModuleInstallFlags } from './types'; @@ -152,7 +151,7 @@ export abstract class ModuleInstaller implements IModuleInstaller { const options = { name: 'VS Code Python', }; - const outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + const outputChannel = this.serviceContainer.get(ILogOutputChannel); const command = `"${execPath.replace(/\\/g, '/')}" ${args.join(' ')}`; traceLog(`[Elevated] ${command}`); diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts index 59b5fe69c9cd..fe54b3a8be87 100644 --- a/src/client/common/process/rawProcessApis.ts +++ b/src/client/common/process/rawProcessApis.ts @@ -121,7 +121,10 @@ export function plainExec( } const stdoutBuffers: Buffer[] = []; - on(proc.stdout, 'data', (data: Buffer) => stdoutBuffers.push(data)); + on(proc.stdout, 'data', (data: Buffer) => { + stdoutBuffers.push(data); + options.outputChannel?.append(data.toString()); + }); const stderrBuffers: Buffer[] = []; on(proc.stderr, 'data', (data: Buffer) => { if (options.mergeStdOutErr) { @@ -130,6 +133,7 @@ export function plainExec( } else { stderrBuffers.push(data); } + options.outputChannel?.append(data.toString()); }); proc.once('close', () => { diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index bcab76e66b09..8298957285e8 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -3,7 +3,7 @@ import { ChildProcess, ExecOptions, SpawnOptions as ChildProcessSpawnOptions } from 'child_process'; import { Observable } from 'rxjs/Observable'; -import { CancellationToken, Uri } from 'vscode'; +import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonExecInfo } from '../../pythonEnvironments/exec'; import { InterpreterInformation, PythonEnvironment } from '../../pythonEnvironments/info'; import { ExecutionInfo, IDisposable } from '../types'; @@ -24,6 +24,7 @@ export type SpawnOptions = ChildProcessSpawnOptions & { mergeStdOutErr?: boolean; throwOnStdErr?: boolean; extraVariables?: NodeJS.ProcessEnv; + outputChannel?: OutputChannel; }; export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 3fac5e7e0044..3359854f89b7 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -15,9 +15,10 @@ import { Extension, ExtensionContext, Memento, - OutputChannel, + LogOutputChannel, Uri, WorkspaceEdit, + OutputChannel, } from 'vscode'; import { LanguageServerType } from '../activation/types'; import type { InstallOptions, InterpreterUri, ModuleInstallFlags } from './installer/types'; @@ -29,8 +30,10 @@ export interface IDisposable { dispose(): void | undefined | Promise; } -export const IOutputChannel = Symbol('IOutputChannel'); -export interface IOutputChannel extends OutputChannel {} +export const ILogOutputChannel = Symbol('ILogOutputChannel'); +export interface ILogOutputChannel extends LogOutputChannel {} +export const ITestOutputChannel = Symbol('ITestOutputChannel'); +export interface ITestOutputChannel extends OutputChannel {} export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); export interface IDocumentSymbolProvider extends DocumentSymbolProvider {} export const IsWindows = Symbol('IS_WINDOWS'); diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 4b2a41105d77..a93ae562c97d 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -3,21 +3,14 @@ 'use strict'; -import { - debug, - DebugConfigurationProvider, - DebugConfigurationProviderTriggerKind, - languages, - OutputChannel, - window, -} from 'vscode'; +import { debug, DebugConfigurationProvider, DebugConfigurationProviderTriggerKind, languages, window } from 'vscode'; import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; import { IExtensionActivationManager } from './activation/types'; import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; import { IApplicationDiagnostics } from './application/types'; import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from './common/application/types'; -import { Commands, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL, UseProposedApi } from './common/constants'; +import { Commands, PYTHON, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; import { IFileSystem } from './common/platform/types'; import { @@ -25,7 +18,7 @@ import { IDisposableRegistry, IExtensions, IInterpreterPathService, - IOutputChannel, + ILogOutputChannel, IPathUtils, } from './common/types'; import { noop } from './common/utils/misc'; @@ -173,7 +166,7 @@ async function activateLegacy(ext: ExtensionState): Promise { const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); dispatcher.registerEventHandlers(); - const outputChannel = serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + const outputChannel = serviceManager.get(ILogOutputChannel); disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); diff --git a/src/client/extensionInit.ts b/src/client/extensionInit.ts index 4ee5f099f15d..851bc943cb8d 100644 --- a/src/client/extensionInit.ts +++ b/src/client/extensionInit.ts @@ -4,9 +4,8 @@ 'use strict'; import { Container } from 'inversify'; -import { Disposable, Memento, OutputChannel, window } from 'vscode'; +import { Disposable, Memento, window } from 'vscode'; import { instance, mock } from 'ts-mockito'; -import { STANDARD_OUTPUT_CHANNEL } from './common/constants'; import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; @@ -16,7 +15,8 @@ import { IDisposableRegistry, IExtensionContext, IMemento, - IOutputChannel, + ILogOutputChannel, + ITestOutputChannel, WORKSPACE_MEMENTO, } from './common/types'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; @@ -26,7 +26,6 @@ import { ServiceContainer } from './ioc/container'; import { ServiceManager } from './ioc/serviceManager'; import { IServiceContainer, IServiceManager } from './ioc/types'; import * as pythonEnvironments from './pythonEnvironments'; -import { TEST_OUTPUT_CHANNEL } from './testing/constants'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { registerLogger } from './logging'; import { OutputChannelLogger } from './logging/outputChannelLogger'; @@ -54,7 +53,7 @@ export function initializeGlobals( serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); serviceManager.addSingletonInstance(IExtensionContext, context); - const standardOutputChannel = window.createOutputChannel(OutputChannelNames.python); + const standardOutputChannel = window.createOutputChannel(OutputChannelNames.python, { log: true }); disposables.push(standardOutputChannel); disposables.push(registerLogger(new OutputChannelLogger(standardOutputChannel))); @@ -62,12 +61,12 @@ export function initializeGlobals( const unitTestOutChannel = workspaceService.isVirtualWorkspace || !workspaceService.isTrusted ? // Do not create any test related output UI when using virtual workspaces. - instance(mock()) + instance(mock()) : window.createOutputChannel(OutputChannelNames.pythonTest); disposables.push(unitTestOutChannel); - serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); - serviceManager.addSingletonInstance(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); + serviceManager.addSingletonInstance(ILogOutputChannel, standardOutputChannel); + serviceManager.addSingletonInstance(ITestOutputChannel, unitTestOutChannel); return { context, diff --git a/src/client/linters/errorHandlers/standard.ts b/src/client/linters/errorHandlers/standard.ts index 6bd2d3c8e115..f6e04b50ff19 100644 --- a/src/client/linters/errorHandlers/standard.ts +++ b/src/client/linters/errorHandlers/standard.ts @@ -1,7 +1,6 @@ import { l10n, Uri } from 'vscode'; import { IApplicationShell } from '../../common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; -import { ExecutionInfo, IOutputChannel } from '../../common/types'; +import { ExecutionInfo, ILogOutputChannel } from '../../common/types'; import { traceError, traceLog } from '../../logging'; import { ILinterManager, LinterId } from '../types'; import { BaseErrorHandler } from './baseErrorHandler'; @@ -29,7 +28,7 @@ export class StandardErrorHandler extends BaseErrorHandler { private async displayLinterError(linterId: LinterId) { const message = l10n.t("There was an error in running the linter '{0}'", linterId); const appShell = this.serviceContainer.get(IApplicationShell); - const outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + const outputChannel = this.serviceContainer.get(ILogOutputChannel); const action = await appShell.showErrorMessage(message, 'View Errors'); if (action === 'View Errors') { outputChannel.show(); diff --git a/src/client/logging/outputChannelLogger.ts b/src/client/logging/outputChannelLogger.ts index 27ea0031c017..40505d33a735 100644 --- a/src/client/logging/outputChannelLogger.ts +++ b/src/client/logging/outputChannelLogger.ts @@ -2,34 +2,29 @@ // Licensed under the MIT License. import * as util from 'util'; -import { OutputChannel } from 'vscode'; +import { LogOutputChannel } from 'vscode'; import { Arguments, ILogging } from './types'; -import { getTimeForLogging } from './util'; - -function formatMessage(level?: string, ...data: Arguments): string { - return level ? `[${level.toUpperCase()} ${getTimeForLogging()}]: ${util.format(...data)}` : util.format(...data); -} export class OutputChannelLogger implements ILogging { - constructor(private readonly channel: OutputChannel) {} + constructor(private readonly channel: LogOutputChannel) {} public traceLog(...data: Arguments): void { this.channel.appendLine(util.format(...data)); } public traceError(...data: Arguments): void { - this.channel.appendLine(formatMessage('error', ...data)); + this.channel.error(util.format(...data)); } public traceWarn(...data: Arguments): void { - this.channel.appendLine(formatMessage('warn', ...data)); + this.channel.warn(util.format(...data)); } public traceInfo(...data: Arguments): void { - this.channel.appendLine(formatMessage('info', ...data)); + this.channel.info(util.format(...data)); } public traceVerbose(...data: Arguments): void { - this.channel.appendLine(formatMessage('debug', ...data)); + this.channel.debug(util.format(...data)); } } diff --git a/src/client/testing/constants.ts b/src/client/testing/constants.ts deleted file mode 100644 index f8c4bb749498..000000000000 --- a/src/client/testing/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -export const TEST_OUTPUT_CHANNEL = 'TEST_OUTPUT_CHANNEL'; diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 1b7d46c995e7..6849f0f8969a 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -99,6 +99,7 @@ export class PythonTestServer implements ITestServer, Disposable { token: options.token, cwd: options.cwd, throwOnStdErr: true, + outputChannel: options.outChannel, }; // Create the Python environment in which to execute the command. diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index b2be2d9c3054..e745adb019b2 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -20,7 +20,7 @@ import { IExtensionSingleActivationService } from '../../activation/types'; import { ICommandManager, IWorkspaceService } from '../../common/application/types'; import * as constants from '../../common/constants'; import { IPythonExecutionFactory } from '../../common/process/types'; -import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry, ITestOutputChannel, Resource } from '../../common/types'; import { DelayedTrigger, IDelayedTrigger } from '../../common/utils/delayTrigger'; import { noop } from '../../common/utils/misc'; import { IInterpreterService } from '../../interpreter/contracts'; @@ -92,6 +92,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc @inject(ICommandManager) private readonly commandManager: ICommandManager, @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, + @inject(ITestOutputChannel) private readonly testOutputChannel: ITestOutputChannel, ) { this.refreshCancellation = new CancellationTokenSource(); @@ -158,12 +159,28 @@ export class PythonTestController implements ITestController, IExtensionSingleAc let executionAdapter: ITestExecutionAdapter; let testProvider: TestProvider; if (settings.testing.unittestEnabled) { - discoveryAdapter = new UnittestTestDiscoveryAdapter(this.pythonTestServer, this.configSettings); - executionAdapter = new UnittestTestExecutionAdapter(this.pythonTestServer, this.configSettings); + discoveryAdapter = new UnittestTestDiscoveryAdapter( + this.pythonTestServer, + this.configSettings, + this.testOutputChannel, + ); + executionAdapter = new UnittestTestExecutionAdapter( + this.pythonTestServer, + this.configSettings, + this.testOutputChannel, + ); testProvider = UNITTEST_PROVIDER; } else { - discoveryAdapter = new PytestTestDiscoveryAdapter(this.pythonTestServer, this.configSettings); - executionAdapter = new PytestTestExecutionAdapter(this.pythonTestServer, this.configSettings); + discoveryAdapter = new PytestTestDiscoveryAdapter( + this.pythonTestServer, + this.configSettings, + this.testOutputChannel, + ); + executionAdapter = new PytestTestExecutionAdapter( + this.pythonTestServer, + this.configSettings, + this.testOutputChannel, + ); testProvider = PYTEST_PROVIDER; } diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 379f1ea9d12c..d2f6f6448777 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -7,7 +7,7 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { IConfigurationService } from '../../../common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { traceVerbose } from '../../../logging'; @@ -21,7 +21,11 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { private deferred: Deferred | undefined; - constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + constructor( + public testServer: ITestServer, + public configSettings: IConfigurationService, + private readonly outputChannel: ITestOutputChannel, + ) { testServer.onDataReceived(this.onDataReceivedHandler, this); } @@ -68,6 +72,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { TEST_UUID: uuid.toString(), TEST_PORT: this.testServer.getPort().toString(), }, + outputChannel: this.outputChannel, }; // Create the Python environment in which to execute the command. diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 70aa2698c0d1..1f6b504074cd 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; @@ -16,7 +16,11 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { private deferred: Deferred | undefined; - constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + constructor( + public testServer: ITestServer, + public configSettings: IConfigurationService, + private readonly outputChannel: ITestOutputChannel, + ) { testServer.onDataReceived(this.onDataReceivedHandler, this); } @@ -31,6 +35,8 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // ** Old version of discover tests. async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { traceVerbose(uri, testIds, debugBool); + // TODO:Remove this line after enabling runs + this.outputChannel.appendLine('Running tests.'); this.deferred = createDeferred(); return this.deferred.promise; } @@ -62,6 +68,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // TEST_UUID: uuid.toString(), // TEST_PORT: this.testServer.getPort().toString(), // }, +// outputChannel: this.outputChannel, // }; // // Create the Python environment in which to execute the command. diff --git a/src/client/testing/testController/pytest/runner.ts b/src/client/testing/testController/pytest/runner.ts index 7bc045b34687..2c6cff724398 100644 --- a/src/client/testing/testController/pytest/runner.ts +++ b/src/client/testing/testController/pytest/runner.ts @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, named } from 'inversify'; +import { inject, injectable } from 'inversify'; import { Disposable, TestItem, TestRun, TestRunProfileKind } from 'vscode'; -import { IOutputChannel } from '../../../common/types'; +import { ITestOutputChannel } from '../../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { ITestDebugLauncher, ITestRunner, LaunchOptions, Options } from '../../common/types'; -import { TEST_OUTPUT_CHANNEL } from '../../constants'; import { filterArguments, getOptionValues } from '../common/argumentsHelper'; import { createTemporaryFile } from '../common/externalDependencies'; import { updateResultFromJunitXml } from '../common/resultsHelper'; @@ -32,7 +31,7 @@ export class PytestRunner implements ITestsRunner { constructor( @inject(ITestRunner) private readonly runner: ITestRunner, @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, - @inject(IOutputChannel) @named(TEST_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel, + @inject(ITestOutputChannel) private readonly outputChannel: ITestOutputChannel, ) {} public async runTests( diff --git a/src/client/testing/testController/unittest/runner.ts b/src/client/testing/testController/unittest/runner.ts index d6bbb59ee640..e0897f554cea 100644 --- a/src/client/testing/testController/unittest/runner.ts +++ b/src/client/testing/testController/unittest/runner.ts @@ -1,16 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { injectable, inject, named } from 'inversify'; +import { injectable, inject } from 'inversify'; import { Location, TestController, TestItem, TestMessage, TestRun, TestRunProfileKind } from 'vscode'; import * as internalScripts from '../../../common/process/internal/scripts'; import { splitLines } from '../../../common/stringUtils'; -import { IOutputChannel } from '../../../common/types'; +import { ITestOutputChannel } from '../../../common/types'; import { noop } from '../../../common/utils/misc'; import { traceError, traceInfo } from '../../../logging'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { ITestRunner, ITestDebugLauncher, IUnitTestSocketServer, LaunchOptions, Options } from '../../common/types'; -import { TEST_OUTPUT_CHANNEL } from '../../constants'; import { clearAllChildren, getTestCaseNodes } from '../common/testItemUtilities'; import { ITestRun, ITestsRunner, TestData, TestRunInstanceOptions, TestRunOptions } from '../common/types'; import { fixLogLines } from '../common/utils'; @@ -33,7 +32,7 @@ export class UnittestRunner implements ITestsRunner { constructor( @inject(ITestRunner) private readonly runner: ITestRunner, @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, - @inject(IOutputChannel) @named(TEST_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel, + @inject(ITestOutputChannel) private readonly outputChannel: ITestOutputChannel, @inject(IUnitTestSocketServer) private readonly server: IUnitTestSocketServer, ) {} diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index fa411b50216f..3f8ecb5797d3 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { @@ -23,7 +23,11 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { private cwd: string | undefined; - constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + constructor( + public testServer: ITestServer, + public configSettings: IConfigurationService, + private readonly outputChannel: ITestOutputChannel, + ) { testServer.onDataReceived(this.onDataReceivedHandler, this); } @@ -50,6 +54,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { command, cwd: this.cwd, uuid, + outChannel: this.outputChannel, }; this.promiseMap.set(uuid, deferred); diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 0bca2778ef75..b39e0cd29560 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { @@ -24,7 +24,11 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { private cwd: string | undefined; - constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + constructor( + public testServer: ITestServer, + public configSettings: IConfigurationService, + private readonly outputChannel: ITestOutputChannel, + ) { testServer.onDataReceived(this.onDataReceivedHandler, this); } @@ -51,6 +55,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uuid, debugBool, testIds, + outChannel: this.outputChannel, }; const deferred = createDeferred(); diff --git a/src/test/activation/node/analysisOptions.unit.test.ts b/src/test/activation/node/analysisOptions.unit.test.ts index d4781f7e03e5..47c0cb0cc838 100644 --- a/src/test/activation/node/analysisOptions.unit.test.ts +++ b/src/test/activation/node/analysisOptions.unit.test.ts @@ -9,7 +9,7 @@ import { NodeLanguageServerAnalysisOptions } from '../../../client/activation/no import { ILanguageServerOutputChannel } from '../../../client/activation/types'; import { IWorkspaceService } from '../../../client/common/application/types'; import { PYTHON, PYTHON_LANGUAGE } from '../../../client/common/constants'; -import { IExperimentService, IOutputChannel } from '../../../client/common/types'; +import { IExperimentService, ILogOutputChannel } from '../../../client/common/types'; suite('Pylance Language Server - Analysis Options', () => { class TestClass extends NodeLanguageServerAnalysisOptions { @@ -28,13 +28,13 @@ suite('Pylance Language Server - Analysis Options', () => { } let analysisOptions: TestClass; - let outputChannel: IOutputChannel; + let outputChannel: ILogOutputChannel; let lsOutputChannel: typemoq.IMock; let workspace: typemoq.IMock; let experimentService: IExperimentService; setup(() => { - outputChannel = typemoq.Mock.ofType().object; + outputChannel = typemoq.Mock.ofType().object; workspace = typemoq.Mock.ofType(); workspace.setup((w) => w.isVirtualWorkspace).returns(() => false); const workspaceConfig = typemoq.Mock.ofType(); diff --git a/src/test/activation/outputChannel.unit.test.ts b/src/test/activation/outputChannel.unit.test.ts index f7d827b4782a..f8f38783bb0e 100644 --- a/src/test/activation/outputChannel.unit.test.ts +++ b/src/test/activation/outputChannel.unit.test.ts @@ -7,7 +7,7 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; import { LanguageServerOutputChannel } from '../../client/activation/common/outputChannel'; import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; -import { IOutputChannel } from '../../client/common/types'; +import { ILogOutputChannel } from '../../client/common/types'; import { sleep } from '../../client/common/utils/async'; import { OutputChannelNames } from '../../client/common/utils/localize'; @@ -15,10 +15,10 @@ suite('Language Server Output Channel', () => { let appShell: TypeMoq.IMock; let languageServerOutputChannel: LanguageServerOutputChannel; let commandManager: TypeMoq.IMock; - let output: TypeMoq.IMock; + let output: TypeMoq.IMock; setup(() => { appShell = TypeMoq.Mock.ofType(); - output = TypeMoq.Mock.ofType(); + output = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); languageServerOutputChannel = new LanguageServerOutputChannel(appShell.object, commandManager.object, []); }); diff --git a/src/test/common/installer/moduleInstaller.unit.test.ts b/src/test/common/installer/moduleInstaller.unit.test.ts index 5c77f8165492..01ac0e315555 100644 --- a/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/src/test/common/installer/moduleInstaller.unit.test.ts @@ -13,7 +13,6 @@ import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { CancellationTokenSource, Disposable, ProgressLocation, Uri, WorkspaceConfiguration } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../../../client/common/constants'; import { CondaInstaller } from '../../../client/common/installer/condaInstaller'; import { ModuleInstaller } from '../../../client/common/installer/moduleInstaller'; import { PipEnvInstaller, pipenvName } from '../../../client/common/installer/pipEnvInstaller'; @@ -32,7 +31,7 @@ import { IConfigurationService, IDisposableRegistry, IInstaller, - IOutputChannel, + ILogOutputChannel, IPythonSettings, Product, } from '../../../client/common/types'; @@ -89,7 +88,7 @@ suite('Module Installer', () => { return super.elevatedInstall(execPath, args); } } - let outputChannel: TypeMoq.IMock; + let outputChannel: TypeMoq.IMock; let appShell: TypeMoq.IMock; let serviceContainer: TypeMoq.IMock; @@ -104,9 +103,9 @@ suite('Module Installer', () => { traceLogStub = sinon.stub(logging, 'traceLog'); serviceContainer = TypeMoq.Mock.ofType(); - outputChannel = TypeMoq.Mock.ofType(); + outputChannel = TypeMoq.Mock.ofType(); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) + .setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))) .returns(() => outputChannel.object); appShell = TypeMoq.Mock.ofType(); serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); diff --git a/src/test/format/formatter.unit.test.ts b/src/test/format/formatter.unit.test.ts index cf0297628a83..05970d0c71f6 100644 --- a/src/test/format/formatter.unit.test.ts +++ b/src/test/format/formatter.unit.test.ts @@ -13,7 +13,6 @@ import { IApplicationShell, IWorkspaceService } from '../../client/common/applic import { WorkspaceService } from '../../client/common/application/workspace'; import { PythonSettings } from '../../client/common/configSettings'; import { ConfigurationService } from '../../client/common/configuration/service'; -import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; import { IPythonToolExecutionService } from '../../client/common/process/types'; import { @@ -21,7 +20,7 @@ import { IConfigurationService, IDisposableRegistry, IFormattingSettings, - IOutputChannel, + ILogOutputChannel, IPythonSettings, } from '../../client/common/types'; import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; @@ -37,7 +36,7 @@ import { MockOutputChannel } from '../mockClasses'; suite('Formatting - Test Arguments', () => { let container: IServiceContainer; - let outputChannel: IOutputChannel; + let outputChannel: ILogOutputChannel; let workspace: IWorkspaceService; let settings: IPythonSettings; const workspaceUri = Uri.file(__dirname); @@ -85,9 +84,7 @@ suite('Formatting - Test Arguments', () => { when(configService.getSettings(anything())).thenReturn(instance(settings)); when(workspace.getWorkspaceFolder(anything())).thenReturn({ name: '', index: 0, uri: workspaceUri }); - when(container.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL)).thenReturn( - instance(outputChannel), - ); + when(container.get(ILogOutputChannel)).thenReturn(instance(outputChannel)); when(container.get(IApplicationShell)).thenReturn(instance(appShell)); when(container.get(IFormatterHelper)).thenReturn(formatterHelper); when(container.get(IWorkspaceService)).thenReturn(instance(workspace)); diff --git a/src/test/linters/common.ts b/src/test/linters/common.ts index c602492ccd67..3c8f72a8d710 100644 --- a/src/test/linters/common.ts +++ b/src/test/linters/common.ts @@ -18,7 +18,7 @@ import { IConfigurationService, IInstaller, IMypyCategorySeverity, - IOutputChannel, + ILogOutputChannel, IPycodestyleCategorySeverity, IPylintCategorySeverity, IPythonSettings, @@ -243,7 +243,7 @@ export class BaseTestFixture { public lintingSettings: LintingSettings; // data - public outputChannel: TypeMoq.IMock; + public outputChannel: TypeMoq.IMock; // artifacts public output: string; @@ -309,10 +309,10 @@ export class BaseTestFixture { // data - this.outputChannel = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + this.outputChannel = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) + .setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))) .returns(() => this.outputChannel.object); this.initData(); diff --git a/src/test/linters/lintengine.test.ts b/src/test/linters/lintengine.test.ts index 20599d7fb713..1bf77c502af5 100644 --- a/src/test/linters/lintengine.test.ts +++ b/src/test/linters/lintengine.test.ts @@ -4,12 +4,12 @@ 'use strict'; import * as TypeMoq from 'typemoq'; -import { OutputChannel, TextDocument, Uri } from 'vscode'; +import { TextDocument, Uri } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; +import { PYTHON_LANGUAGE } from '../../client/common/constants'; import '../../client/common/extensions'; import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, ILintingSettings, IOutputChannel, IPythonSettings } from '../../client/common/types'; +import { IConfigurationService, ILintingSettings, ILogOutputChannel, IPythonSettings } from '../../client/common/types'; import { IInterpreterService } from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; import { LintingEngine } from '../../client/linters/lintingEngine'; @@ -54,10 +54,8 @@ suite('Linting - LintingEngine', () => { .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) .returns(() => configService.object); - const outputChannel = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) - .returns(() => outputChannel.object); + const outputChannel = TypeMoq.Mock.ofType(); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))).returns(() => outputChannel.object); lintManager = TypeMoq.Mock.ofType(); lintManager.setup((x) => x.isLintingEnabled(TypeMoq.It.isAny())).returns(async () => true); diff --git a/src/test/mockClasses.ts b/src/test/mockClasses.ts index 0273cf27fdb9..c962c4d67ca4 100644 --- a/src/test/mockClasses.ts +++ b/src/test/mockClasses.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as util from 'util'; import { Flake8CategorySeverity, ILintingSettings, @@ -7,13 +8,32 @@ import { IPylintCategorySeverity, } from '../client/common/types'; -export class MockOutputChannel implements vscode.OutputChannel { +export class MockOutputChannel implements vscode.LogOutputChannel { public name: string; public output: string; public isShown!: boolean; + private _eventEmitter = new vscode.EventEmitter(); + public onDidChangeLogLevel: vscode.Event = this._eventEmitter.event; constructor(name: string) { this.name = name; this.output = ''; + this.logLevel = vscode.LogLevel.Debug; + } + public logLevel: vscode.LogLevel; + trace(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + debug(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + info(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + warn(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + error(error: string | Error, ...args: any[]): void { + this.appendLine(util.format(error, ...args)); } public append(value: string) { this.output += value; diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index e6ea57a88673..89f4ab1a2d07 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -411,3 +411,35 @@ export class InlayHint { public kind?: vscode.InlayHintKind, ) {} } + +export enum LogLevel { + /** + * No messages are logged with this level. + */ + Off = 0, + + /** + * All messages are logged with this level. + */ + Trace = 1, + + /** + * Messages with debug and higher log level are logged with this level. + */ + Debug = 2, + + /** + * Messages with info and higher log level are logged with this level. + */ + Info = 3, + + /** + * Messages with warning and higher log level are logged with this level. + */ + Warning = 4, + + /** + * Only error messages are logged with this level. + */ + Error = 5, +} diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index e3c6763eace7..1b8a9d78d580 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -4,8 +4,7 @@ import { Container } from 'inversify'; import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { Disposable, Memento, OutputChannel } from 'vscode'; -import { STANDARD_OUTPUT_CHANNEL } from '../client/common/constants'; +import { Disposable, Memento } from 'vscode'; import { IS_WINDOWS } from '../client/common/platform/constants'; import { FileSystem } from '../client/common/platform/fileSystem'; import { PathUtils } from '../client/common/platform/pathUtils'; @@ -28,10 +27,11 @@ import { ICurrentProcess, IDisposableRegistry, IMemento, - IOutputChannel, + ILogOutputChannel, IPathUtils, IsWindows, WORKSPACE_MEMENTO, + ITestOutputChannel, } from '../client/common/types'; import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; import { registerTypes as formattersRegisterTypes } from '../client/formatters/serviceRegistry'; @@ -48,7 +48,6 @@ import { ServiceContainer } from '../client/ioc/container'; import { ServiceManager } from '../client/ioc/serviceManager'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; import { registerTypes as lintersRegisterTypes } from '../client/linters/serviceRegistry'; -import { TEST_OUTPUT_CHANNEL } from '../client/testing/constants'; import { registerTypes as unittestsRegisterTypes } from '../client/testing/serviceRegistry'; import { LegacyFileSystem } from './legacyFileSystem'; import { MockOutputChannel } from './mockClasses'; @@ -83,14 +82,10 @@ export class IocContainer { const stdOutputChannel = new MockOutputChannel('Python'); this.disposables.push(stdOutputChannel); - this.serviceManager.addSingletonInstance( - IOutputChannel, - stdOutputChannel, - STANDARD_OUTPUT_CHANNEL, - ); + this.serviceManager.addSingletonInstance(ILogOutputChannel, stdOutputChannel); const testOutputChannel = new MockOutputChannel('Python Test - UnitTests'); this.disposables.push(testOutputChannel); - this.serviceManager.addSingletonInstance(IOutputChannel, testOutputChannel, TEST_OUTPUT_CHANNEL); + this.serviceManager.addSingletonInstance(ITestOutputChannel, testOutputChannel); this.serviceManager.addSingleton( IInterpreterAutoSelectionService, diff --git a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts index 02a7193172af..c8b6085e599d 100644 --- a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts +++ b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts @@ -5,13 +5,12 @@ import * as TypeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, IOutputChannel, Product } from '../../../../client/common/types'; +import { IInstaller, ITestOutputChannel, Product } from '../../../../client/common/types'; import { getNamesAndValues } from '../../../../client/common/utils/enum'; import { IServiceContainer } from '../../../../client/ioc/types'; import { UNIT_TEST_PRODUCTS } from '../../../../client/testing/common/constants'; import { TestConfigurationManager } from '../../../../client/testing/common/testConfigurationManager'; import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/testing/common/types'; -import { TEST_OUTPUT_CHANNEL } from '../../../../client/testing/constants'; class MockTestConfigurationManager extends TestConfigurationManager { // The workspace arg is ignored. @@ -42,7 +41,7 @@ suite('Unit Test Configuration Manager (unit)', () => { const installer = TypeMoq.Mock.ofType().object; const serviceContainer = TypeMoq.Mock.ofType(); serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(TEST_OUTPUT_CHANNEL))) + .setup((s) => s.get(TypeMoq.It.isValue(ITestOutputChannel))) .returns(() => outputChannel); serviceContainer .setup((s) => s.get(TypeMoq.It.isValue(ITestConfigSettingsService))) diff --git a/src/test/testing/configuration.unit.test.ts b/src/test/testing/configuration.unit.test.ts index fec936a2a21a..abb57aac2309 100644 --- a/src/test/testing/configuration.unit.test.ts +++ b/src/test/testing/configuration.unit.test.ts @@ -7,7 +7,13 @@ import { expect } from 'chai'; import * as typeMoq from 'typemoq'; import { OutputChannel, Uri, WorkspaceConfiguration } from 'vscode'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; -import { IConfigurationService, IInstaller, IOutputChannel, IPythonSettings, Product } from '../../client/common/types'; +import { + IConfigurationService, + IInstaller, + ITestOutputChannel, + IPythonSettings, + Product, +} from '../../client/common/types'; import { getNamesAndValues } from '../../client/common/utils/enum'; import { IServiceContainer } from '../../client/ioc/types'; import { UNIT_TEST_PRODUCTS } from '../../client/testing/common/constants'; @@ -18,7 +24,6 @@ import { ITestConfigurationManagerFactory, ITestsHelper, } from '../../client/testing/common/types'; -import { TEST_OUTPUT_CHANNEL } from '../../client/testing/constants'; import { ITestingSettings } from '../../client/testing/configuration/types'; import { NONE_SELECTED, UnitTestConfigurationService } from '../../client/testing/configuration'; @@ -56,7 +61,7 @@ suite('Unit Tests - ConfigurationService', () => { configurationService.setup((c) => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); serviceContainer - .setup((c) => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))) + .setup((c) => c.get(typeMoq.It.isValue(ITestOutputChannel))) .returns(() => outputChannel.object); serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); serviceContainer diff --git a/src/test/testing/configurationFactory.unit.test.ts b/src/test/testing/configurationFactory.unit.test.ts index 1418147d615c..74f7dd0da19b 100644 --- a/src/test/testing/configurationFactory.unit.test.ts +++ b/src/test/testing/configurationFactory.unit.test.ts @@ -7,11 +7,10 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as typeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, IOutputChannel, Product } from '../../client/common/types'; +import { IInstaller, ITestOutputChannel, Product } from '../../client/common/types'; import { IServiceContainer } from '../../client/ioc/types'; import { ITestConfigSettingsService, ITestConfigurationManagerFactory } from '../../client/testing/common/types'; import { TestConfigurationManagerFactory } from '../../client/testing/configurationFactory'; -import { TEST_OUTPUT_CHANNEL } from '../../client/testing/constants'; import * as pytest from '../../client/testing/configuration/pytest/testConfigurationManager'; import * as unittest from '../../client/testing/configuration/unittest/testConfigurationManager'; @@ -26,7 +25,7 @@ suite('Unit Tests - ConfigurationManagerFactory', () => { const testConfigService = typeMoq.Mock.ofType(); serviceContainer - .setup((c) => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))) + .setup((c) => c.get(typeMoq.It.isValue(ITestOutputChannel))) .returns(() => outputChannel.object); serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); serviceContainer diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index 113e4175fe69..3d3521291f74 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -3,14 +3,16 @@ import * as assert from 'assert'; import * as path from 'path'; +import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../../client/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; import { UnittestTestDiscoveryAdapter } from '../../../../client/testing/testController/unittest/testDiscoveryAdapter'; suite('Unittest test discovery adapter', () => { let stubConfigSettings: IConfigurationService; + let outputChannel: typemoq.IMock; setup(() => { stubConfigSettings = ({ @@ -18,6 +20,7 @@ suite('Unittest test discovery adapter', () => { testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, }), } as unknown) as IConfigurationService; + outputChannel = typemoq.Mock.ofType(); }); test('discoverTests should send the discovery command to the test server', async () => { @@ -25,6 +28,7 @@ suite('Unittest test discovery adapter', () => { const stubTestServer = ({ sendCommand(opt: TestCommandOptions): Promise { + delete opt.outChannel; options = opt; return Promise.resolve(); }, @@ -37,7 +41,7 @@ suite('Unittest test discovery adapter', () => { const uri = Uri.file('/foo/bar'); const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'discovery.py'); - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); adapter.discoverTests(uri); assert.deepStrictEqual(options, { @@ -62,7 +66,7 @@ suite('Unittest test discovery adapter', () => { const uri = Uri.file('/foo/bar'); const data = { status: 'success' }; const uuid = '123456789'; - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); const promise = adapter.discoverTests(uri); adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); @@ -87,7 +91,7 @@ suite('Unittest test discovery adapter', () => { const uri = Uri.file('/foo/bar'); - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); const promise = adapter.discoverTests(uri); const data = { status: 'success' }; diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index f8648c8963fa..d88f033d39a4 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -3,14 +3,16 @@ import * as assert from 'assert'; import * as path from 'path'; +import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../../client/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; suite('Unittest test execution adapter', () => { let stubConfigSettings: IConfigurationService; + let outputChannel: typemoq.IMock; setup(() => { stubConfigSettings = ({ @@ -18,6 +20,7 @@ suite('Unittest test execution adapter', () => { testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, }), } as unknown) as IConfigurationService; + outputChannel = typemoq.Mock.ofType(); }); test('runTests should send the run command to the test server', async () => { @@ -25,6 +28,7 @@ suite('Unittest test execution adapter', () => { const stubTestServer = ({ sendCommand(opt: TestCommandOptions): Promise { + delete opt.outChannel; options = opt; return Promise.resolve(); }, @@ -37,7 +41,7 @@ suite('Unittest test execution adapter', () => { const uri = Uri.file('/foo/bar'); const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); adapter.runTests(uri, [], false); const expectedOptions: TestCommandOptions = { @@ -66,7 +70,7 @@ suite('Unittest test execution adapter', () => { const data = { status: 'success' }; const uuid = '123456789'; - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); // triggers runTests flow which will run onDataReceivedHandler and the // promise resolves into the parsed data. @@ -93,7 +97,7 @@ suite('Unittest test execution adapter', () => { const uri = Uri.file('/foo/bar'); - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); // triggers runTests flow which will run onDataReceivedHandler and the // promise resolves into the parsed data. diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index b6be8d6081de..539647aece9f 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -3,9 +3,10 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; import { TestController, TestItem, Uri } from 'vscode'; -import { IConfigurationService } from '../../../client/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; // 7/7 import { WorkspaceTestAdapter } from '../../../client/testing/testController/workspaceTestAdapter'; @@ -20,6 +21,7 @@ suite('Workspace test adapter', () => { let discoverTestsStub: sinon.SinonStub; let sendTelemetryStub: sinon.SinonStub; + let outputChannel: typemoq.IMock; let telemetryEvent: { eventName: EventName; properties: Record }[] = []; @@ -97,6 +99,7 @@ suite('Workspace test adapter', () => { discoverTestsStub = sandbox.stub(UnittestTestDiscoveryAdapter.prototype, 'discoverTests'); sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + outputChannel = typemoq.Mock.ofType(); }); teardown(() => { @@ -109,8 +112,16 @@ suite('Workspace test adapter', () => { test("When discovering tests, the workspace test adapter should call the test discovery adapter's discoverTest method", async () => { discoverTestsStub.resolves(); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); // 7/7 + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, @@ -134,8 +145,16 @@ suite('Workspace test adapter', () => { }), ); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); // 7/7 + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, @@ -155,8 +174,16 @@ suite('Workspace test adapter', () => { test('If discovery succeeds, send a telemetry event with the "failed" key set to false', async () => { discoverTestsStub.resolves({ status: 'success' }); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', @@ -177,8 +204,16 @@ suite('Workspace test adapter', () => { test('If discovery failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { discoverTestsStub.rejects(new Error('foo')); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 9fb4bbec329b..ebbe7ca59e72 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -112,6 +112,7 @@ mockedVSCode.FileSystemError = vscodeMocks.vscMockExtHostedTypes.FileSystemError mockedVSCode.LanguageStatusSeverity = vscodeMocks.LanguageStatusSeverity; mockedVSCode.QuickPickItemKind = vscodeMocks.QuickPickItemKind; mockedVSCode.InlayHint = vscodeMocks.InlayHint; +mockedVSCode.LogLevel = vscodeMocks.LogLevel; (mockedVSCode as any).NotebookCellKind = vscodeMocks.vscMockExtHostedTypes.NotebookCellKind; (mockedVSCode as any).CellOutputKind = vscodeMocks.vscMockExtHostedTypes.CellOutputKind; (mockedVSCode as any).NotebookCellRunState = vscodeMocks.vscMockExtHostedTypes.NotebookCellRunState; From 8f8f624710149b50ddaa9e5adc028d318529f073 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 17 Apr 2023 19:34:24 -0700 Subject: [PATCH 051/336] Direct users to the Jupyter extension when using Run in Interactive window (#21072) Closes https://github.com/microsoft/vscode-python/issues/20576 --- package.json | 23 ++++++- package.nls.json | 2 + src/client/common/application/commands.ts | 1 + src/client/common/application/contextKeys.ts | 1 + src/client/common/constants.ts | 1 + src/client/common/serviceRegistry.ts | 5 ++ src/client/common/utils/localize.ts | 3 + src/client/jupyter/jupyterIntegration.ts | 5 +- src/client/jupyter/requireJupyterPrompt.ts | 45 ++++++++++++++ src/client/telemetry/constants.ts | 1 + src/client/telemetry/index.ts | 16 +++++ ...eractiveWindowMiddlewareAddon.unit.test.ts | 3 +- .../jupyter/requireJupyterPrompt.unit.test.ts | 60 +++++++++++++++++++ 13 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 src/client/jupyter/requireJupyterPrompt.ts create mode 100644 src/test/jupyter/requireJupyterPrompt.unit.test.ts diff --git a/package.json b/package.json index 893eb2299abd..62bd7f89b700 100644 --- a/package.json +++ b/package.json @@ -374,6 +374,11 @@ "light": "resources/light/repl.svg" }, "title": "%python.command.python.viewOutput.title%" + }, + { + "category": "Python", + "command": "python.installJupyter", + "title": "%python.command.python.installJupyter.title%" } ], "configuration": { @@ -1705,13 +1710,25 @@ { "submenu": "python.run", "group": "Python", - "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported" + "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && isWorkspaceTrusted" }, { "command": "python.sortImports", "group": "Refactor", "title": "%python.command.python.sortImports.title%", "when": "editorLangId == python && !notebookEditorFocused && !virtualWorkspace && shellExecutionSupported" + }, + { + "submenu": "python.runFileInteractive", + "group": "Jupyter2", + "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && !isJupyterInstalled && isWorkspaceTrusted" + } + ], + "python.runFileInteractive": [ + { + "command": "python.installJupyter", + "group": "Jupyter2", + "when": "resourceLangId == python && !virtualWorkspace && shellExecutionSupported" } ], "python.run": [ @@ -1779,6 +1796,10 @@ "id": "python.run", "label": "%python.editor.context.submenu.runPython%", "icon": "$(play)" + }, + { + "id": "python.runFileInteractive", + "label": "%python.editor.context.submenu.runPythonInteractive%" } ], "viewsWelcome": [ diff --git a/package.nls.json b/package.nls.json index cb777d95c214..0bf0ac2a8426 100644 --- a/package.nls.json +++ b/package.nls.json @@ -10,6 +10,7 @@ "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", + "python.command.python.installJupyter.title": "Install the Jupyter extension", "python.command.python.viewLanguageServerOutput.title": "Show Language Server Output", "python.command.python.configureTests.title": "Configure Tests", "python.command.testing.rerunFailedTests.title": "Rerun Failed Tests", @@ -26,6 +27,7 @@ "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", "python.menu.createNewFile.title": "Python File", "python.editor.context.submenu.runPython": "Run Python", + "python.editor.context.submenu.runPythonInteractive": "Run in Interactive window", "python.activeStateToolPath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).", "python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", "python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 277baffd19a4..2a4404440101 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -17,6 +17,7 @@ export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping; */ interface ICommandNameWithoutArgumentTypeMapping { [Commands.InstallPythonOnMac]: []; + [Commands.InstallJupyter]: []; [Commands.InstallPythonOnLinux]: []; [Commands.InstallPython]: []; [Commands.ClearWorkspaceInterpreter]: []; diff --git a/src/client/common/application/contextKeys.ts b/src/client/common/application/contextKeys.ts index 2f791ae66846..d6249f05eaec 100644 --- a/src/client/common/application/contextKeys.ts +++ b/src/client/common/application/contextKeys.ts @@ -5,4 +5,5 @@ export enum ExtensionContextKey { showInstallPythonTile = 'showInstallPythonTile', HasFailedTests = 'hasFailedTests', RefreshingTests = 'refreshingTests', + IsJupyterInstalled = 'isJupyterInstalled', } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index f53923b3e1b4..104504650aed 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -46,6 +46,7 @@ export namespace Commands { export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; + export const InstallJupyter = 'python.installJupyter'; export const InstallPython = 'python.installPython'; export const InstallPythonOnLinux = 'python.installPythonOnLinux'; export const InstallPythonOnMac = 'python.installPythonOnMac'; diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 8e2c4d4d3ebe..5b527499460a 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -90,6 +90,7 @@ import { IMultiStepInputFactory, MultiStepInputFactory } from './utils/multiStep import { Random } from './utils/random'; import { ContextKeyManager } from './application/contextKeyManager'; import { CreatePythonFileCommandHandler } from './application/commands/createPythonFile'; +import { RequireJupyterPrompt } from '../jupyter/requireJupyterPrompt'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); @@ -110,6 +111,10 @@ export function registerTypes(serviceManager: IServiceManager): void { IJupyterExtensionDependencyManager, JupyterExtensionDependencyManager, ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + RequireJupyterPrompt, + ); serviceManager.addSingleton( IExtensionSingleActivationService, CreatePythonFileCommandHandler, diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 10c70c8c6cd0..e67aad586764 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -188,6 +188,9 @@ export namespace LanguageService { ); } export namespace Interpreters { + export const requireJupyter = l10n.t( + 'Running in Interactive window requires Jupyter Extension. Would you like to install it? [Learn more](https://aka.ms/pythonJupyterSupport).', + ); export const installingPython = l10n.t('Installing Python into Environment...'); export const discovering = l10n.t('Discovering Python Interpreters'); export const refreshing = l10n.t('Refreshing Python Interpreters'); diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index 16da174f3178..a0fa0fedb63f 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -8,7 +8,7 @@ import { inject, injectable, named } from 'inversify'; import { dirname } from 'path'; import { CancellationToken, Event, Extension, Memento, Uri } from 'vscode'; import type { SemVer } from 'semver'; -import { IWorkspaceService } from '../common/application/types'; +import { IContextKeyManager, IWorkspaceService } from '../common/application/types'; import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; import { InterpreterUri, ModuleInstallFlags } from '../common/installer/types'; import { @@ -35,6 +35,7 @@ import { import { PythonEnvironment } from '../pythonEnvironments/info'; import { IDataViewerDataProvider, IJupyterUriProvider } from './types'; import { PylanceApi } from '../activation/node/pylanceApi'; +import { ExtensionContextKey } from '../common/application/contextKeys'; /** * This allows Python extension to update Product enum without breaking Jupyter. * I.e. we have a strict contract, else using numbers (in enums) is bound to break across products. @@ -201,9 +202,11 @@ export class JupyterExtensionIntegration { @inject(IComponentAdapter) private pyenvs: IComponentAdapter, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(ICondaService) private readonly condaService: ICondaService, + @inject(IContextKeyManager) private readonly contextManager: IContextKeyManager, ) {} public registerApi(jupyterExtensionApi: JupyterExtensionApi): JupyterExtensionApi | undefined { + this.contextManager.setContext(ExtensionContextKey.IsJupyterInstalled, true); if (!this.workspaceService.isTrusted) { this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(jupyterExtensionApi)); return undefined; diff --git a/src/client/jupyter/requireJupyterPrompt.ts b/src/client/jupyter/requireJupyterPrompt.ts new file mode 100644 index 000000000000..3e6878ba4269 --- /dev/null +++ b/src/client/jupyter/requireJupyterPrompt.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { Common, Interpreters } from '../common/utils/localize'; +import { Commands, JUPYTER_EXTENSION_ID } from '../common/constants'; +import { IDisposable, IDisposableRegistry } from '../common/types'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +@injectable() +export class RequireJupyterPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposable[], + ) {} + + public async activate(): Promise { + this.disposables.push(this.commandManager.registerCommand(Commands.InstallJupyter, () => this._showPrompt())); + } + + public async _showPrompt(): Promise { + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo]; + const telemetrySelections: ['Yes', 'No'] = ['Yes', 'No']; + const selection = await this.appShell.showInformationMessage(Interpreters.requireJupyter, ...prompts); + sendTelemetryEvent(EventName.REQUIRE_JUPYTER_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await this.commandManager.executeCommand( + 'workbench.extensions.installExtension', + JUPYTER_EXTENSION_ID, + undefined, + ); + } + } +} diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index d30a2683562c..778eb7cc39a0 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -30,6 +30,7 @@ export enum EventName { PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT', PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT', CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT', + REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT', ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH', ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION', ENVFILE_WORKSPACE = 'ENVFILE_WORKSPACE', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index df2b454cbe27..9e86f29201ee 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1315,6 +1315,22 @@ export interface IEventNamePropertyMapping { */ selection: 'Allow' | 'Close' | undefined; }; + /** + * Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed. + */ + /* __GDPR__ + "conda_inherit_env_prompt" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" } + } + */ + [EventName.REQUIRE_JUPYTER_PROMPT]: { + /** + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected + * `undefined` When 'x' is selected + */ + selection: 'Yes' | 'No' | undefined; + }; /** * Telemetry event sent with details when user clicks the prompt with the following message: * diff --git a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts b/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts index 472dea147503..256e57a5d724 100644 --- a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts +++ b/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts @@ -19,7 +19,7 @@ import { } from '../../../client/interpreter/contracts'; import { IInterpreterSelector } from '../../../client/interpreter/configuration/types'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { IWorkspaceService } from '../../../client/common/application/types'; +import { IContextKeyManager, IWorkspaceService } from '../../../client/common/application/types'; import { MockMemento } from '../../mocks/mementos'; suite('Pylance Language Server - Interactive Window LSP Notebooks', () => { @@ -41,6 +41,7 @@ suite('Pylance Language Server - Interactive Window LSP Notebooks', () => { mock(), mock(), mock(), + mock(), ); jupyterApi.registerGetNotebookUriForTextDocumentUriFunction(getNotebookUriFunction); }); diff --git a/src/test/jupyter/requireJupyterPrompt.unit.test.ts b/src/test/jupyter/requireJupyterPrompt.unit.test.ts new file mode 100644 index 000000000000..0eb6c9e06958 --- /dev/null +++ b/src/test/jupyter/requireJupyterPrompt.unit.test.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { mock, instance, verify, anything, when } from 'ts-mockito'; +import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; +import { Commands, JUPYTER_EXTENSION_ID } from '../../client/common/constants'; +import { IDisposableRegistry } from '../../client/common/types'; +import { Common, Interpreters } from '../../client/common/utils/localize'; +import { RequireJupyterPrompt } from '../../client/jupyter/requireJupyterPrompt'; + +suite('RequireJupyterPrompt Unit Tests', () => { + let requireJupyterPrompt: RequireJupyterPrompt; + let appShell: IApplicationShell; + let commandManager: ICommandManager; + let disposables: IDisposableRegistry; + + setup(() => { + appShell = mock(); + commandManager = mock(); + disposables = mock(); + + requireJupyterPrompt = new RequireJupyterPrompt( + instance(appShell), + instance(commandManager), + instance(disposables), + ); + }); + + test('Activation registers command', async () => { + await requireJupyterPrompt.activate(); + + verify(commandManager.registerCommand(Commands.InstallJupyter, anything())).once(); + }); + + test('Show prompt with Yes selection installs Jupyter extension', async () => { + when( + appShell.showInformationMessage(Interpreters.requireJupyter, Common.bannerLabelYes, Common.bannerLabelNo), + ).thenReturn(Promise.resolve(Common.bannerLabelYes)); + + await requireJupyterPrompt.activate(); + await requireJupyterPrompt._showPrompt(); + + verify( + commandManager.executeCommand('workbench.extensions.installExtension', JUPYTER_EXTENSION_ID, undefined), + ).once(); + }); + + test('Show prompt with No selection does not install Jupyter extension', async () => { + when( + appShell.showInformationMessage(Interpreters.requireJupyter, Common.bannerLabelYes, Common.bannerLabelNo), + ).thenReturn(Promise.resolve(Common.bannerLabelNo)); + + await requireJupyterPrompt.activate(); + await requireJupyterPrompt._showPrompt(); + + verify( + commandManager.executeCommand('workbench.extensions.installExtension', JUPYTER_EXTENSION_ID, undefined), + ).never(); + }); +}); From 61882f7e78c4bcd9d361cfaa189c87f53ce5325f Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 18 Apr 2023 08:03:14 -0700 Subject: [PATCH 052/336] Add quick pick hover support to explain conda environment lacking a Python interpreter (#21073) Closes https://github.com/microsoft/vscode-python/issues/20786 --- package.json | 3 ++- src/client/common/utils/localize.ts | 3 +++ .../commands/setInterpreter.ts | 1 + src/client/interpreter/configuration/types.ts | 6 +----- .../vscode.proposed.quickPickItemTooltip.d.ts | 16 ++++++++++++++++ 5 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 typings/vscode-proposed/vscode.proposed.quickPickItemTooltip.d.ts diff --git a/package.json b/package.json index 62bd7f89b700..60c80693f36e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "contribEditorContentMenu", "quickPickSortByLabel", "envShellEvent", - "testObserver" + "testObserver", + "quickPickItemTooltip" ], "author": { "name": "Microsoft Corporation" diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index e67aad586764..4c3cd7a45d0d 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -221,6 +221,9 @@ export namespace Interpreters { } export namespace InterpreterQuickPickList { + export const condaEnvWithoutPythonTooltip = l10n.t( + 'Python is not available in this environment, it will automatically be installed upon selecting it', + ); export const noPythonInstalled = l10n.t('Python is not installed, please download and install it'); export const clickForInstructions = l10n.t('Click for instructions...'); export const globalGroupName = l10n.t('Global'); diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 9a1d643269ef..c0876ff518dd 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -415,6 +415,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem if (isInterpreterQuickPickItem(item) && isProblematicCondaEnvironment(item.interpreter)) { if (!items[i].label.includes(Octicons.Warning)) { items[i].label = `${Octicons.Warning} ${items[i].label}`; + items[i].tooltip = InterpreterQuickPickList.condaEnvWithoutPythonTooltip; } } }); diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index 90facb7fe640..2f3882e1246e 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -52,11 +52,7 @@ export interface IInterpreterQuickPickItem extends QuickPickItem { interpreter: PythonEnvironment; } -export interface ISpecialQuickPickItem { - label: string; - description?: string; - detail?: string; - alwaysShow: boolean; +export interface ISpecialQuickPickItem extends QuickPickItem { path?: string; } diff --git a/typings/vscode-proposed/vscode.proposed.quickPickItemTooltip.d.ts b/typings/vscode-proposed/vscode.proposed.quickPickItemTooltip.d.ts new file mode 100644 index 000000000000..4e7d00fa5edf --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.quickPickItemTooltip.d.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/73904 + + export interface QuickPickItem { + /** + * An optional flag to sort the final results by index of first query match in label. Defaults to true. + */ + tooltip?: string | MarkdownString; + } +} From d05b83f6c1a222464eb894f6a530fc1912bbbdac Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 18 Apr 2023 08:28:56 -0700 Subject: [PATCH 053/336] Python test execution simple (#21053) closes https://github.com/microsoft/vscode-python/issues/20897 closes https://github.com/microsoft/vscode-python/issues/20084 closes https://github.com/microsoft/vscode-python/issues/20081 --------- Co-authored-by: Karthik Nadig --- .../.data/unittest_folder/test_subtract.py | 3 +- .../expected_discovery_test_output.py | 2 + .../expected_execution_test_output.py | 328 ++++++++++++++++++ pythonFiles/tests/pytestadapter/helpers.py | 6 +- .../tests/pytestadapter/test_discovery.py | 9 +- .../tests/pytestadapter/test_execution.py | 165 +++++++++ pythonFiles/vscode_pytest/__init__.py | 210 +++++++++-- 7 files changed, 688 insertions(+), 35 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/expected_execution_test_output.py create mode 100644 pythonFiles/tests/pytestadapter/test_execution.py diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py index 80087fed0f3c..087e5140def4 100644 --- a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py +++ b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py @@ -22,4 +22,5 @@ def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbe self, ): result = subtract(-2, -3) - self.assertEqual(result, 1) + # This is intentional to test assertion failures + self.assertEqual(result, 100000) diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index e1422a81c979..8e96d109ba78 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -3,6 +3,8 @@ from .helpers import TEST_DATA_PATH, find_test_line_number +# This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. + # This is the expected output for the empty_discovery.py file. # └── TEST_DATA_PATH_STR = os.fspath(TEST_DATA_PATH) diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py new file mode 100644 index 000000000000..a894403c7d71 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -0,0 +1,328 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +TEST_SUBTRACT_FUNCTION = "unittest_folder/test_subtract.py::TestSubtractFunction::" +TEST_ADD_FUNCTION = "unittest_folder/test_add.py::TestAddFunction::" +SUCCESS = "success" +FAILURE = "failure" +TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" + +# This is the expected output for the unittest_folder execute tests +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ ├── test_add_negative_numbers: success +# │ └── test_add_positive_numbers: success +# └── test_subtract.py +# └── TestSubtractFunction +# ├── test_subtract_negative_numbers: failure +# └── test_subtract_positive_numbers: success +uf_execution_expected_output = { + f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers": { + "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + "outcome": FAILURE, + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers": { + "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + + +# This is the expected output for the unittest_folder only execute add.py tests +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ ├── test_add_negative_numbers: success +# │ └── test_add_positive_numbers: success +uf_single_file_expected_output = { + f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the unittest_folder execute only signle method +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ └── test_add_positive_numbers: success +uf_single_method_execution_expected_output = { + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + } +} + +# This is the expected output for the unittest_folder tests run where two tests +# run are in different files. +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ └── test_add_positive_numbers: success +# └── test_subtract.py +# └── TestSubtractFunction +# └── test_subtract_positive_numbers: success +uf_non_adjacent_tests_execution_expected_output = { + TEST_SUBTRACT_FUNCTION + + "test_subtract_positive_numbers": { + "test": TEST_SUBTRACT_FUNCTION + "test_subtract_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + TEST_ADD_FUNCTION + + "test_add_positive_numbers": { + "test": TEST_ADD_FUNCTION + "test_add_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the simple_pytest.py file. +# └── simple_pytest.py +# └── test_function: success +simple_execution_pytest_expected_output = { + "simple_pytest.py::test_function": { + "test": "simple_pytest.py::test_function", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + +# This is the expected output for the unittest_pytest_same_file.py file. +# ├── unittest_pytest_same_file.py +# ├── TestExample +# │ └── test_true_unittest: success +# └── test_true_pytest: success +unit_pytest_same_file_execution_expected_output = { + "unittest_pytest_same_file.py::TestExample::test_true_unittest": { + "test": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "unittest_pytest_same_file.py::test_true_pytest": { + "test": "unittest_pytest_same_file.py::test_true_pytest", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the dual_level_nested_folder.py tests +# └── dual_level_nested_folder +# └── test_top_folder.py +# └── test_top_function_t: success +# └── test_top_function_f: failure +# └── nested_folder_one +# └── test_bottom_folder.py +# └── test_bottom_function_t: success +# └── test_bottom_function_f: failure +dual_level_nested_folder_execution_expected_output = { + "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the nested_folder tests. +# └── nested_folder_one +# └── nested_folder_two +# └── test_nest.py +# └── test_function: success +double_nested_folder_expected_execution_output = { + "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function": { + "test": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + +# This is the expected output for the nested_folder tests. +# └── parametrize_tests.py +# └── test_adding[3+5-8]: success +# └── test_adding[2+4-6]: success +# └── test_adding[6+9-16]: failure +parametrize_tests_expected_execution_output = { + "parametrize_tests.py::test_adding[3+5-8]": { + "test": "parametrize_tests.py::test_adding[3+5-8]", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "parametrize_tests.py::test_adding[2+4-6]": { + "test": "parametrize_tests.py::test_adding[2+4-6]", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "parametrize_tests.py::test_adding[6+9-16]": { + "test": "parametrize_tests.py::test_adding[6+9-16]", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the single parameterized tests. +# └── parametrize_tests.py +# └── test_adding[3+5-8]: success +single_parametrize_tests_expected_execution_output = { + "parametrize_tests.py::test_adding[3+5-8]": { + "test": "parametrize_tests.py::test_adding[3+5-8]", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the single parameterized tests. +# └── text_docstring.txt +# └── text_docstring: success +doctest_pytest_expected_execution_output = { + "text_docstring.txt::text_docstring.txt": { + "test": "text_docstring.txt::text_docstring.txt", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + +# Will run all tests in the cwd that fit the test file naming pattern. +no_test_ids_pytest_execution_expected_output = { + "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function": { + "test": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers": { + "test": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers": { + "test": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers": { + "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers": { + "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index 8d485456c145..b078439f6eac 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -11,7 +11,7 @@ import subprocess import sys import uuid -from typing import Dict, List, Union +from typing import Any, Dict, List, Optional, Union TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict @@ -83,7 +83,7 @@ def _new_sock() -> socket.socket: ) -def process_rpc_json(data: str) -> Dict[str, str]: +def process_rpc_json(data: str) -> Dict[str, Any]: """Process the JSON data which comes from the server which runs the pytest discovery.""" str_stream: io.StringIO = io.StringIO(data) @@ -107,7 +107,7 @@ def process_rpc_json(data: str) -> Dict[str, str]: return json.loads(raw_json) -def runner(args: List[str]) -> Union[Dict[str, str], None]: +def runner(args: List[str]) -> Optional[Dict[str, Any]]: """Run the pytest discovery and return the JSON data from the server.""" process_args: List[str] = [ sys.executable, diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 57fa9d624bd6..bb6e7255704e 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. import os import shutil -import signal import pytest @@ -31,10 +30,10 @@ def test_syntax_error(tmp_path): shutil.copyfile(file_path, p) actual = runner(["--collect-only", os.fspath(p)]) assert actual - assert all(item in actual for item in ("status", "cwd", "errors")) + assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["errors"]) == 2 + assert len(actual["error"]) == 2 def test_parameterized_error_collect(): @@ -45,10 +44,10 @@ def test_parameterized_error_collect(): file_path_str = "error_parametrize_discovery.py" actual = runner(["--collect-only", file_path_str]) assert actual - assert all(item in actual for item in ("status", "cwd", "errors")) + assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["errors"]) == 2 + assert len(actual["error"]) == 2 @pytest.mark.parametrize( diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py new file mode 100644 index 000000000000..8613deb96098 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -0,0 +1,165 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os +import shutil + +import pytest +from tests.pytestadapter import expected_execution_test_output + +from .helpers import TEST_DATA_PATH, runner + + +def test_syntax_error_execution(tmp_path): + """Test pytest execution on a file that has a syntax error. + + Copies the contents of a .txt file to a .py file in the temporary directory + to then run pytest exeuction on. + + The json should still be returned but the errors list should be present. + + Keyword arguments: + tmp_path -- pytest fixture that creates a temporary directory. + """ + # Saving some files as .txt to avoid that file displaying a syntax error for + # the extension as a whole. Instead, rename it before running this test + # in order to test the error handling. + file_path = TEST_DATA_PATH / "error_syntax_discovery.txt" + temp_dir = tmp_path / "temp_data" + temp_dir.mkdir() + p = temp_dir / "error_syntax_discovery.py" + shutil.copyfile(file_path, p) + actual = runner(["error_syntax_discover.py::test_function"]) + assert actual + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 1 + + +def test_bad_id_error_execution(): + """Test pytest discovery with a non-existent test_id. + + The json should still be returned but the errors list should be present. + """ + actual = runner(["not/a/real::test_id"]) + assert actual + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 1 + + +@pytest.mark.parametrize( + "test_ids, expected_const", + [ + ( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + ], + expected_execution_test_output.uf_execution_expected_output, + ), + ( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + ], + expected_execution_test_output.uf_single_file_expected_output, + ), + ( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + ], + expected_execution_test_output.uf_single_method_execution_expected_output, + ), + ( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + ], + expected_execution_test_output.uf_non_adjacent_tests_execution_expected_output, + ), + ( + [ + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "unittest_pytest_same_file.py::test_true_pytest", + ], + expected_execution_test_output.unit_pytest_same_file_execution_expected_output, + ), + ( + [ + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + ], + expected_execution_test_output.dual_level_nested_folder_execution_expected_output, + ), + ( + [ + "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function" + ], + expected_execution_test_output.double_nested_folder_expected_execution_output, + ), + ( + [ + "parametrize_tests.py::test_adding[3+5-8]", + "parametrize_tests.py::test_adding[2+4-6]", + "parametrize_tests.py::test_adding[6+9-16]", + ], + expected_execution_test_output.parametrize_tests_expected_execution_output, + ), + ( + [ + "parametrize_tests.py::test_adding[3+5-8]", + ], + expected_execution_test_output.single_parametrize_tests_expected_execution_output, + ), + ( + [ + "text_docstring.txt::text_docstring.txt", + ], + expected_execution_test_output.doctest_pytest_expected_execution_output, + ), + ( + [ + "", + ], + expected_execution_test_output.no_test_ids_pytest_execution_expected_output, + ), + ], +) +def test_pytest_execution(test_ids, expected_const): + """ + Test that pytest discovery works as expected where run pytest is always successful + but the actual test results are both successes and failures.: + 1. uf_execution_expected_output: unittest tests run on multiple files. + 2. uf_single_file_expected_output: test run on a single file. + 3. uf_single_method_execution_expected_output: test run on a single method in a file. + 4. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. + 5. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. + 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file at the top level and one test file in a nested folder. + 7. double_nested_folder_expected_execution_output: test run on a double nested folder. + 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. + 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. + 10. doctest_pytest_expected_execution_output: test run on doctest file. + 11. no_test_ids_pytest_execution_expected_output: test run with no inputted test ids. + + + Keyword arguments: + test_ids -- an array of test_ids to run. + expected_const -- a dictionary of the expected output from running pytest discovery on the files. + """ + args = test_ids + actual = runner(args) + assert actual + assert all(item in actual for item in ("status", "cwd", "result")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + result_data = actual["result"] + for key in result_data: + if result_data[key]["outcome"] == "failure": + result_data[key]["message"] = "ERROR MESSAGE" + assert result_data == expected_const diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 22acaab57953..6063e4113d55 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -69,7 +69,8 @@ def pytest_exception_interact(node, call, report): """ # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. - ERRORS.append(call.excinfo.exconly()) + if call.excinfo and call.excinfo.typename != "AssertionError": + ERRORS.append(call.excinfo.exconly()) def pytest_keyboard_interrupt(excinfo): @@ -82,34 +83,138 @@ def pytest_keyboard_interrupt(excinfo): ERRORS.append(excinfo.exconly()) +class TestOutcome(Dict): + """A class that handles outcome for a single test. + + for pytest the outcome for a test is only 'passed', 'skipped' or 'failed' + """ + + test: str + outcome: Literal["success", "failure", "skipped"] + message: Union[str, None] + traceback: Union[str, None] + subtest: Optional[str] + + +def create_test_outcome( + test: str, + outcome: str, + message: Union[str, None], + traceback: Union[str, None], + subtype: Optional[str] = None, +) -> TestOutcome: + """A function that creates a TestOutcome object.""" + return TestOutcome( + test=test, + outcome=outcome, + message=message, + traceback=traceback, # TODO: traceback + subtest=None, + ) + + +class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): + """A class that stores all test run results.""" + + outcome: str + tests: Dict[str, TestOutcome] + + +collected_tests = testRunResultDict() +IS_DISCOVERY = False + + +def pytest_load_initial_conftests(early_config, parser, args): + if "--collect-only" in args: + global IS_DISCOVERY + IS_DISCOVERY = True + + +def pytest_report_teststatus(report, config): + """ + A pytest hook that is called when a test is called. It is called 3 times per test, + during setup, call, and teardown. + Keyword arguments: + report -- the report on the test setup, call, and teardown. + config -- configuration object. + """ + + if report.when == "call": + traceback = None + message = None + report_value = "skipped" + if report.passed: + report_value = "success" + elif report.failed: + report_value = "failure" + message = report.longreprtext + item_result = create_test_outcome( + report.nodeid, + report_value, + message, + traceback, + ) + collected_tests[report.nodeid] = item_result + + +ERROR_MESSAGE_CONST = { + 2: "Pytest was unable to start or run any tests due to issues with test discovery or test collection.", + 3: "Pytest was interrupted by the user, for example by pressing Ctrl+C during test execution.", + 4: "Pytest encountered an internal error or exception during test execution.", + 5: "Pytest was unable to find any tests to run.", +} + + def pytest_sessionfinish(session, exitstatus): """A pytest hook that is called after pytest has fulled finished. Keyword arguments: session -- the pytest session object. exitstatus -- the status code of the session. + + 0: All tests passed successfully. + 1: One or more tests failed. + 2: Pytest was unable to start or run any tests due to issues with test discovery or test collection. + 3: Pytest was interrupted by the user, for example by pressing Ctrl+C during test execution. + 4: Pytest encountered an internal error or exception during test execution. + 5: Pytest was unable to find any tests to run. """ cwd = pathlib.Path.cwd() - try: - session_node: Union[TestNode, None] = build_test_tree(session) - if not session_node: - raise VSCodePytestError( - "Something went wrong following pytest finish, \ - no session node was created" + if IS_DISCOVERY: + try: + session_node: Union[TestNode, None] = build_test_tree(session) + if not session_node: + raise VSCodePytestError( + "Something went wrong following pytest finish, \ + no session node was created" + ) + post_response(os.fsdecode(cwd), session_node) + except Exception as e: + ERRORS.append( + f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" ) - post_response(os.fsdecode(cwd), session_node) - except Exception as e: - ERRORS.append( - f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" + errorNode: TestNode = { + "name": "", + "path": "", + "type_": "error", + "children": [], + "id_": "", + } + post_response(os.fsdecode(cwd), errorNode) + else: + if exitstatus == 0 or exitstatus == 1: + exitstatus_bool = "success" + else: + ERRORS.append( + f"Pytest exited with error status: {exitstatus}, {ERROR_MESSAGE_CONST[exitstatus]}" + ) + exitstatus_bool = "error" + + execution_post( + os.fsdecode(cwd), + exitstatus_bool, + collected_tests if collected_tests else None, ) - errorNode: TestNode = { - "name": "", - "path": "", - "type_": "error", - "children": [], - "id_": "", - } - post_response(os.fsdecode(cwd), errorNode) def build_test_tree(session: pytest.Session) -> TestNode: @@ -284,13 +389,67 @@ def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode } -class PayloadDict(TypedDict): +class DiscoveryPayloadDict(TypedDict): """A dictionary that is used to send a post request to the server.""" cwd: str status: Literal["success", "error"] tests: Optional[TestNode] - errors: Optional[List[str]] + error: Optional[List[str]] + + +class ExecutionPayloadDict(Dict): + """ + A dictionary that is used to send a execution post request to the server. + """ + + cwd: str + status: Literal["success", "error"] + result: Union[testRunResultDict, None] + not_found: Union[List[str], None] # Currently unused need to check + error: Union[str, None] # Currently unused need to check + + +def execution_post( + cwd: str, + status: Literal["success", "error"], + tests: Union[testRunResultDict, None], +): + """ + Sends a post request to the server after the tests have been executed. + Keyword arguments: + cwd -- the current working directory. + session_node -- the status of running the tests + tests -- the tests that were run and their status. + """ + testPort = os.getenv("TEST_PORT", 45454) + testuuid = os.getenv("TEST_UUID") + payload: ExecutionPayloadDict = ExecutionPayloadDict( + cwd=cwd, status=status, result=tests, not_found=None, error=None + ) + if ERRORS: + payload["error"] = ERRORS + + addr = ("localhost", int(testPort)) + data = json.dumps(payload) + request = f"""Content-Length: {len(data)} +Content-Type: application/json +Request-uuid: {testuuid} + +{data}""" + test_output_file: Optional[str] = os.getenv("TEST_OUTPUT_FILE", None) + if test_output_file == "stdout": + print(request) + elif test_output_file: + pathlib.Path(test_output_file).write_text(request, encoding="utf-8") + else: + try: + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) + except Exception as e: + print(f"Plugin error connection error[vscode-pytest]: {e}") + print(f"[vscode-pytest] data: {request}") def post_response(cwd: str, session_node: TestNode) -> None: @@ -301,15 +460,14 @@ def post_response(cwd: str, session_node: TestNode) -> None: session_node -- the session node, which is the top of the testing tree. errors -- a list of errors that occurred during test collection. """ - payload: PayloadDict = { + payload: DiscoveryPayloadDict = { "cwd": cwd, "status": "success" if not ERRORS else "error", "tests": session_node, - "errors": [], + "error": [], } - if ERRORS: - payload["errors"] = ERRORS - + if ERRORS is not None: + payload["error"] = ERRORS testPort: Union[str, int] = os.getenv("TEST_PORT", 45454) testuuid: Union[str, None] = os.getenv("TEST_UUID") addr = "localhost", int(testPort) From 7ddfd9f346cf8f51ed4dfa3effc910727a03ac66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Pi=C3=B1a=20Martinez?= Date: Tue, 18 Apr 2023 19:08:42 +0200 Subject: [PATCH 054/336] Add black to `extensions.json` (#20912) Solves #20855 --- .vscode/extensions.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 045d61d31678..5ade8dec4885 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,6 +6,7 @@ "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "ms-python.python", + "ms-python.black-formatter", "ms-python.vscode-pylance" ] } From d23afe8b20c81c962a5292be1e6deceaa8fc6b4c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 19 Apr 2023 08:38:32 -0700 Subject: [PATCH 055/336] Add unittest subtest (#21081) closes https://github.com/microsoft/vscode-python/issues/21038 also updates the subtests names --------- Co-authored-by: Kartik Raj --- .../unittestadapter/.data/test_subtest.py | 18 +++++ .../tests/unittestadapter/test_execution.py | 29 +++++++ pythonFiles/unittestadapter/execution.py | 5 +- .../testController/workspaceTestAdapter.ts | 77 ++++++++++++++++++- 4 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 pythonFiles/tests/unittestadapter/.data/test_subtest.py diff --git a/pythonFiles/tests/unittestadapter/.data/test_subtest.py b/pythonFiles/tests/unittestadapter/.data/test_subtest.py new file mode 100644 index 000000000000..b913b8773701 --- /dev/null +++ b/pythonFiles/tests/unittestadapter/.data/test_subtest.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + +# Test class for the test_subtest_run test. +# The test_failed_tests function should return a dictionary that has a "success" status +# and the "result" value is a dict with 6 entries, one for each subtest. + + +class NumbersTest(unittest.TestCase): + def test_even(self): + """ + Test that numbers between 0 and 5 are all even. + """ + for i in range(0, 6): + with self.subTest(i=i): + self.assertEqual(i % 2, 0) diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index 5353703c59f2..7f58049a56b7 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -94,6 +94,35 @@ def test_single_ids_run() -> None: assert id_result["outcome"] == "success" +def test_subtest_run() -> None: + """This test runs on a the test_subtest which has a single method, test_even, + that uses unittest subtest. + + The actual result of run should return a dict payload with 6 entry for the 6 subtests. + """ + id = "test_subtest.NumbersTest.test_even" + actual = run_tests( + os.fspath(TEST_DATA_PATH), [id], "test_subtest.py", None, "fake-uuid" + ) + subtests_ids = [ + "test_subtest.NumbersTest.test_even (i=0)", + "test_subtest.NumbersTest.test_even (i=1)", + "test_subtest.NumbersTest.test_even (i=2)", + "test_subtest.NumbersTest.test_even (i=3)", + "test_subtest.NumbersTest.test_even (i=4)", + "test_subtest.NumbersTest.test_even (i=5)", + ] + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert "result" in actual + result = actual["result"] + assert len(result) == 6 + for id in subtests_ids: + assert id in result + + @pytest.mark.parametrize( "test_ids, pattern, cwd, expected_outcome", [ diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 5acb11b2609f..37288651f531 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -130,7 +130,10 @@ def formatResult( formatted = formatted[1:] tb = "".join(formatted) - test_id = test.id() + if subtest: + test_id = subtest.id() + else: + test_id = test.id() result = { "test": test.id(), diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index dc9c65c431fd..8ce51e5dab56 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -22,6 +22,7 @@ import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { TestProvider } from '../types'; import { + clearAllChildren, createErrorTestItem, DebugTestTag, ErrorTestItemOptions, @@ -135,8 +136,11 @@ export class WorkspaceTestAdapter { } if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { + // Map which holds the subtest information for each test item. + const subTestStats: Map = new Map(); + + // iterate through payload and update the UI accordingly. for (const keyTemp of Object.keys(rawTestExecData.result)) { - // check for result and update the UI accordingly. const testCases: TestItem[] = []; // grab leaf level test items @@ -147,7 +151,6 @@ export class WorkspaceTestAdapter { if ( rawTestExecData.result[keyTemp].outcome === 'failure' || - rawTestExecData.result[keyTemp].outcome === 'subtest-failure' || rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' ) { const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; @@ -175,8 +178,7 @@ export class WorkspaceTestAdapter { }); } else if ( rawTestExecData.result[keyTemp].outcome === 'success' || - rawTestExecData.result[keyTemp].outcome === 'expected-failure' || - rawTestExecData.result[keyTemp].outcome === 'subtest-passed' + rawTestExecData.result[keyTemp].outcome === 'expected-failure' ) { const grabTestItem = this.runIdToTestItem.get(keyTemp); const grabVSid = this.runIdToVSid.get(keyTemp); @@ -203,6 +205,73 @@ export class WorkspaceTestAdapter { } }); } + } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { + // split on " " since the subtest ID has the parent test ID in the first part of the ID. + const parentTestCaseId = keyTemp.split(' ')[0]; + const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); + const data = rawTestExecData.result[keyTemp]; + // find the subtest's parent test item + if (parentTestItem) { + const subtestStats = subTestStats.get(parentTestCaseId); + if (subtestStats) { + subtestStats.failed += 1; + } else { + subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); + runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); + // clear since subtest items don't persist between runs + clearAllChildren(parentTestItem); + } + const subtestId = keyTemp; + const subTestItem = testController?.createTestItem(subtestId, subtestId); + runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); + // create a new test item for the subtest + if (subTestItem) { + const traceback = data.traceback ?? ''; + const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; + runInstance.appendOutput(fixLogLines(text)); + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + const message = new TestMessage(rawTestExecData?.result[keyTemp].message ?? ''); + if (parentTestItem.uri && parentTestItem.range) { + message.location = new Location(parentTestItem.uri, parentTestItem.range); + } + runInstance.failed(subTestItem, message); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { + // split on " " since the subtest ID has the parent test ID in the first part of the ID. + const parentTestCaseId = keyTemp.split(' ')[0]; + const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); + + // find the subtest's parent test item + if (parentTestItem) { + const subtestStats = subTestStats.get(parentTestCaseId); + if (subtestStats) { + subtestStats.passed += 1; + } else { + subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); + runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); + // clear since subtest items don't persist between runs + clearAllChildren(parentTestItem); + } + const subtestId = keyTemp; + const subTestItem = testController?.createTestItem(subtestId, subtestId); + // create a new test item for the subtest + if (subTestItem) { + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + runInstance.passed(subTestItem); + runInstance.appendOutput(fixLogLines(`${subtestId} Passed\r\n`)); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } } } } From a009edbda761ca23c460d4f16eeeabb6a8b00e72 Mon Sep 17 00:00:00 2001 From: Aidos Kanapyanov <65722512+aidoskanapyanov@users.noreply.github.com> Date: Wed, 19 Apr 2023 22:28:46 +0600 Subject: [PATCH 056/336] Default to `--no-cov` when debugging pytest (#21048) Resolve #19985. --- src/client/testing/testController/pytest/runner.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/testing/testController/pytest/runner.ts b/src/client/testing/testController/pytest/runner.ts index 2c6cff724398..83da819b861a 100644 --- a/src/client/testing/testController/pytest/runner.ts +++ b/src/client/testing/testController/pytest/runner.ts @@ -95,6 +95,10 @@ export class PytestRunner implements ITestsRunner { testArgs.push('--capture', 'no'); } + if (options.debug && !testArgs.some((a) => a.startsWith('--no-cov'))) { + testArgs.push('--no-cov'); + } + // Positional arguments control the tests to be run. const rawData = idToRawData.get(testNode.id); if (!rawData) { From 05cb25549c7a2ef74e7fd8c542af0eaf3946ba3d Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 19 Apr 2023 11:10:21 -0700 Subject: [PATCH 057/336] Create environment proposed API (#21074) --- src/client/environmentApi.ts | 2 + src/client/proposedApiTypes.ts | 6 +- .../creation/createEnvApi.ts | 51 +++++-- .../creation/createEnvironment.ts | 36 ++--- .../creation/proposed.createEnvApis.ts | 143 ++++++++++++++++++ .../provider/condaCreationProvider.ts | 16 +- .../creation/provider/venvCreationProvider.ts | 16 +- .../pythonEnvironments/creation/types.ts | 48 +----- .../creation/createEnvApi.unit.test.ts | 11 +- .../creation/createEnvironment.unit.test.ts | 16 +- .../condaCreationProvider.unit.test.ts | 17 ++- .../venvCreationProvider.unit.test.ts | 17 ++- 12 files changed, 270 insertions(+), 109 deletions(-) create mode 100644 src/client/pythonEnvironments/creation/proposed.createEnvApis.ts diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts index 533d187ca520..3e846eb2772f 100644 --- a/src/client/environmentApi.ts +++ b/src/client/environmentApi.ts @@ -31,6 +31,7 @@ import { ResolvedEnvironment, Resource, } from './apiTypes'; +import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi'; type ActiveEnvironmentChangeEvent = { resource: WorkspaceFolder | undefined; @@ -253,6 +254,7 @@ export function buildEnvironmentApi( sendApiTelemetry('onDidChangeEnvironments'); return onEnvironmentsChanged.event; }, + ...buildEnvironmentCreationApi(), }; return environmentApi; } diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 1b772b406644..13ad5af543ec 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -1,4 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -export interface ProposedExtensionAPI {} +export interface ProposedExtensionAPI { + /** + * Top level proposed APIs should go here. + */ +} diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts index cfbf81909d59..6445e0938025 100644 --- a/src/client/pythonEnvironments/creation/createEnvApi.ts +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -9,14 +9,15 @@ import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; import { condaCreationProvider } from './provider/condaCreationProvider'; import { VenvCreationProvider } from './provider/venvCreationProvider'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { CreateEnv } from '../../common/utils/localize'; import { - CreateEnvironmentExitedEventArgs, - CreateEnvironmentOptions, CreateEnvironmentProvider, + CreateEnvironmentOptions, CreateEnvironmentResult, -} from './types'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { CreateEnv } from '../../common/utils/localize'; + ProposedCreateEnvironmentAPI, + EnvironmentDidCreateEvent, +} from './proposed.createEnvApis'; class CreateEnvironmentProviders { private _createEnvProviders: CreateEnvironmentProvider[] = []; @@ -26,6 +27,9 @@ class CreateEnvironmentProviders { } public add(provider: CreateEnvironmentProvider) { + if (this._createEnvProviders.filter((p) => p.id === provider.id).length > 0) { + throw new Error(`Create Environment provider with id ${provider.id} already registered`); + } this._createEnvProviders.push(provider); } @@ -63,15 +67,36 @@ export function registerCreateEnvironmentFeatures( return handleCreateEnvironmentCommand(providers, options); }, ), - ); - disposables.push(registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick))); - disposables.push(registerCreateEnvironmentProvider(condaCreationProvider())); - disposables.push( - onCreateEnvironmentExited(async (e: CreateEnvironmentExitedEventArgs) => { - if (e.result?.path && e.options?.selectEnvironment) { - await interpreterPathService.update(e.result.uri, ConfigurationTarget.WorkspaceFolder, e.result.path); - showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.result.path)}`); + registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick)), + registerCreateEnvironmentProvider(condaCreationProvider()), + onCreateEnvironmentExited(async (e: EnvironmentDidCreateEvent) => { + if (e.path && e.options?.selectEnvironment) { + await interpreterPathService.update( + e.workspaceFolder?.uri, + ConfigurationTarget.WorkspaceFolder, + e.path, + ); + showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.path)}`); } }), ); } + +export function buildEnvironmentCreationApi(): ProposedCreateEnvironmentAPI { + return { + onWillCreateEnvironment: onCreateEnvironmentStarted, + onDidCreateEnvironment: onCreateEnvironmentExited, + createEnvironment: async ( + options?: CreateEnvironmentOptions | undefined, + ): Promise => { + const providers = _createEnvironmentProviders.getAll(); + try { + return await handleCreateEnvironmentCommand(providers, options); + } catch (err) { + return { path: undefined, workspaceFolder: undefined, action: undefined, error: err as Error }; + } + }, + registerCreateEnvironmentProvider: (provider: CreateEnvironmentProvider) => + registerCreateEnvironmentProvider(provider), + }; +} diff --git a/src/client/pythonEnvironments/creation/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts index bdeaf89ba82d..4593ff1abf92 100644 --- a/src/client/pythonEnvironments/creation/createEnvironment.ts +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -11,15 +11,15 @@ import { } from '../../common/vscodeApis/windowApis'; import { traceError, traceVerbose } from '../../logging'; import { - CreateEnvironmentExitedEventArgs, CreateEnvironmentOptions, - CreateEnvironmentProvider, CreateEnvironmentResult, - CreateEnvironmentStartedEventArgs, -} from './types'; + CreateEnvironmentProvider, + EnvironmentWillCreateEvent, + EnvironmentDidCreateEvent, +} from './proposed.createEnvApis'; -const onCreateEnvironmentStartedEvent = new EventEmitter(); -const onCreateEnvironmentExitedEvent = new EventEmitter(); +const onCreateEnvironmentStartedEvent = new EventEmitter(); +const onCreateEnvironmentExitedEvent = new EventEmitter(); let startedEventCount = 0; @@ -32,14 +32,20 @@ function fireStartedEvent(options?: CreateEnvironmentOptions): void { startedEventCount += 1; } -function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: unknown): void { - onCreateEnvironmentExitedEvent.fire({ result, options, error }); +function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: Error): void { + onCreateEnvironmentExitedEvent.fire({ + options, + workspaceFolder: result?.workspaceFolder, + path: result?.path, + action: result?.action, + error: error || result?.error, + }); startedEventCount -= 1; } export function getCreationEvents(): { - onCreateEnvironmentStarted: Event; - onCreateEnvironmentExited: Event; + onCreateEnvironmentStarted: Event; + onCreateEnvironmentExited: Event; isCreatingEnvironment: () => boolean; } { return { @@ -54,7 +60,7 @@ async function createEnvironment( options: CreateEnvironmentOptions, ): Promise { let result: CreateEnvironmentResult | undefined; - let err: unknown | undefined; + let err: Error | undefined; try { fireStartedEvent(options); result = await provider.createEnvironment(options); @@ -65,7 +71,7 @@ async function createEnvironment( return undefined; } } - err = ex; + err = ex as Error; throw err; } finally { fireExitedEvent(result, options, err); @@ -185,11 +191,7 @@ export async function handleCreateEnvironmentCommand( const action = await MultiStepNode.run(envTypeStep); if (options?.showBackButton) { if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) { - result = { - path: result?.path, - uri: result?.uri, - action: action === MultiStepAction.Back ? 'Back' : 'Cancel', - }; + result = { action, workspaceFolder: undefined, path: undefined, error: undefined }; } } diff --git a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts new file mode 100644 index 000000000000..52209a5a31d0 --- /dev/null +++ b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Event, Disposable, WorkspaceFolder } from 'vscode'; +import { EnvironmentTools } from '../../apiTypes'; + +export type CreateEnvironmentUserActions = 'Back' | 'Cancel'; +export type EnvironmentProviderId = string; + +/** + * Options used when creating a Python environment. + */ +export interface CreateEnvironmentOptions { + /** + * Default `true`. If `true`, the environment creation handler is expected to install packages. + */ + installPackages?: boolean; + + /** + * Default `true`. If `true`, the environment creation provider is expected to add the environment to ignore list + * for the source control. + */ + ignoreSourceControl?: boolean; + + /** + * Default `false`. If `true` the creation provider should show back button when showing QuickPick or QuickInput. + */ + showBackButton?: boolean; + + /** + * Default `true`. If `true`, the environment after creation will be selected. + */ + selectEnvironment?: boolean; +} + +/** + * Params passed on `onWillCreateEnvironment` event handler. + */ +export interface EnvironmentWillCreateEvent { + /** + * Options used to create a Python environment. + */ + options: CreateEnvironmentOptions | undefined; +} + +/** + * Params passed on `onDidCreateEnvironment` event handler. + */ +export interface EnvironmentDidCreateEvent extends CreateEnvironmentResult { + /** + * Options used to create the Python environment. + */ + options: CreateEnvironmentOptions | undefined; +} + +export interface CreateEnvironmentResult { + /** + * Workspace folder associated with the environment. + */ + workspaceFolder: WorkspaceFolder | undefined; + + /** + * Path to the executable python in the environment + */ + path: string | undefined; + + /** + * User action that resulted in exit from the create environment flow. + */ + action: CreateEnvironmentUserActions | undefined; + + /** + * Error if any occurred during environment creation. + */ + error: Error | undefined; +} + +/** + * Extensions that want to contribute their own environment creation can do that by registering an object + * that implements this interface. + */ +export interface CreateEnvironmentProvider { + /** + * This API is called when user selects this provider from a QuickPick to select the type of environment + * user wants. This API is expected to show a QuickPick or QuickInput to get the user input and return + * the path to the Python executable in the environment. + * + * @param {CreateEnvironmentOptions} [options] Options used to create a Python environment. + * + * @returns a promise that resolves to the path to the + * Python executable in the environment. Or any action taken by the user, such as back or cancel. + */ + createEnvironment(options?: CreateEnvironmentOptions): Promise; + + /** + * Unique ID for the creation provider, typically : + */ + id: EnvironmentProviderId; + + /** + * Display name for the creation provider. + */ + name: string; + + /** + * Description displayed to the user in the QuickPick to select environment provider. + */ + description: string; + + /** + * Tools used to manage this environment. e.g., ['conda']. In the most to least priority order + * for resolving and working with the environment. + */ + tools: EnvironmentTools[]; +} + +export interface ProposedCreateEnvironmentAPI { + /** + * This API can be used to detect when the environment creation starts for any registered + * provider (including internal providers). This will also receive any options passed in + * or defaults used to create environment. + */ + onWillCreateEnvironment: Event; + + /** + * This API can be used to detect when the environment provider exits for any registered + * provider (including internal providers). This will also receive created environment path, + * any errors, or user actions taken from the provider. + */ + onDidCreateEnvironment: Event; + + /** + * This API will show a QuickPick to select an environment provider from available list of + * providers. Based on the selection the `createEnvironment` will be called on the provider. + */ + createEnvironment(options?: CreateEnvironmentOptions): Promise; + + /** + * This API should be called to register an environment creation provider. It returns + * a (@link Disposable} which can be used to remove the registration. + */ + registerCreateEnvironmentProvider(provider: CreateEnvironmentProvider): Disposable; +} diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 5bf032f9f65f..39cd40afd41a 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -5,12 +5,7 @@ import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; import * as path from 'path'; import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; import { traceError, traceLog } from '../../../logging'; -import { - CreateEnvironmentOptions, - CreateEnvironmentProgress, - CreateEnvironmentProvider, - CreateEnvironmentResult, -} from '../types'; +import { CreateEnvironmentProgress } from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; @@ -28,6 +23,11 @@ import { CONDA_ENV_EXISTING_MARKER, } from './condaProgressAndTelemetry'; import { splitLines } from '../../../common/stringUtils'; +import { + CreateEnvironmentOptions, + CreateEnvironmentResult, + CreateEnvironmentProvider, +} from '../proposed.createEnvApis'; function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] { let addGitIgnore = true; @@ -247,7 +247,7 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise {} - -export interface CreateEnvironmentOptions { - /** - * Default `true`. If `true`, the environment creation handler is expected to install packages. - */ - installPackages?: boolean; - - /** - * Default `true`. If `true`, the environment creation provider is expected to add the environment to ignore list - * for the source control. - */ - ignoreSourceControl?: boolean; - - /** - * Default `false`. If `true` the creation provider should show back button when showing QuickPick or QuickInput. - */ - showBackButton?: boolean; - - /** - * Default `true`. If `true`, the environment will be selected as the environment to be used for the workspace. - */ - selectEnvironment?: boolean; -} - -export interface CreateEnvironmentResult { - path: string | undefined; - uri: Uri | undefined; - action?: 'Back' | 'Cancel'; -} - -export interface CreateEnvironmentStartedEventArgs { - options: CreateEnvironmentOptions | undefined; -} - -export interface CreateEnvironmentExitedEventArgs { - result: CreateEnvironmentResult | undefined; - error?: unknown; - options: CreateEnvironmentOptions | undefined; -} - -export interface CreateEnvironmentProvider { - createEnvironment(options?: CreateEnvironmentOptions): Promise; - name: string; - description: string; - id: string; -} diff --git a/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts index 1286ac44d58d..786bd26a881c 100644 --- a/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts +++ b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts @@ -12,8 +12,8 @@ import * as commandApis from '../../../client/common/vscodeApis/commandApis'; import { IInterpreterQuickPick } from '../../../client/interpreter/configuration/types'; import { registerCreateEnvironmentFeatures } from '../../../client/pythonEnvironments/creation/createEnvApi'; import * as windowApis from '../../../client/common/vscodeApis/windowApis'; -import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/types'; import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvironment'; +import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/proposed.createEnvApis'; chaiUse(chaiAsPromised); @@ -57,6 +57,11 @@ suite('Create Environment APIs', () => { [true, false].forEach((selectEnvironment) => { test(`Set environment selectEnvironment == ${selectEnvironment}`, async () => { + const workspace1 = { + uri: Uri.file('/path/to/env'), + name: 'workspace1', + index: 0, + }; const provider = typemoq.Mock.ofType(); provider.setup((p) => p.name).returns(() => 'test'); provider.setup((p) => p.id).returns(() => 'test-id'); @@ -66,7 +71,9 @@ suite('Create Environment APIs', () => { .returns(() => Promise.resolve({ path: '/path/to/env', - uri: Uri.file('/path/to/env'), + workspaceFolder: workspace1, + action: undefined, + error: undefined, }), ); provider.setup((p) => (p as any).then).returns(() => undefined); diff --git a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts index 507a2aee88cd..f16f81233369 100644 --- a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts +++ b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts @@ -8,9 +8,9 @@ import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; import * as windowApis from '../../../client/common/vscodeApis/windowApis'; import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvironment'; -import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/types'; import { IDisposableRegistry } from '../../../client/common/types'; import { onCreateEnvironmentStarted } from '../../../client/pythonEnvironments/creation/createEnvApi'; +import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/proposed.createEnvApis'; chaiUse(chaiAsPromised); @@ -233,7 +233,12 @@ suite('Create Environments Tests', () => { showBackButton: true, }); - assert.deepStrictEqual(result, { path: undefined, uri: undefined, action: 'Back' }); + assert.deepStrictEqual(result, { + action: 'Back', + workspaceFolder: undefined, + path: undefined, + error: undefined, + }); assert.isTrue(showQuickPickStub.notCalled); assert.isTrue(showQuickPickWithBackStub.calledOnce); }); @@ -259,7 +264,12 @@ suite('Create Environments Tests', () => { showBackButton: true, }); - assert.deepStrictEqual(result, { path: undefined, uri: undefined, action: 'Cancel' }); + assert.deepStrictEqual(result, { + action: 'Cancel', + workspaceFolder: undefined, + path: undefined, + error: undefined, + }); assert.isTrue(showQuickPickStub.notCalled); assert.isTrue(showQuickPickWithBackStub.calledOnce); }); diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index db5eb351211e..cb4df95c8c1f 100644 --- a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -7,11 +7,7 @@ import { assert, use as chaiUse } from 'chai'; import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { CancellationToken, ProgressOptions, Uri } from 'vscode'; -import { - CreateEnvironmentProgress, - CreateEnvironmentProvider, - CreateEnvironmentResult, -} from '../../../../client/pythonEnvironments/creation/types'; +import { CreateEnvironmentProgress } from '../../../../client/pythonEnvironments/creation/types'; import { condaCreationProvider } from '../../../../client/pythonEnvironments/creation/provider/condaCreationProvider'; import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; @@ -23,6 +19,10 @@ import { createDeferred } from '../../../../client/common/utils/async'; import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; import { CONDA_ENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/condaProgressAndTelemetry'; import { CreateEnv } from '../../../../client/common/utils/localize'; +import { + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../../../../client/pythonEnvironments/creation/proposed.createEnvApis'; chaiUse(chaiAsPromised); @@ -131,7 +131,12 @@ suite('Conda Creation provider tests', () => { _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); _complete!(); - assert.deepStrictEqual(await promise, { path: 'new_environment', uri: workspace1.uri }); + assert.deepStrictEqual(await promise, { + path: 'new_environment', + workspaceFolder: workspace1, + action: undefined, + error: undefined, + }); assert.isTrue(showErrorMessageWithLogsStub.notCalled); }); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index b56fb158d6ae..1c22264f2ada 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -6,11 +6,7 @@ import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; import * as sinon from 'sinon'; import { CancellationToken, ProgressOptions, Uri } from 'vscode'; -import { - CreateEnvironmentProgress, - CreateEnvironmentProvider, - CreateEnvironmentResult, -} from '../../../../client/pythonEnvironments/creation/types'; +import { CreateEnvironmentProgress } from '../../../../client/pythonEnvironments/creation/types'; import { VenvCreationProvider } from '../../../../client/pythonEnvironments/creation/provider/venvCreationProvider'; import { IInterpreterQuickPick } from '../../../../client/interpreter/configuration/types'; import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; @@ -23,6 +19,10 @@ import { Output } from '../../../../client/common/process/types'; import { VENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; import { CreateEnv } from '../../../../client/common/utils/localize'; import * as venvUtils from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; +import { + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../../../../client/pythonEnvironments/creation/proposed.createEnvApis'; chaiUse(chaiAsPromised); @@ -155,7 +155,12 @@ suite('venv Creation provider tests', () => { _complete!(); const actual = await promise; - assert.deepStrictEqual(actual, { path: 'new_environment', uri: workspace1.uri }); + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + action: undefined, + error: undefined, + }); interpreterQuickPick.verifyAll(); progressMock.verifyAll(); assert.isTrue(showErrorMessageWithLogsStub.notCalled); From 9bb5a4466ea112f4a84c9e4c36e14ffbaaffb7ec Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 19 Apr 2023 16:11:46 -0700 Subject: [PATCH 058/336] Deprecate python logging level setting (#21083) Deprecates the logging level setting. The default for logging level from VS Code is Info, so in this PR moved a few items to verbose so that we get the same logging experience. Closes https://github.com/microsoft/vscode-python/issues/20844 --- package.json | 1 + package.nls.json | 1 + .../diagnostics/applicationDiagnostics.ts | 4 +- .../common/process/pythonEnvironment.ts | 8 ++-- src/client/extensionActivation.ts | 7 ---- src/client/interpreter/activation/service.ts | 11 +----- src/client/logging/index.ts | 37 ++++--------------- src/client/logging/settings.ts | 12 ------ src/client/logging/types.ts | 14 +++---- .../base/info/environmentInfoService.ts | 4 +- .../base/info/interpreter.ts | 4 +- .../locators/composite/envsCollectionCache.ts | 4 +- .../creation/provider/venvCreationProvider.ts | 4 +- .../creation/provider/venvUtils.ts | 6 +-- .../pythonEnvironments/info/interpreter.ts | 4 +- src/client/tensorBoard/tensorBoardSession.ts | 14 +++---- .../tensorBoard/tensorBoardSessionProvider.ts | 4 +- .../common/testConfigurationManager.ts | 4 +- .../testing/testController/unittest/runner.ts | 4 +- src/test/testLogger.ts | 3 +- 20 files changed, 52 insertions(+), 98 deletions(-) delete mode 100644 src/client/logging/settings.ts diff --git a/package.json b/package.json index 60c80693f36e..e0b418a4cd9b 100644 --- a/package.json +++ b/package.json @@ -922,6 +922,7 @@ }, "python.logging.level": { "default": "error", + "deprecationMessage": "%python.logging.level.deprecation%", "description": "%python.logging.level.description%", "enum": [ "debug", diff --git a/package.nls.json b/package.nls.json index 0bf0ac2a8426..cfbbeb7d41d5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -100,6 +100,7 @@ "python.linting.pylintEnabled.description": "Whether to lint Python files using pylint.", "python.linting.pylintPath.description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", "python.logging.level.description": "The logging level the extension logs at, defaults to 'error'", + "python.logging.level.deprecation": "This setting is deprecated. Please use command `Developer: Set Log Level...` to set logging level.", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", "python.sortImports.args.description": "Arguments passed in. Each argument is a separate item in the array.", diff --git a/src/client/application/diagnostics/applicationDiagnostics.ts b/src/client/application/diagnostics/applicationDiagnostics.ts index ba31021fc34f..493c6cfece53 100644 --- a/src/client/application/diagnostics/applicationDiagnostics.ts +++ b/src/client/application/diagnostics/applicationDiagnostics.ts @@ -7,7 +7,7 @@ import { IWorkspaceService } from '../../common/application/types'; import { isTestExecution } from '../../common/constants'; import { Resource } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; -import { traceInfo, traceLog } from '../../logging'; +import { traceLog, traceVerbose } from '../../logging'; import { IApplicationDiagnostics } from '../types'; import { IDiagnostic, IDiagnosticsService, ISourceMapSupportService } from './types'; @@ -21,7 +21,7 @@ function log(diagnostics: IDiagnostic[]): void { break; } default: { - traceInfo(message); + traceVerbose(message); } } }); diff --git a/src/client/common/process/pythonEnvironment.ts b/src/client/common/process/pythonEnvironment.ts index f4c6f8954126..9566f373aa91 100644 --- a/src/client/common/process/pythonEnvironment.ts +++ b/src/client/common/process/pythonEnvironment.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import * as path from 'path'; -import { traceError, traceInfo } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; import { Conda, CondaEnvironmentInfo } from '../../pythonEnvironments/common/environmentManagers/conda'; import { buildPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/exec'; import { InterpreterInformation } from '../../pythonEnvironments/info'; @@ -71,7 +71,7 @@ class PythonEnvironment implements IPythonEnvironment { try { data = await this.deps.exec(info.command, info.args); } catch (ex) { - traceInfo(`Error when getting version of module ${moduleName}`, ex); + traceVerbose(`Error when getting version of module ${moduleName}`, ex); return undefined; } return parse(data.stdout); @@ -84,7 +84,7 @@ class PythonEnvironment implements IPythonEnvironment { try { await this.deps.exec(info.command, info.args); } catch (ex) { - traceInfo(`Error when checking if module is installed ${moduleName}`, ex); + traceVerbose(`Error when checking if module is installed ${moduleName}`, ex); return false; } return true; @@ -93,7 +93,7 @@ class PythonEnvironment implements IPythonEnvironment { private async getInterpreterInformationImpl(): Promise { try { const python = this.getExecutionInfo(); - return await getInterpreterInfo(python, this.deps.shellExec, { info: traceInfo, error: traceError }); + return await getInterpreterInfo(python, this.deps.shellExec, { verbose: traceVerbose, error: traceError }); } catch (ex) { traceError(`Failed to get interpreter information for '${this.pythonPath}'`, ex); } diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index a93ae562c97d..ba7bdf61c8f2 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -30,7 +30,6 @@ import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; import { LinterCommands } from './linters/linterCommands'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; -import { setLoggingLevel } from './logging'; import { PythonFormattingEditProvider } from './providers/formatProvider'; import { ReplProvider } from './providers/replProvider'; import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; @@ -47,7 +46,6 @@ import * as pythonEnvironments from './pythonEnvironments'; import { ActivationResult, ExtensionState } from './components'; import { Components } from './extensionInit'; import { setDefaultLanguageServer } from './activation/common/defaultlanguageServer'; -import { getLoggingLevel } from './logging/settings'; import { DebugService } from './common/application/debugService'; import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; @@ -137,11 +135,6 @@ async function activateLegacy(ext: ExtensionState): Promise { const extensions = serviceContainer.get(IExtensions); await setDefaultLanguageServer(extensions, serviceManager); - // Note we should not trigger any extension related code which logs, until we have set logging level. So we cannot - // use configurations service to get level setting. Instead, we use Workspace service to query for setting as it - // directly queries VSCode API. - setLoggingLevel(getLoggingLevel()); - const configuration = serviceManager.get(IConfigurationService); // Settings are dependent on Experiment service, so we need to initialize it after experiments are activated. serviceContainer.get(IConfigurationService).getSettings().register(); diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index 897dad9cb75d..48eccfe7bfca 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -25,14 +25,7 @@ import { EventName } from '../../telemetry/constants'; import { IInterpreterService } from '../contracts'; import { IEnvironmentActivationService } from './types'; import { TraceOptions } from '../../logging/types'; -import { - traceDecoratorError, - traceDecoratorVerbose, - traceError, - traceInfo, - traceVerbose, - traceWarn, -} from '../../logging'; +import { traceDecoratorError, traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging'; import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda'; import { StopWatch } from '../../common/utils/stopWatch'; import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; @@ -290,7 +283,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi // that's the case, wait and try again. This happens especially on AzDo const excString = (exc as Error).toString(); if (condaRetryMessages.find((m) => excString.includes(m)) && tryCount < 10) { - traceInfo(`Conda is busy, attempting to retry ...`); + traceVerbose(`Conda is busy, attempting to retry ...`); result = undefined; tryCount += 1; await sleep(500); diff --git a/src/client/logging/index.ts b/src/client/logging/index.ts index b28cadc74682..39d5652e100a 100644 --- a/src/client/logging/index.ts +++ b/src/client/logging/index.ts @@ -11,7 +11,7 @@ import { StopWatch } from '../common/utils/stopWatch'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { FileLogger } from './fileLogger'; -import { Arguments, ILogging, LoggingLevelSettingType, LogLevel, TraceDecoratorType, TraceOptions } from './types'; +import { Arguments, ILogging, LogLevel, TraceDecoratorType, TraceOptions } from './types'; import { argsToLogString, returnValueToLogString } from './util'; const DEFAULT_OPTS: TraceOptions = TraceOptions.Arguments | TraceOptions.ReturnValue; @@ -26,21 +26,6 @@ export function registerLogger(logger: ILogging): Disposable { }; } -const logLevelMap: Map = new Map([ - ['error', LogLevel.Error], - ['warn', LogLevel.Warn], - ['info', LogLevel.Info], - ['debug', LogLevel.Debug], - ['none', LogLevel.Off], - ['off', LogLevel.Off], - [undefined, LogLevel.Error], -]); - -let globalLoggingLevel: LogLevel; -export function setLoggingLevel(level?: LoggingLevelSettingType): void { - globalLoggingLevel = logLevelMap.get(level) ?? LogLevel.Error; -} - export function initializeFileLogging(disposables: Disposable[]): void { if (process.env.VSC_PYTHON_LOG_FILE) { const fileLogger = new FileLogger(createWriteStream(process.env.VSC_PYTHON_LOG_FILE)); @@ -54,27 +39,19 @@ export function traceLog(...args: Arguments): void { } export function traceError(...args: Arguments): void { - if (globalLoggingLevel >= LogLevel.Error) { - loggers.forEach((l) => l.traceError(...args)); - } + loggers.forEach((l) => l.traceError(...args)); } export function traceWarn(...args: Arguments): void { - if (globalLoggingLevel >= LogLevel.Warn) { - loggers.forEach((l) => l.traceWarn(...args)); - } + loggers.forEach((l) => l.traceWarn(...args)); } export function traceInfo(...args: Arguments): void { - if (globalLoggingLevel >= LogLevel.Info) { - loggers.forEach((l) => l.traceInfo(...args)); - } + loggers.forEach((l) => l.traceInfo(...args)); } export function traceVerbose(...args: Arguments): void { - if (globalLoggingLevel >= LogLevel.Debug) { - loggers.forEach((l) => l.traceVerbose(...args)); - } + loggers.forEach((l) => l.traceVerbose(...args)); } /** Logging Decorators go here */ @@ -89,7 +66,7 @@ export function traceDecoratorInfo(message: string): TraceDecoratorType { return createTracingDecorator({ message, opts: DEFAULT_OPTS, level: LogLevel.Info }); } export function traceDecoratorWarn(message: string): TraceDecoratorType { - return createTracingDecorator({ message, opts: DEFAULT_OPTS, level: LogLevel.Warn }); + return createTracingDecorator({ message, opts: DEFAULT_OPTS, level: LogLevel.Warning }); } // Information about a function/method call. @@ -223,7 +200,7 @@ export function logTo(logLevel: LogLevel, ...args: Arguments): void { case LogLevel.Error: traceError(...args); break; - case LogLevel.Warn: + case LogLevel.Warning: traceWarn(...args); break; case LogLevel.Info: diff --git a/src/client/logging/settings.ts b/src/client/logging/settings.ts deleted file mode 100644 index 97dce79700d2..000000000000 --- a/src/client/logging/settings.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { LoggingLevelSettingType } from './types'; -import { WorkspaceService } from '../common/application/workspace'; - -/** - * Uses Workspace service to query for `python.logging.level` setting and returns it. - */ -export function getLoggingLevel(): LoggingLevelSettingType | 'off' { - const workspace = new WorkspaceService(); - return workspace.getConfiguration('python').get('logging.level') ?? 'error'; -} diff --git a/src/client/logging/types.ts b/src/client/logging/types.ts index 92b514d218f9..c05800868512 100644 --- a/src/client/logging/types.ts +++ b/src/client/logging/types.ts @@ -4,17 +4,17 @@ /* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ -export type LoggingLevelSettingType = 'off' | 'error' | 'warn' | 'info' | 'debug'; +export type Arguments = unknown[]; + export enum LogLevel { Off = 0, - Error = 10, - Warn = 20, - Info = 30, - Debug = 40, + Trace = 1, + Debug = 2, + Info = 3, + Warning = 4, + Error = 5, } -export type Arguments = unknown[]; - export interface ILogging { traceLog(...data: Arguments): void; traceError(...data: Arguments): void; diff --git a/src/client/pythonEnvironments/base/info/environmentInfoService.ts b/src/client/pythonEnvironments/base/info/environmentInfoService.ts index d3d3a252c99f..6a981d21b6df 100644 --- a/src/client/pythonEnvironments/base/info/environmentInfoService.ts +++ b/src/client/pythonEnvironments/base/info/environmentInfoService.ts @@ -7,7 +7,7 @@ import { createDeferred, Deferred, sleep } from '../../../common/utils/async'; import { createRunningWorkerPool, IWorkerPool, QueuePosition } from '../../../common/utils/workerPool'; import { getInterpreterInfo, InterpreterInformation } from './interpreter'; import { buildPythonExecInfo } from '../../exec'; -import { traceError, traceInfo, traceWarn } from '../../../logging'; +import { traceError, traceVerbose, traceWarn } from '../../../logging'; import { Conda, CONDA_ACTIVATION_TIMEOUT, isCondaEnvironment } from '../../common/environmentManagers/conda'; import { PythonEnvInfo, PythonEnvKind } from '.'; import { normCasePath } from '../../common/externalDependencies'; @@ -153,7 +153,7 @@ class EnvironmentInfoService implements IEnvironmentInfoService { // as complete env info may not be available at this time. const isCondaEnv = env.kind === PythonEnvKind.Conda || (await isCondaEnvironment(env.executable.filename)); if (isCondaEnv) { - traceInfo( + traceVerbose( `Validating ${env.executable.filename} normally failed with error, falling back to using conda run: (${reason})`, ); if (this.condaRunWorkerPool === undefined) { diff --git a/src/client/pythonEnvironments/base/info/interpreter.ts b/src/client/pythonEnvironments/base/info/interpreter.ts index 5341a8f561ae..d0cb1f13f8f8 100644 --- a/src/client/pythonEnvironments/base/info/interpreter.ts +++ b/src/client/pythonEnvironments/base/info/interpreter.ts @@ -8,7 +8,7 @@ import { InterpreterInfoJson, } from '../../../common/process/internal/scripts'; import { Architecture } from '../../../common/utils/platform'; -import { traceError, traceInfo } from '../../../logging'; +import { traceError, traceVerbose } from '../../../logging'; import { shellExecute } from '../../common/externalDependencies'; import { copyPythonExecInfo, PythonExecInfo } from '../../exec'; import { parseVersion } from './pythonVersion'; @@ -102,6 +102,6 @@ export async function getInterpreterInfo( traceError(`Failed to parse interpreter information for >> ${quoted} << with ${ex}`); return undefined; } - traceInfo(`Found interpreter for >> ${quoted} <<: ${JSON.stringify(json)}`); + traceVerbose(`Found interpreter for >> ${quoted} <<: ${JSON.stringify(json)}`); return extractInterpreterInfo(python.pythonExecutable, json); } diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts index d3ece41f163d..dadab2512e16 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts @@ -3,7 +3,7 @@ import { Event } from 'vscode'; import { isTestExecution } from '../../../../common/constants'; -import { traceInfo, traceVerbose } from '../../../../logging'; +import { traceVerbose } from '../../../../logging'; import { arePathsSame, getFileInfo, pathExists } from '../../../common/externalDependencies'; import { PythonEnvInfo, PythonEnvKind } from '../../info'; import { areEnvsDeepEqual, areSameEnv, getEnvPath } from '../../info/env'; @@ -225,7 +225,7 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher, token: CancellationToken) => { - traceInfo(`Starting TensorBoard with log directory ${logDir}...`); + traceVerbose(`Starting TensorBoard with log directory ${logDir}...`); const spawnTensorBoard = this.waitForTensorBoardStart(observable); const userCancellation = createPromiseFromCancellation({ @@ -421,7 +421,7 @@ export class TensorBoardSession { switch (result) { case 'canceled': - traceInfo('Canceled starting TensorBoard session.'); + traceVerbose('Canceled starting TensorBoard session.'); sendTelemetryEvent( EventName.TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION, sessionStartStopwatch.elapsedTime, @@ -468,7 +468,7 @@ export class TensorBoardSession { this.url = match[1]; urlThatTensorBoardIsRunningAt.resolve('success'); } - traceInfo(output.out); + traceVerbose(output.out); } else if (output.source === 'stderr') { traceError(output.out); } @@ -482,7 +482,7 @@ export class TensorBoardSession { } private async showPanel() { - traceInfo('Showing TensorBoard panel'); + traceVerbose('Showing TensorBoard panel'); const panel = this.webviewPanel || (await this.createPanel()); panel.reveal(); this._active = true; diff --git a/src/client/tensorBoard/tensorBoardSessionProvider.ts b/src/client/tensorBoard/tensorBoardSessionProvider.ts index f689dab6a6ba..53878bd543c2 100644 --- a/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ b/src/client/tensorBoard/tensorBoardSessionProvider.ts @@ -17,7 +17,7 @@ import { } from '../common/types'; import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; import { IInterpreterService } from '../interpreter/contracts'; -import { traceError, traceInfo } from '../logging'; +import { traceError, traceVerbose } from '../logging'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; @@ -94,7 +94,7 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer } private async createNewSession(): Promise { - traceInfo('Starting new TensorBoard session...'); + traceVerbose('Starting new TensorBoard session...'); try { const newSession = new TensorBoardSession( this.installer, diff --git a/src/client/testing/common/testConfigurationManager.ts b/src/client/testing/common/testConfigurationManager.ts index c2b050cb524a..be3f0109da02 100644 --- a/src/client/testing/common/testConfigurationManager.ts +++ b/src/client/testing/common/testConfigurationManager.ts @@ -5,12 +5,12 @@ import { IFileSystem } from '../../common/platform/types'; import { IInstaller } from '../../common/types'; import { createDeferred } from '../../common/utils/async'; import { IServiceContainer } from '../../ioc/types'; -import { traceInfo } from '../../logging'; +import { traceVerbose } from '../../logging'; import { UNIT_TEST_PRODUCTS } from './constants'; import { ITestConfigSettingsService, ITestConfigurationManager, UnitTestProduct } from './types'; function handleCancelled(): void { - traceInfo('testing configuration (in UI) cancelled'); + traceVerbose('testing configuration (in UI) cancelled'); throw Error('cancelled'); } diff --git a/src/client/testing/testController/unittest/runner.ts b/src/client/testing/testController/unittest/runner.ts index e0897f554cea..d558f051eccb 100644 --- a/src/client/testing/testController/unittest/runner.ts +++ b/src/client/testing/testController/unittest/runner.ts @@ -7,7 +7,7 @@ import * as internalScripts from '../../../common/process/internal/scripts'; import { splitLines } from '../../../common/stringUtils'; import { ITestOutputChannel } from '../../../common/types'; import { noop } from '../../../common/utils/misc'; -import { traceError, traceInfo } from '../../../logging'; +import { traceError, traceVerbose } from '../../../logging'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { ITestRunner, ITestDebugLauncher, IUnitTestSocketServer, LaunchOptions, Options } from '../../common/types'; import { clearAllChildren, getTestCaseNodes } from '../common/testItemUtilities'; @@ -98,7 +98,7 @@ export class UnittestRunner implements ITestsRunner { traceError(`${message} ${data.join(' ')}`); }); this.server.on('log', (message: string, ...data: string[]) => { - traceInfo(`${message} ${data.join(' ')}`); + traceVerbose(`${message} ${data.join(' ')}`); }); this.server.on('connect', noop); this.server.on('start', noop); diff --git a/src/test/testLogger.ts b/src/test/testLogger.ts index b41f17ecafcc..26484ee119c7 100644 --- a/src/test/testLogger.ts +++ b/src/test/testLogger.ts @@ -5,6 +5,7 @@ import { initializeFileLogging, logTo } from '../client/logging'; import { LogLevel } from '../client/logging/types'; + // IMPORTANT: This file should only be importing from the '../client/logging' directory, as we // delete everything in '../client' except for '../client/logging' before running smoke tests. @@ -31,7 +32,7 @@ function monkeypatchConsole() { const streams = ['log', 'error', 'warn', 'info', 'debug', 'trace']; const levels: { [key: string]: LogLevel } = { error: LogLevel.Error, - warn: LogLevel.Warn, + warn: LogLevel.Warning, debug: LogLevel.Debug, trace: LogLevel.Debug, info: LogLevel.Info, From 77c63f163a0f7c2a41e9e2e26fcf5aa0f5df1ae9 Mon Sep 17 00:00:00 2001 From: Erik De Bonte Date: Thu, 20 Apr 2023 09:08:25 -0700 Subject: [PATCH 059/336] Remove Pylance auto-indent experiment code (#21088) --- src/client/activation/node/analysisOptions.ts | 80 +------------------ .../pylanceLSExtensionManager.ts | 6 +- .../node/analysisOptions.unit.test.ts | 11 +-- 3 files changed, 5 insertions(+), 92 deletions(-) diff --git a/src/client/activation/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts index 80dbc53a59ee..71295649c25a 100644 --- a/src/client/activation/node/analysisOptions.ts +++ b/src/client/activation/node/analysisOptions.ts @@ -1,27 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ConfigurationTarget, extensions, WorkspaceConfiguration } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; -import * as semver from 'semver'; import { IWorkspaceService } from '../../common/application/types'; -import { PYLANCE_EXTENSION_ID } from '../../common/constants'; -import { IExperimentService } from '../../common/types'; import { LanguageServerAnalysisOptionsBase } from '../common/analysisOptions'; import { ILanguageServerOutputChannel } from '../types'; -import { traceWarn } from '../../logging'; - -const EDITOR_CONFIG_SECTION = 'editor'; -const FORMAT_ON_TYPE_CONFIG_SETTING = 'formatOnType'; export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsBase { // eslint-disable-next-line @typescript-eslint/no-useless-constructor - constructor( - lsOutputChannel: ILanguageServerOutputChannel, - workspace: IWorkspaceService, - private readonly experimentService: IExperimentService, - ) { + constructor(lsOutputChannel: ILanguageServerOutputChannel, workspace: IWorkspaceService) { super(lsOutputChannel, workspace); } @@ -34,72 +22,6 @@ export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt return ({ experimentationSupport: true, trustedWorkspaceSupport: true, - autoIndentSupport: await this.isAutoIndentEnabled(), } as unknown) as LanguageClientOptions; } - - private async isAutoIndentEnabled() { - let editorConfig = this.getPythonSpecificEditorSection(); - - // Only explicitly enable formatOnType for those who are in the experiment - // but have not explicitly given a value for the setting - if (!NodeLanguageServerAnalysisOptions.isConfigSettingSetByUser(editorConfig, FORMAT_ON_TYPE_CONFIG_SETTING)) { - const inExperiment = await this.isInAutoIndentExperiment(); - if (inExperiment) { - await NodeLanguageServerAnalysisOptions.setPythonSpecificFormatOnType(editorConfig, true); - - // Refresh our view of the config settings. - editorConfig = this.getPythonSpecificEditorSection(); - } - } - - const formatOnTypeEffectiveValue = editorConfig.get(FORMAT_ON_TYPE_CONFIG_SETTING); - - return formatOnTypeEffectiveValue; - } - - private static isConfigSettingSetByUser(configuration: WorkspaceConfiguration, setting: string): boolean { - const inspect = configuration.inspect(setting); - if (inspect === undefined) { - return false; - } - - return ( - inspect.globalValue !== undefined || - inspect.workspaceValue !== undefined || - inspect.workspaceFolderValue !== undefined || - inspect.globalLanguageValue !== undefined || - inspect.workspaceLanguageValue !== undefined || - inspect.workspaceFolderLanguageValue !== undefined - ); - } - - private async isInAutoIndentExperiment(): Promise { - if (await this.experimentService.inExperiment('pylanceAutoIndent')) { - return true; - } - - const pylanceVersion = extensions.getExtension(PYLANCE_EXTENSION_ID)?.packageJSON.version as string; - return pylanceVersion !== undefined && semver.prerelease(pylanceVersion)?.includes('dev') === true; - } - - private getPythonSpecificEditorSection() { - return this.workspace.getConfiguration(EDITOR_CONFIG_SECTION, undefined, /* languageSpecific */ true); - } - - private static async setPythonSpecificFormatOnType( - editorConfig: WorkspaceConfiguration, - value: boolean | undefined, - ) { - try { - await editorConfig.update( - FORMAT_ON_TYPE_CONFIG_SETTING, - value, - ConfigurationTarget.Global, - /* overrideInLanguage */ true, - ); - } catch (ex) { - traceWarn(`Failed to set formatOnType to ${value}`); - } - } } diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts index c7df3318e3b7..7b03d909a512 100644 --- a/src/client/languageServer/pylanceLSExtensionManager.ts +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -49,11 +49,7 @@ export class PylanceLSExtensionManager implements IDisposable, ILanguageServerEx private readonly extensions: IExtensions, readonly applicationShell: IApplicationShell, ) { - this.analysisOptions = new NodeLanguageServerAnalysisOptions( - outputChannel, - workspaceService, - experimentService, - ); + this.analysisOptions = new NodeLanguageServerAnalysisOptions(outputChannel, workspaceService); this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions); this.serverProxy = new NodeLanguageServerProxy( this.clientFactory, diff --git a/src/test/activation/node/analysisOptions.unit.test.ts b/src/test/activation/node/analysisOptions.unit.test.ts index 47c0cb0cc838..d5e97f93768e 100644 --- a/src/test/activation/node/analysisOptions.unit.test.ts +++ b/src/test/activation/node/analysisOptions.unit.test.ts @@ -2,14 +2,14 @@ // Licensed under the MIT License. import { assert, expect } from 'chai'; import * as typemoq from 'typemoq'; -import { WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import { WorkspaceFolder } from 'vscode'; import { DocumentFilter } from 'vscode-languageclient/node'; import { NodeLanguageServerAnalysisOptions } from '../../../client/activation/node/analysisOptions'; import { ILanguageServerOutputChannel } from '../../../client/activation/types'; import { IWorkspaceService } from '../../../client/common/application/types'; import { PYTHON, PYTHON_LANGUAGE } from '../../../client/common/constants'; -import { IExperimentService, ILogOutputChannel } from '../../../client/common/types'; +import { ILogOutputChannel } from '../../../client/common/types'; suite('Pylance Language Server - Analysis Options', () => { class TestClass extends NodeLanguageServerAnalysisOptions { @@ -31,19 +31,14 @@ suite('Pylance Language Server - Analysis Options', () => { let outputChannel: ILogOutputChannel; let lsOutputChannel: typemoq.IMock; let workspace: typemoq.IMock; - let experimentService: IExperimentService; setup(() => { outputChannel = typemoq.Mock.ofType().object; workspace = typemoq.Mock.ofType(); workspace.setup((w) => w.isVirtualWorkspace).returns(() => false); - const workspaceConfig = typemoq.Mock.ofType(); - workspace.setup((w) => w.getConfiguration('editor', undefined, true)).returns(() => workspaceConfig.object); - workspaceConfig.setup((w) => w.get('formatOnType')).returns(() => true); lsOutputChannel = typemoq.Mock.ofType(); lsOutputChannel.setup((l) => l.channel).returns(() => outputChannel); - experimentService = typemoq.Mock.ofType().object; - analysisOptions = new TestClass(lsOutputChannel.object, workspace.object, experimentService); + analysisOptions = new TestClass(lsOutputChannel.object, workspace.object); }); test('Workspace folder is undefined', () => { From 7b4bc291254ce15c8e4f80d0f4e18db7a3a851c0 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 20 Apr 2023 11:39:39 -0700 Subject: [PATCH 060/336] Overload methods testing rewrite (#21082) I now overload the method signature for discovery and execution so instead the only place where the new rewrite code needs to be enabled is in the controller where it calls either the old method signature without the`pythonExecFactory` or provides it to use the new code. --- .../testing/testController/common/types.ts | 20 +- .../testing/testController/controller.ts | 116 ++++++------ .../pytest/pytestDiscoveryAdapter.ts | 19 +- .../pytest/pytestExecutionAdapter.ts | 117 ++++++------ .../testController/workspaceTestAdapter.ts | 44 ++--- .../pytestDiscoveryAdapter.unit.test.ts | 179 +++++++++--------- 6 files changed, 255 insertions(+), 240 deletions(-) diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 1fe480c45f26..52c6c787040c 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -12,8 +12,8 @@ import { Uri, WorkspaceFolder, } from 'vscode'; -// ** import { IPythonExecutionFactory } from '../../../common/process/types'; import { TestDiscoveryOptions } from '../../common/types'; +import { IPythonExecutionFactory } from '../../../common/process/types'; export type TestRunInstanceOptions = TestRunOptions & { exclude?: readonly TestItem[]; @@ -179,21 +179,21 @@ export interface ITestServer { } export interface ITestDiscoveryAdapter { - // ** Uncomment second line and comment out first line to use the new discovery method. + // ** first line old method signature, second line new method signature discoverTests(uri: Uri): Promise; - // discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise; + discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise; } // interface for execution/runner adapter export interface ITestExecutionAdapter { - // ** Uncomment second line and comment out first line to use the new execution method. + // ** first line old method signature, second line new method signature runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise; - // runTests( - // uri: Uri, - // testIds: string[], - // debugBool?: boolean, - // executionFactory?: IPythonExecutionFactory, - // ): Promise; + runTests( + uri: Uri, + testIds: string[], + debugBool?: boolean, + executionFactory?: IPythonExecutionFactory, + ): Promise; } // Same types as in pythonFiles/unittestadapter/utils.py diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index e745adb019b2..fb176a30af88 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -241,39 +241,46 @@ export class PythonTestController implements ITestController, IExtensionSingleAc if (uri) { const settings = this.configSettings.getSettings(uri); traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); + const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; if (settings.testing.pytestEnabled) { // Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; - // ** uncomment ~231 - 241 to NEW new test discovery mechanism - // const workspace = this.workspaceService.getWorkspaceFolder(uri); - // traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); - // const testAdapter = - // this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); - // testAdapter.discoverTests( - // this.testController, - // this.refreshCancellation.token, - // this.testAdapters.size > 1, - // this.workspaceService.workspaceFile?.fsPath, - // this.pythonExecFactory, - // ); - // uncomment ~243 to use OLD test discovery mechanism - await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); + if (rewriteTestingEnabled) { + // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + const workspace = this.workspaceService.getWorkspaceFolder(uri); + traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + const testAdapter = + this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); + testAdapter.discoverTests( + this.testController, + this.refreshCancellation.token, + this.testAdapters.size > 1, + this.workspaceService.workspaceFile?.fsPath, + this.pythonExecFactory, + ); + } else { + // else use OLD test discovery mechanism + await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); + } } else if (settings.testing.unittestEnabled) { // ** Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; - // uncomment ~248 - 258 to NEW new test discovery mechanism - // const workspace = this.workspaceService.getWorkspaceFolder(uri); - // traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); - // const testAdapter = - // this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); - // testAdapter.discoverTests( - // this.testController, - // this.refreshCancellation.token, - // this.testAdapters.size > 1, - // this.workspaceService.workspaceFile?.fsPath, - // ); - // uncomment ~260 to use OLD test discovery mechanism - await this.unittest.refreshTestData(this.testController, uri, this.refreshCancellation.token); + if (rewriteTestingEnabled) { + // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + const workspace = this.workspaceService.getWorkspaceFolder(uri); + traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + const testAdapter = + this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); + testAdapter.discoverTests( + this.testController, + this.refreshCancellation.token, + this.testAdapters.size > 1, + this.workspaceService.workspaceFile?.fsPath, + ); + } else { + // else use OLD test discovery mechanism + await this.unittest.refreshTestData(this.testController, uri, this.refreshCancellation.token); + } } else { if (this.sendTestDisabledTelemetry) { this.sendTestDisabledTelemetry = false; @@ -384,25 +391,26 @@ export class PythonTestController implements ITestController, IExtensionSingleAc const settings = this.configSettings.getSettings(workspace.uri); if (testItems.length > 0) { + const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; if (settings.testing.pytestEnabled) { sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { tool: 'pytest', debugging: request.profile?.kind === TestRunProfileKind.Debug, }); - // ** new execution runner/adapter - // const testAdapter = - // this.testAdapters.get(workspace.uri) || - // (this.testAdapters.values().next().value as WorkspaceTestAdapter); - // return testAdapter.executeTests( - // this.testController, - // runInstance, - // testItems, - // token, - // request.profile?.kind === TestRunProfileKind.Debug, - // this.pythonExecFactory, - // ); - - // below is old way of running pytest execution + // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + if (rewriteTestingEnabled) { + const testAdapter = + this.testAdapters.get(workspace.uri) || + (this.testAdapters.values().next().value as WorkspaceTestAdapter); + return testAdapter.executeTests( + this.testController, + runInstance, + testItems, + token, + request.profile?.kind === TestRunProfileKind.Debug, + this.pythonExecFactory, + ); + } return this.pytest.runTests( { includes: testItems, @@ -415,23 +423,23 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); } if (settings.testing.unittestEnabled) { - // potentially squeeze in the new execution way here? sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { tool: 'unittest', debugging: request.profile?.kind === TestRunProfileKind.Debug, }); - // new execution runner/adapter - // const testAdapter = - // this.testAdapters.get(workspace.uri) || - // (this.testAdapters.values().next().value as WorkspaceTestAdapter); - // return testAdapter.executeTests( - // this.testController, - // runInstance, - // testItems, - // token, - // request.profile?.kind === TestRunProfileKind.Debug, - // ); - + // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + if (rewriteTestingEnabled) { + const testAdapter = + this.testAdapters.get(workspace.uri) || + (this.testAdapters.values().next().value as WorkspaceTestAdapter); + return testAdapter.executeTests( + this.testController, + runInstance, + testItems, + token, + request.profile?.kind === TestRunProfileKind.Debug, + ); + } // below is old way of running unittest execution return this.unittest.runTests( { diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index d2f6f6448777..b9df9bb30725 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -37,20 +37,19 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } } - // ** Old version of discover tests. - discoverTests(uri: Uri): Promise { + discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + if (executionFactory !== undefined) { + // ** new version of discover tests. + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; + traceVerbose(pytestArgs); + return this.runPytestDiscovery(uri, executionFactory); + } + // if executionFactory is undefined, we are using the old method signature of discover tests. traceVerbose(uri); this.deferred = createDeferred(); return this.deferred.promise; } - // Uncomment this version of the function discoverTests to use the new discovery method. - // public async discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { - // const settings = this.configSettings.getSettings(uri); - // const { pytestArgs } = settings.testing; - // traceVerbose(pytestArgs); - - // return this.runPytestDiscovery(uri, executionFactory); - // } async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { const deferred = createDeferred(); diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 1f6b504074cd..af97dad0526d 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -2,10 +2,17 @@ // Licensed under the MIT License. import { Uri } from 'vscode'; +import path from 'path'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; /** * Wrapper Class for pytest test execution. This is where we call `runTestCommand`? @@ -32,70 +39,68 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } } - // ** Old version of discover tests. - async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { + async runTests( + uri: Uri, + testIds: string[], + debugBool?: boolean, + executionFactory?: IPythonExecutionFactory, + ): Promise { traceVerbose(uri, testIds, debugBool); - // TODO:Remove this line after enabling runs + if (executionFactory !== undefined) { + // ** new version of run tests. + return this.runTestsNew(uri, testIds, debugBool, executionFactory); + } + // if executionFactory is undefined, we are using the old method signature of run tests. this.outputChannel.appendLine('Running tests.'); this.deferred = createDeferred(); return this.deferred.promise; } -} - -// public async runTests( -// uri: Uri, -// testIds: string[], -// debugBool?: boolean, -// executionFactory?: IPythonExecutionFactory, -// ): Promise { -// const deferred = createDeferred(); -// const relativePathToPytest = 'pythonFiles'; -// const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); -// this.configSettings.isTestExecution(); -// const uuid = this.testServer.createUUID(uri.fsPath); -// this.promiseMap.set(uuid, deferred); -// const settings = this.configSettings.getSettings(uri); -// const { pytestArgs } = settings.testing; - -// const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; -// const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); -// const spawnOptions: SpawnOptions = { -// cwd: uri.fsPath, -// throwOnStdErr: true, -// extraVariables: { -// PYTHONPATH: pythonPathCommand, -// TEST_UUID: uuid.toString(), -// TEST_PORT: this.testServer.getPort().toString(), -// }, -// outputChannel: this.outputChannel, -// }; + private async runTestsNew( + uri: Uri, + testIds: string[], + debugBool?: boolean, + executionFactory?: IPythonExecutionFactory, + ): Promise { + const deferred = createDeferred(); + const relativePathToPytest = 'pythonFiles'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + this.configSettings.isTestExecution(); + const uuid = this.testServer.createUUID(uri.fsPath); + this.promiseMap.set(uuid, deferred); + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; -// // Create the Python environment in which to execute the command. -// const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { -// allowEnvironmentFetchExceptions: false, -// resource: uri, -// }; -// // need to check what will happen in the exec service is NOT defined and is null -// const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); -// const testIdsString = testIds.join(' '); -// console.debug('what to do with debug bool?', debugBool); -// try { -// execService?.exec(['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), spawnOptions); -// } catch (ex) { -// console.error(ex); -// } + const spawnOptions: SpawnOptions = { + cwd: uri.fsPath, + throwOnStdErr: true, + extraVariables: { + PYTHONPATH: pythonPathCommand, + TEST_UUID: uuid.toString(), + TEST_PORT: this.testServer.getPort().toString(), + }, + outputChannel: this.outputChannel, + }; -// return deferred.promise; -// } -// } + // Create the Python environment in which to execute the command. + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + }; + // need to check what will happen in the exec service is NOT defined and is null + const execService = await executionFactory?.createActivatedEnvironment(creationOptions); -// function buildExecutionCommand(args: string[]): TestExecutionCommand { -// const executionScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + const testIdsString = testIds.join(' '); + console.debug('what to do with debug bool?', debugBool); + try { + execService?.exec(['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), spawnOptions); + } catch (ex) { + console.error(ex); + } -// return { -// script: executionScript, -// args: ['--udiscovery', ...args], -// }; -// } + return deferred.promise; + } +} diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 8ce51e5dab56..b339256c3e08 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -17,7 +17,7 @@ import { import { splitLines } from '../../common/stringUtils'; import { createDeferred, Deferred } from '../../common/utils/async'; import { Testing } from '../../common/utils/localize'; -import { traceError } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { TestProvider } from '../types'; @@ -37,6 +37,7 @@ import { ITestExecutionAdapter, } from './common/types'; import { fixLogLines } from './common/utils'; +import { IPythonExecutionFactory } from '../../common/process/types'; /** * This class exposes a test-provider-agnostic way of discovering tests. @@ -69,13 +70,13 @@ export class WorkspaceTestAdapter { this.vsIdToRunId = new Map(); } - // ** add executionFactory?: IPythonExecutionFactory, to the parameters public async executeTests( testController: TestController, runInstance: TestRun, includes: TestItem[], token?: CancellationToken, debugBool?: boolean, + executionFactory?: IPythonExecutionFactory, ): Promise { if (this.executing) { return this.executing.promise; @@ -102,18 +103,18 @@ export class WorkspaceTestAdapter { } }); - // ** First line is old way, section with if statement below is new way. - rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); - // if (executionFactory !== undefined) { - // rawTestExecData = await this.executionAdapter.runTests( - // this.workspaceUri, - // testCaseIds, - // debugBool, - // executionFactory, - // ); - // } else { - // traceVerbose('executionFactory is undefined'); - // } + // ** execution factory only defined for new rewrite way + if (executionFactory !== undefined) { + rawTestExecData = await this.executionAdapter.runTests( + this.workspaceUri, + testCaseIds, + debugBool, + executionFactory, + ); + traceVerbose('executionFactory defined'); + } else { + rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); + } deferred.resolve(); } catch (ex) { // handle token and telemetry here @@ -278,12 +279,12 @@ export class WorkspaceTestAdapter { return Promise.resolve(); } - // add `executionFactory?: IPythonExecutionFactory,` to the function for new pytest method public async discoverTests( testController: TestController, token?: CancellationToken, isMultiroot?: boolean, workspaceFilePath?: string, + executionFactory?: IPythonExecutionFactory, ): Promise { sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); @@ -299,13 +300,12 @@ export class WorkspaceTestAdapter { let rawTestData; try { - // ** First line is old way, section with if statement below is new way. - rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); - // if (executionFactory !== undefined) { - // rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); - // } else { - // traceVerbose('executionFactory is undefined'); - // } + // ** execution factory only defined for new rewrite way + if (executionFactory !== undefined) { + rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); + } else { + rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); + } deferred.resolve(); } catch (ex) { sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: true }); diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index f5a2203dbd9a..12c79a23c7fd 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -1,91 +1,94 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. -// import * as assert from 'assert'; -// import { Uri } from 'vscode'; -// import * as typeMoq from 'typemoq'; -// import { IConfigurationService } from '../../../../client/common/types'; -// import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; -// import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; -// import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; -// import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as assert from 'assert'; +import { Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; -// suite('pytest test discovery adapter', () => { -// let testServer: typeMoq.IMock; -// let configService: IConfigurationService; -// let execFactory = typeMoq.Mock.ofType(); -// let adapter: PytestTestDiscoveryAdapter; -// let execService: typeMoq.IMock; -// let deferred: Deferred; -// setup(() => { -// testServer = typeMoq.Mock.ofType(); -// testServer.setup((t) => t.getPort()).returns(() => 12345); -// testServer -// .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) -// .returns(() => ({ -// dispose: () => { -// /* no-body */ -// }, -// })); -// configService = ({ -// getSettings: () => ({ -// testing: { pytestArgs: ['.'] }, -// }), -// } as unknown) as IConfigurationService; -// execFactory = typeMoq.Mock.ofType(); -// execService = typeMoq.Mock.ofType(); -// execFactory -// .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) -// .returns(() => Promise.resolve(execService.object)); -// deferred = createDeferred(); -// execService -// .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) -// .returns(() => { -// deferred.resolve(); -// return Promise.resolve({ stdout: '{}' }); -// }); -// execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); -// execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); -// }); -// test('onDataReceivedHandler should parse only if known UUID', async () => { -// const uri = Uri.file('/my/test/path/'); -// const uuid = 'uuid123'; -// const data = { status: 'success' }; -// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); -// const eventData: DataReceivedEvent = { -// uuid, -// data: JSON.stringify(data), -// }; +suite('pytest test discovery adapter', () => { + let testServer: typeMoq.IMock; + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType(); + let adapter: PytestTestDiscoveryAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let outputChannel: typeMoq.IMock; -// adapter = new PytestTestDiscoveryAdapter(testServer.object, configService); -// // ** const promise = adapter.discoverTests(uri, execFactory.object); -// const promise = adapter.discoverTests(uri); -// await deferred.promise; -// adapter.onDataReceivedHandler(eventData); -// const result = await promise; -// assert.deepStrictEqual(result, data); -// }); -// test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { -// const uri = Uri.file('/my/test/path/'); -// const uuid = 'uuid456'; -// let data = { status: 'error' }; -// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); -// const wrongUriEventData: DataReceivedEvent = { -// uuid: 'incorrect-uuid456', -// data: JSON.stringify(data), -// }; -// adapter = new PytestTestDiscoveryAdapter(testServer.object, configService); -// // ** const promise = adapter.discoverTests(uri, execFactory.object); -// const promise = adapter.discoverTests(uri); -// adapter.onDataReceivedHandler(wrongUriEventData); + setup(() => { + testServer = typeMoq.Mock.ofType(); + testServer.setup((t) => t.getPort()).returns(() => 12345); + testServer + .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'] }, + }), + } as unknown) as IConfigurationService; + execFactory = typeMoq.Mock.ofType(); + execService = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + outputChannel = typeMoq.Mock.ofType(); + }); + test('onDataReceivedHandler should parse only if known UUID', async () => { + const uri = Uri.file('/my/test/path/'); + const uuid = 'uuid123'; + const data = { status: 'success' }; + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const eventData: DataReceivedEvent = { + uuid, + data: JSON.stringify(data), + }; -// data = { status: 'success' }; -// const correctUriEventData: DataReceivedEvent = { -// uuid, -// data: JSON.stringify(data), -// }; -// adapter.onDataReceivedHandler(correctUriEventData); -// const result = await promise; -// assert.deepStrictEqual(result, data); -// }); -// }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); + const promise = adapter.discoverTests(uri, execFactory.object); + // const promise = adapter.discoverTests(uri); + await deferred.promise; + adapter.onDataReceivedHandler(eventData); + const result = await promise; + assert.deepStrictEqual(result, data); + }); + test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { + const uri = Uri.file('/my/test/path/'); + const uuid = 'uuid456'; + let data = { status: 'error' }; + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const wrongUriEventData: DataReceivedEvent = { + uuid: 'incorrect-uuid456', + data: JSON.stringify(data), + }; + adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); + const promise = adapter.discoverTests(uri, execFactory.object); + // const promise = adapter.discoverTests(uri); + adapter.onDataReceivedHandler(wrongUriEventData); + + data = { status: 'success' }; + const correctUriEventData: DataReceivedEvent = { + uuid, + data: JSON.stringify(data), + }; + adapter.onDataReceivedHandler(correctUriEventData); + const result = await promise; + assert.deepStrictEqual(result, data); + }); +}); From d905cf0555e0032cfe10a2af2b925e686246e30e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 20 Apr 2023 18:49:13 -0700 Subject: [PATCH 061/336] Pytest fix execution args (#21094) fix args for runtest IDs --- .../pytest/pytestExecutionAdapter.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index af97dad0526d..d2cbd3151e6f 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { Uri } from 'vscode'; -import path from 'path'; +import * as path from 'path'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { traceVerbose } from '../../../logging'; @@ -13,7 +13,10 @@ import { SpawnOptions, } from '../../../common/process/types'; import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { removePositionalFoldersAndFiles } from './arguments'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; /** * Wrapper Class for pytest test execution. This is where we call `runTestCommand`? */ @@ -93,12 +96,29 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // need to check what will happen in the exec service is NOT defined and is null const execService = await executionFactory?.createActivatedEnvironment(creationOptions); - const testIdsString = testIds.join(' '); - console.debug('what to do with debug bool?', debugBool); try { - execService?.exec(['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), spawnOptions); + // Remove positional test folders and files, we will add as needed per node + const testArgs = removePositionalFoldersAndFiles(pytestArgs); + + // if user has provided `--rootdir` then use that, otherwise add `cwd` + if (testArgs.filter((a) => a.startsWith('--rootdir')).length === 0) { + // Make sure root dir is set so pytest can find the relative paths + testArgs.splice(0, 0, '--rootdir', uri.fsPath); + } + + if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { + testArgs.push('--capture', 'no'); + } + + console.debug(`Running test with arguments: ${testArgs.join(' ')}\r\n`); + console.debug(`Current working directory: ${uri.fsPath}\r\n`); + + const argArray = ['-m', 'pytest', '-p', 'vscode_pytest'].concat(testArgs).concat(testIds); + console.debug('argArray', argArray); + execService?.exec(argArray, spawnOptions); } catch (ex) { - console.error(ex); + console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); + return Promise.reject(ex); } return deferred.promise; From 9f24fbf11f4a676354e7c28d0866ddc088d5a074 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Sun, 23 Apr 2023 10:23:22 -0700 Subject: [PATCH 062/336] Support `pythonTerminalEnvVarActivation` experiment in multiroot workspaces (#21047) --- package.json | 5 +- src/client/common/experiments/helpers.ts | 7 - src/client/common/utils/localize.ts | 1 + .../terminalEnvVarCollectionService.ts | 82 +++++++----- src/client/interpreter/serviceRegistry.ts | 4 +- ...rminalEnvVarCollectionService.unit.test.ts | 123 ++++++++++++++---- ...scode.proposed.envCollectionWorkspace.d.ts | 38 ++++++ 7 files changed, 195 insertions(+), 65 deletions(-) create mode 100644 types/vscode.proposed.envCollectionWorkspace.d.ts diff --git a/package.json b/package.json index e0b418a4cd9b..6ac8ebcfa01c 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "quickPickSortByLabel", "envShellEvent", "testObserver", - "quickPickItemTooltip" + "quickPickItemTooltip", + "envCollectionWorkspace" ], "author": { "name": "Microsoft Corporation" @@ -43,7 +44,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.77.0-20230309" + "vscode": "^1.78.0-20230421" }, "keywords": [ "python", diff --git a/src/client/common/experiments/helpers.ts b/src/client/common/experiments/helpers.ts index 4aed04da3fd0..04da948fd15d 100644 --- a/src/client/common/experiments/helpers.ts +++ b/src/client/common/experiments/helpers.ts @@ -3,17 +3,10 @@ 'use strict'; -import { workspace } from 'vscode'; -import { isTestExecution } from '../constants'; import { IExperimentService } from '../types'; import { TerminalEnvVarActivation } from './groups'; export function inTerminalEnvVarExperiment(experimentService: IExperimentService): boolean { - if (workspace.workspaceFile && !isTestExecution()) { - // Don't run experiment in multi-root workspaces for now, requires work on VSCode: - // https://github.com/microsoft/vscode/issues/171173 - return false; - } if (!experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)) { return false; } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 4c3cd7a45d0d..633d9426ba74 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -198,6 +198,7 @@ export namespace Interpreters { 'We noticed you\'re using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings. [Learn more](https://aka.ms/AA66i8f).', ); export const activatingTerminals = l10n.t('Reactivating terminals...'); + export const activateTerminalDescription = l10n.t('Activated environment for'); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', ); diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 7010a87a203c..e492414ecfd1 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -2,9 +2,9 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { ProgressOptions, ProgressLocation } from 'vscode'; -import { IExtensionSingleActivationService } from '../../activation/types'; -import { IApplicationShell, IApplicationEnvironment } from '../../common/application/types'; +import { ProgressOptions, ProgressLocation, MarkdownString } from 'vscode'; +import { IExtensionActivationService } from '../../activation/types'; +import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; import { IPlatformService } from '../../common/platform/types'; import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; @@ -14,6 +14,7 @@ import { Resource, IDisposableRegistry, IConfigurationService, + IPathUtils, } from '../../common/types'; import { Deferred, createDeferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; @@ -23,7 +24,7 @@ import { defaultShells } from './service'; import { IEnvironmentActivationService } from './types'; @injectable() -export class TerminalEnvVarCollectionService implements IExtensionSingleActivationService { +export class TerminalEnvVarCollectionService implements IExtensionActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false, @@ -31,6 +32,8 @@ export class TerminalEnvVarCollectionService implements IExtensionSingleActivati private deferred: Deferred | undefined; + private registeredOnce = false; + private previousEnvVars = _normCaseKeys(process.env); constructor( @@ -42,39 +45,51 @@ export class TerminalEnvVarCollectionService implements IExtensionSingleActivati @inject(IApplicationEnvironment) private applicationEnvironment: IApplicationEnvironment, @inject(IDisposableRegistry) private disposables: IDisposableRegistry, @inject(IEnvironmentActivationService) private environmentActivationService: IEnvironmentActivationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IPathUtils) private readonly pathUtils: IPathUtils, ) {} - public async activate(): Promise { + public async activate(resource: Resource): Promise { if (!inTerminalEnvVarExperiment(this.experimentService)) { this.context.environmentVariableCollection.clear(); return; } - this.interpreterService.onDidChangeInterpreter( - async (resource) => { - this.showProgress(); - await this._applyCollection(resource); - this.hideProgress(); - }, - this, - this.disposables, - ); - this.applicationEnvironment.onDidChangeShell( - async (shell: string) => { - this.showProgress(); - // Pass in the shell where known instead of relying on the application environment, because of bug - // on VSCode: https://github.com/microsoft/vscode/issues/160694 - await this._applyCollection(undefined, shell); - this.hideProgress(); - }, - this, - this.disposables, - ); - - this._applyCollection(undefined).ignoreErrors(); + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + this.showProgress(); + await this._applyCollection(r).ignoreErrors(); + this.hideProgress(); + }, + this, + this.disposables, + ); + this.applicationEnvironment.onDidChangeShell( + async (shell: string) => { + this.showProgress(); + // Pass in the shell where known instead of relying on the application environment, because of bug + // on VSCode: https://github.com/microsoft/vscode/issues/160694 + await this._applyCollection(undefined, shell).ignoreErrors(); + this.hideProgress(); + }, + this, + this.disposables, + ); + this.registeredOnce = true; + } + this._applyCollection(resource).ignoreErrors(); } public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { + let workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + if ( + !workspaceFolder && + Array.isArray(this.workspaceService.workspaceFolders) && + this.workspaceService.workspaceFolders.length > 0 + ) { + [workspaceFolder] = this.workspaceService.workspaceFolders; + } const settings = this.configurationService.getSettings(resource); if (!settings.terminal.activateEnvironment) { traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); @@ -95,7 +110,7 @@ export class TerminalEnvVarCollectionService implements IExtensionSingleActivati await this._applyCollection(resource, defaultShell?.shell); return; } - this.context.environmentVariableCollection.clear(); + this.context.environmentVariableCollection.clear({ workspaceFolder }); this.previousEnvVars = _normCaseKeys(process.env); return; } @@ -107,10 +122,10 @@ export class TerminalEnvVarCollectionService implements IExtensionSingleActivati if (prevValue !== value) { if (value !== undefined) { traceVerbose(`Setting environment variable ${key} in collection to ${value}`); - this.context.environmentVariableCollection.replace(key, value); + this.context.environmentVariableCollection.replace(key, value, { workspaceFolder }); } else { traceVerbose(`Clearing environment variable ${key} from collection`); - this.context.environmentVariableCollection.delete(key); + this.context.environmentVariableCollection.delete(key, { workspaceFolder }); } } }); @@ -118,9 +133,14 @@ export class TerminalEnvVarCollectionService implements IExtensionSingleActivati // If the previous env var is not in the current env, clear it from collection. if (!(key in env)) { traceVerbose(`Clearing environment variable ${key} from collection`); - this.context.environmentVariableCollection.delete(key); + this.context.environmentVariableCollection.delete(key, { workspaceFolder }); } }); + const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); + const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); + this.context.environmentVariableCollection.setDescription(description, { + workspaceFolder, + }); } @traceDecoratorVerbose('Display activating terminals') diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 15dd6de7b722..04af15415b04 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -109,8 +109,8 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); - serviceManager.addSingleton( - IExtensionSingleActivationService, + serviceManager.addSingleton( + IExtensionActivationService, TerminalEnvVarCollectionService, ); } diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 324de1174584..e5883903ae74 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -7,8 +7,12 @@ import * as sinon from 'sinon'; import { assert, expect } from 'chai'; import { cloneDeep } from 'lodash'; import { mock, instance, when, anything, verify, reset } from 'ts-mockito'; -import { EnvironmentVariableCollection, ProgressLocation, Uri } from 'vscode'; -import { IApplicationShell, IApplicationEnvironment } from '../../../client/common/application/types'; +import { EnvironmentVariableCollection, ProgressLocation, Uri, WorkspaceFolder } from 'vscode'; +import { + IApplicationShell, + IApplicationEnvironment, + IWorkspaceService, +} from '../../../client/common/application/types'; import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; import { IPlatformService } from '../../../client/common/platform/types'; import { @@ -19,7 +23,7 @@ import { IPythonSettings, } from '../../../client/common/types'; import { Interpreters } from '../../../client/common/utils/localize'; -import { getOSType } from '../../../client/common/utils/platform'; +import { OSType, getOSType } from '../../../client/common/utils/platform'; import { defaultShells } from '../../../client/interpreter/activation/service'; import { TerminalEnvVarCollectionService, @@ -27,6 +31,7 @@ import { } from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PathUtils } from '../../../client/common/platform/pathUtils'; suite('Terminal Environment Variable Collection Service', () => { let platform: IPlatformService; @@ -37,15 +42,21 @@ suite('Terminal Environment Variable Collection Service', () => { let collection: EnvironmentVariableCollection; let applicationEnvironment: IApplicationEnvironment; let environmentActivationService: IEnvironmentActivationService; + let workspaceService: IWorkspaceService; let terminalEnvVarCollectionService: TerminalEnvVarCollectionService; const progressOptions = { location: ProgressLocation.Window, title: Interpreters.activatingTerminals, }; + let configService: IConfigurationService; + const displayPath = 'display/path'; const customShell = 'powershell'; const defaultShell = defaultShells[getOSType()]; setup(() => { + workspaceService = mock(); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + when(workspaceService.workspaceFolders).thenReturn(undefined); platform = mock(); when(platform.osType).thenReturn(getOSType()); interpreterService = mock(); @@ -63,9 +74,10 @@ suite('Terminal Environment Variable Collection Service', () => { }) .thenResolve(); environmentActivationService = mock(); - const configService = mock(); + configService = mock(); when(configService.getSettings(anything())).thenReturn(({ terminal: { activateEnvironment: true }, + pythonPath: displayPath, } as unknown) as IPythonSettings); terminalEnvVarCollectionService = new TerminalEnvVarCollectionService( instance(platform), @@ -76,7 +88,9 @@ suite('Terminal Environment Variable Collection Service', () => { instance(applicationEnvironment), [], instance(environmentActivationService), + instance(workspaceService), instance(configService), + new PathUtils(getOSType() === OSType.Windows), ); }); @@ -89,7 +103,7 @@ suite('Terminal Environment Variable Collection Service', () => { applyCollectionStub.resolves(); when(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).thenReturn(); when(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).thenReturn(); - await terminalEnvVarCollectionService.activate(); + await terminalEnvVarCollectionService.activate(undefined); assert(applyCollectionStub.calledOnce, 'Collection not applied on activation'); }); @@ -101,7 +115,7 @@ suite('Terminal Environment Variable Collection Service', () => { when(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).thenReturn(); when(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).thenReturn(); - await terminalEnvVarCollectionService.activate(); + await terminalEnvVarCollectionService.activate(undefined); verify(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).never(); verify(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).never(); @@ -119,7 +133,7 @@ suite('Terminal Environment Variable Collection Service', () => { callback = cb; }); when(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).thenReturn(); - await terminalEnvVarCollectionService.activate(); + await terminalEnvVarCollectionService.activate(undefined); await callback!(resource); assert(applyCollectionStub.calledWithExactly(resource)); @@ -133,7 +147,7 @@ suite('Terminal Environment Variable Collection Service', () => { callback = cb; }); when(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).thenReturn(); - await terminalEnvVarCollectionService.activate(); + await terminalEnvVarCollectionService.activate(undefined); await callback!(customShell); assert(applyCollectionStub.calledWithExactly(undefined, customShell)); @@ -151,13 +165,74 @@ suite('Terminal Environment Variable Collection Service', () => { ), ).thenResolve(envVars); - when(collection.replace(anything(), anything())).thenResolve(); - when(collection.delete(anything())).thenResolve(); + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything(), anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); - verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda')).once(); - verify(collection.delete('PATH')).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + verify(collection.delete('PATH', anything())).once(); + }); + + test('Verify envs are not applied if env activation is disabled', async () => { + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; + delete envVars.PATH; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything(), anything())).thenResolve(); + reset(configService); + when(configService.getSettings(anything())).thenReturn(({ + terminal: { activateEnvironment: false }, + pythonPath: displayPath, + } as unknown) as IPythonSettings); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).never(); + verify(collection.delete('PATH', anything())).never(); + }); + + test('Verify correct scope is used when applying envs and setting description', async () => { + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; + delete envVars.PATH; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, customShell), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenCall((_e, _v, scope) => { + assert.deepEqual(scope, { workspaceFolder }); + return Promise.resolve(); + }); + when(collection.delete(anything(), anything())).thenCall((_e, scope) => { + assert.deepEqual(scope, { workspaceFolder }); + return Promise.resolve(); + }); + let description = ''; + when(collection.setDescription(anything(), anything())).thenCall((d, scope) => { + assert.deepEqual(scope, { workspaceFolder }); + description = d.value; + }); + + await terminalEnvVarCollectionService._applyCollection(resource, customShell); + + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + verify(collection.delete('PATH', anything())).once(); + expect(description).to.equal(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); }); test('Only relative changes to previously applied variables are applied to the collection', async () => { @@ -175,8 +250,8 @@ suite('Terminal Environment Variable Collection Service', () => { ), ).thenResolve(envVars); - when(collection.replace(anything(), anything())).thenResolve(); - when(collection.delete(anything())).thenResolve(); + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything(), anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); @@ -195,8 +270,8 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); - verify(collection.delete('CONDA_PREFIX')).once(); - verify(collection.delete('RANDOM_VAR')).once(); + verify(collection.delete('CONDA_PREFIX', anything())).once(); + verify(collection.delete('RANDOM_VAR', anything())).once(); }); test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { @@ -218,13 +293,13 @@ suite('Terminal Environment Variable Collection Service', () => { ), ).thenResolve(envVars); - when(collection.replace(anything(), anything())).thenResolve(); - when(collection.delete(anything())).thenResolve(); + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything(), anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); - verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda')).once(); - verify(collection.delete(anything())).never(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + verify(collection.delete(anything(), anything())).never(); }); test('If no activated variables are returned for default shell, clear collection', async () => { @@ -237,11 +312,13 @@ suite('Terminal Environment Variable Collection Service', () => { ), ).thenResolve(undefined); - when(collection.replace(anything(), anything())).thenResolve(); - when(collection.delete(anything())).thenResolve(); + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything(), anything())).thenResolve(); + when(collection.setDescription(anything(), anything())).thenReturn(); await terminalEnvVarCollectionService._applyCollection(undefined, defaultShell?.shell); - verify(collection.clear()).once(); + verify(collection.clear(anything())).once(); + verify(collection.setDescription(anything(), anything())).never(); }); }); diff --git a/types/vscode.proposed.envCollectionWorkspace.d.ts b/types/vscode.proposed.envCollectionWorkspace.d.ts new file mode 100644 index 000000000000..b1176a9d46c2 --- /dev/null +++ b/types/vscode.proposed.envCollectionWorkspace.d.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/171173 + + export interface EnvironmentVariableMutator { + readonly type: EnvironmentVariableMutatorType; + readonly value: string; + readonly scope: EnvironmentVariableScope | undefined; + } + + export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { + /** + * Sets a description for the environment variable collection, this will be used to describe the changes in the UI. + * @param description A description for the environment variable collection. + * @param scope Specific scope to which this description applies to. + */ + setDescription(description: string | MarkdownString | undefined, scope?: EnvironmentVariableScope): void; + replace(variable: string, value: string, scope?: EnvironmentVariableScope): void; + append(variable: string, value: string, scope?: EnvironmentVariableScope): void; + prepend(variable: string, value: string, scope?: EnvironmentVariableScope): void; + get(variable: string, scope?: EnvironmentVariableScope): EnvironmentVariableMutator | undefined; + delete(variable: string, scope?: EnvironmentVariableScope): void; + clear(scope?: EnvironmentVariableScope): void; + + } + + export type EnvironmentVariableScope = { + /** + * The workspace folder to which this collection applies to. If unspecified, collection applies to all workspace folders. + */ + workspaceFolder?: WorkspaceFolder; + }; +} From 60db593a1bd10830d627fae2319380ac1c3099cb Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 25 Apr 2023 14:17:39 -0700 Subject: [PATCH 063/336] Show message in logs when `distutils` is missing. (#21125) Fixes https://github.com/microsoft/vscode-python/issues/21120 --- pythonFiles/create_venv.py | 5 +++++ .../creation/provider/venvProgressAndTelemetry.ts | 11 +++++++++++ src/client/telemetry/index.ts | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pythonFiles/create_venv.py b/pythonFiles/create_venv.py index 749c36f8088b..cac084fd2222 100644 --- a/pythonFiles/create_venv.py +++ b/pythonFiles/create_venv.py @@ -161,12 +161,17 @@ def main(argv: Optional[Sequence[str]] = None) -> None: venv_installed = is_installed("venv") pip_installed = is_installed("pip") ensure_pip_installed = is_installed("ensurepip") + distutils_installed = is_installed("distutils") if not venv_installed: if sys.platform == "win32": raise VenvError("CREATE_VENV.VENV_NOT_FOUND") else: use_micro_venv = True + if not distutils_installed: + print("Install `python3-distutils` package or equivalent for your OS.") + print("On Debian/Ubuntu: `sudo apt install python3-distutils`") + raise VenvError("CREATE_VENV.DISTUTILS_NOT_INSTALLED") if venv_exists(args.name): # A virtual environment with same name exists. diff --git a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts index 818e06c3f630..e092c40c3fe0 100644 --- a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts +++ b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts @@ -29,6 +29,7 @@ const INSTALLING_PIP_MARKER = 'CREATE_VENV.INSTALLING_PIP'; const INSTALL_PIP_FAILED_MARKER = 'CREATE_VENV.INSTALL_PIP_FAILED'; const DOWNLOADING_PIP_MARKER = 'CREATE_VENV.DOWNLOADING_PIP'; const DOWNLOAD_PIP_FAILED_MARKER = 'CREATE_VENV.DOWNLOAD_PIP_FAILED'; +const DISTUTILS_NOT_INSTALLED_MARKER = 'CREATE_VENV.DISTUTILS_NOT_INSTALLED'; export class VenvProgressAndTelemetry { private readonly processed = new Set(); @@ -88,6 +89,16 @@ export class VenvProgressAndTelemetry { return PIP_NOT_INSTALLED_MARKER; }, ], + [ + DISTUTILS_NOT_INSTALLED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noDistUtils', + }); + return VENV_NOT_INSTALLED_MARKER; + }, + ], [ VENV_NOT_INSTALLED_MARKER, (_progress: CreateEnvironmentProgress) => { diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 9e86f29201ee..3f6a483ad687 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2057,7 +2057,7 @@ export interface IEventNamePropertyMapping { */ [EventName.ENVIRONMENT_FAILED]: { environmentType: 'venv' | 'conda' | 'microvenv'; - reason: 'noVenv' | 'noPip' | 'other'; + reason: 'noVenv' | 'noPip' | 'noDistUtils' | 'other'; }; /** * Telemetry event sent before installing packages. From d6269a47000012dfb34b7222c7b62c88fe8dd49a Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 26 Apr 2023 14:08:01 -0700 Subject: [PATCH 064/336] Add Pytest Logging On Workspace Level (#21133) works on https://github.com/microsoft/vscode-python/issues/21124 but is only half the solution. --- src/client/common/utils/localize.ts | 4 +++ .../pytest/pytestDiscoveryAdapter.ts | 14 ++++------ .../testController/workspaceTestAdapter.ts | 27 ++++++++++++------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 633d9426ba74..078f16541509 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -407,9 +407,13 @@ export namespace Testing { export const testNotConfigured = l10n.t('No test framework configured.'); export const cancelUnittestDiscovery = l10n.t('Canceled unittest test discovery'); export const errorUnittestDiscovery = l10n.t('Unittest test discovery error'); + export const cancelPytestDiscovery = l10n.t('Canceled pytest test discovery'); + export const errorPytestDiscovery = l10n.t('pytest test discovery error'); export const seePythonOutput = l10n.t('(see Output > Python)'); export const cancelUnittestExecution = l10n.t('Canceled unittest test execution'); export const errorUnittestExecution = l10n.t('Unittest test execution error'); + export const cancelPytestExecution = l10n.t('Canceled pytest test execution'); + export const errorPytestExecution = l10n.t('Pytest test execution error'); } export namespace OutdatedDebugger { diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index b9df9bb30725..792826f4c3a5 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -80,15 +80,11 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { resource: uri, }; const execService = await executionFactory.createActivatedEnvironment(creationOptions); - - try { - execService.exec( - ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), - spawnOptions, - ); - } catch (ex) { - console.error(ex); - } + execService + .exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) + .catch((ex) => { + deferred.reject(ex as Error); + }); return deferred.promise; } } diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index b339256c3e08..39efc67f7c7e 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -120,14 +120,17 @@ export class WorkspaceTestAdapter { // handle token and telemetry here sendTelemetryEvent(EventName.UNITTEST_RUN_ALL_FAILED, undefined); - const cancel = token?.isCancellationRequested + let cancel = token?.isCancellationRequested ? Testing.cancelUnittestExecution : Testing.errorUnittestExecution; + if (this.testProvider === 'pytest') { + cancel = token?.isCancellationRequested ? Testing.cancelPytestExecution : Testing.errorPytestExecution; + } traceError(`${cancel}\r\n`, ex); // Also report on the test view const message = util.format(`${cancel} ${Testing.seePythonOutput}\r\n`, ex); - const options = buildErrorNodeOptions(this.workspaceUri, message); + const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); const errorNode = createErrorTestItem(testController, options); testController.items.add(errorNode); @@ -310,15 +313,18 @@ export class WorkspaceTestAdapter { } catch (ex) { sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: true }); - const cancel = token?.isCancellationRequested + let cancel = token?.isCancellationRequested ? Testing.cancelUnittestDiscovery : Testing.errorUnittestDiscovery; + if (this.testProvider === 'pytest') { + cancel = token?.isCancellationRequested ? Testing.cancelPytestDiscovery : Testing.errorPytestDiscovery; + } traceError(`${cancel}\r\n`, ex); // Report also on the test view. const message = util.format(`${cancel} ${Testing.seePythonOutput}\r\n`, ex); - const options = buildErrorNodeOptions(this.workspaceUri, message); + const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); const errorNode = createErrorTestItem(testController, options); testController.items.add(errorNode); @@ -336,17 +342,19 @@ export class WorkspaceTestAdapter { // Check if there were any errors in the discovery process. if (rawTestData.status === 'error') { + const testingErrorConst = + this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; const { errors } = rawTestData; - traceError(Testing.errorUnittestDiscovery, '\r\n', errors!.join('\r\n\r\n')); + traceError(testingErrorConst, '\r\n', errors!.join('\r\n\r\n')); let errorNode = testController.items.get(`DiscoveryError:${workspacePath}`); const message = util.format( - `${Testing.errorUnittestDiscovery} ${Testing.seePythonOutput}\r\n`, + `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, errors!.join('\r\n\r\n'), ); if (errorNode === undefined) { - const options = buildErrorNodeOptions(this.workspaceUri, message); + const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); errorNode = createErrorTestItem(testController, options); testController.items.add(errorNode); } @@ -462,10 +470,11 @@ function populateTestTree( }); } -function buildErrorNodeOptions(uri: Uri, message: string): ErrorTestItemOptions { +function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { + const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error'; return { id: `DiscoveryError:${uri.fsPath}`, - label: `Unittest Discovery Error [${path.basename(uri.fsPath)}]`, + label: `${labelText} [${path.basename(uri.fsPath)}]`, error: message, }; } From 1f4bfe99fcd08949df5d737b47ad28aba00a7227 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 26 Apr 2023 16:39:08 -0700 Subject: [PATCH 065/336] Ensure microvenv is added to path after selection. (#21132) fixes https://github.com/microsoft/vscode-python/issues/21122 --- src/client/interpreter/activation/service.ts | 33 ++++++++--- .../terminalEnvVarCollectionService.ts | 56 ++++++++++++++++--- .../activation/service.unit.test.ts | 20 +++++++ ...rminalEnvVarCollectionService.unit.test.ts | 4 +- 4 files changed, 95 insertions(+), 18 deletions(-) diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index 48eccfe7bfca..e5da57227b19 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -6,6 +6,7 @@ import '../../common/extensions'; +import * as path from 'path'; import { inject, injectable } from 'inversify'; import { IWorkspaceService } from '../../common/application/types'; @@ -25,10 +26,18 @@ import { EventName } from '../../telemetry/constants'; import { IInterpreterService } from '../contracts'; import { IEnvironmentActivationService } from './types'; import { TraceOptions } from '../../logging/types'; -import { traceDecoratorError, traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging'; +import { + traceDecoratorError, + traceDecoratorVerbose, + traceError, + traceInfo, + traceVerbose, + traceWarn, +} from '../../logging'; import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda'; import { StopWatch } from '../../common/utils/stopWatch'; import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { getSearchPathEnvVarNames } from '../../common/utils/exec'; const ENVIRONMENT_PREFIX = 'e8b39361-0157-4923-80e1-22d70d46dee6'; const CACHE_DURATION = 10 * 60 * 1000; @@ -193,6 +202,11 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi shellInfo = { shellType: customShellType, shell }; } try { + const processService = await this.processServiceFactory.create(resource); + const customEnvVars = (await this.envVarsService.getEnvironmentVariables(resource)) ?? {}; + const hasCustomEnvVars = Object.keys(customEnvVars).length; + const env = hasCustomEnvVars ? customEnvVars : { ...this.currentProcess.env }; + let command: string | undefined; const [args, parse] = internalScripts.printEnvVariables(); args.forEach((arg, i) => { @@ -217,6 +231,16 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi ); traceVerbose(`Activation Commands received ${activationCommands} for shell ${shellInfo.shell}`); if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { + if (interpreter?.envType === EnvironmentType.Venv) { + const key = getSearchPathEnvVarNames()[0]; + if (env[key]) { + env[key] = `${path.dirname(interpreter.path)}${path.delimiter}${env[key]}`; + } else { + env[key] = `${path.dirname(interpreter.path)}`; + } + + return env; + } return undefined; } // Run the activate command collect the environment from it. @@ -226,11 +250,6 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi command = `${activationCommand} && echo '${ENVIRONMENT_PREFIX}' && python ${args.join(' ')}`; } - const processService = await this.processServiceFactory.create(resource); - const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); - const hasCustomEnvVars = Object.keys(customEnvVars).length; - const env = hasCustomEnvVars ? customEnvVars : { ...this.currentProcess.env }; - // Make sure python warnings don't interfere with getting the environment. However // respect the warning in the returned values const oldWarnings = env[PYTHON_WARNINGS]; @@ -283,7 +302,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi // that's the case, wait and try again. This happens especially on AzDo const excString = (exc as Error).toString(); if (condaRetryMessages.find((m) => excString.includes(m)) && tryCount < 10) { - traceVerbose(`Conda is busy, attempting to retry ...`); + traceInfo(`Conda is busy, attempting to retry ...`); result = undefined; tryCount += 1; await sleep(500); diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index e492414ecfd1..26852303d099 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as path from 'path'; import { inject, injectable } from 'inversify'; -import { ProgressOptions, ProgressLocation, MarkdownString } from 'vscode'; +import { ProgressOptions, ProgressLocation, MarkdownString, WorkspaceFolder } from 'vscode'; +import { pathExists } from 'fs-extra'; import { IExtensionActivationService } from '../../activation/types'; import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; @@ -22,6 +24,7 @@ import { traceDecoratorVerbose, traceVerbose } from '../../logging'; import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; import { IEnvironmentActivationService } from './types'; +import { EnvironmentType } from '../../pythonEnvironments/info'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService { @@ -53,6 +56,17 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async activate(resource: Resource): Promise { if (!inTerminalEnvVarExperiment(this.experimentService)) { this.context.environmentVariableCollection.clear(); + await this.handleMicroVenv(resource); + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + await this.handleMicroVenv(r); + }, + this, + this.disposables, + ); + this.registeredOnce = true; + } return; } if (!this.registeredOnce) { @@ -82,14 +96,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { - let workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); - if ( - !workspaceFolder && - Array.isArray(this.workspaceService.workspaceFolders) && - this.workspaceService.workspaceFolders.length > 0 - ) { - [workspaceFolder] = this.workspaceService.workspaceFolders; - } + const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); if (!settings.terminal.activateEnvironment) { traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); @@ -143,6 +150,37 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ }); } + private async handleMicroVenv(resource: Resource) { + const workspaceFolder = this.getWorkspaceFolder(resource); + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.envType === EnvironmentType.Venv) { + const activatePath = path.join(path.dirname(interpreter.path), 'activate'); + if (!(await pathExists(activatePath))) { + this.context.environmentVariableCollection.replace( + 'PATH', + `${path.dirname(interpreter.path)}${path.delimiter}${process.env.Path}`, + { + workspaceFolder, + }, + ); + return; + } + } + this.context.environmentVariableCollection.clear(); + } + + private getWorkspaceFolder(resource: Resource): WorkspaceFolder | undefined { + let workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + if ( + !workspaceFolder && + Array.isArray(this.workspaceService.workspaceFolders) && + this.workspaceService.workspaceFolders.length > 0 + ) { + [workspaceFolder] = this.workspaceService.workspaceFolders; + } + return workspaceFolder; + } + @traceDecoratorVerbose('Display activating terminals') private showProgress(): void { if (!this.deferred) { diff --git a/src/test/interpreters/activation/service.unit.test.ts b/src/test/interpreters/activation/service.unit.test.ts index 002189d412db..9b2c121c89b4 100644 --- a/src/test/interpreters/activation/service.unit.test.ts +++ b/src/test/interpreters/activation/service.unit.test.ts @@ -28,6 +28,7 @@ import { EnvironmentActivationService } from '../../../client/interpreter/activa import { IInterpreterService } from '../../../client/interpreter/contracts'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { getSearchPathEnvVarNames } from '../../../client/common/utils/exec'; const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6'; const defaultShells = { @@ -118,6 +119,25 @@ suite('Interpreters Activation - Python Environment Variables', () => { helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), ).once(); }); + test('Env variables returned for microvenv', async () => { + when(platform.osType).thenReturn(osType.value); + + const microVenv = { ...pythonInterpreter, envType: EnvironmentType.Venv }; + const key = getSearchPathEnvVarNames()[0]; + const varsFromEnv = { [key]: '/foo/bar' }; + + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), microVenv), + ).thenResolve(); + + const env = await service.getActivatedEnvironmentVariables(resource, microVenv); + + verify(platform.osType).once(); + expect(env).to.deep.equal(varsFromEnv); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), microVenv), + ).once(); + }); test('Validate command used to activation and printing env vars', async () => { const cmd = ['1', '2']; const envVars = { one: '1', two: '2' }; diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index e5883903ae74..feecf63f5577 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -117,11 +117,11 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService.activate(undefined); - verify(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).never(); + verify(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).once(); verify(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).never(); assert(applyCollectionStub.notCalled, 'Collection should not be applied on activation'); - verify(collection.clear()).once(); + verify(collection.clear()).atLeast(1); }); test('When interpreter changes, apply new activated variables to the collection', async () => { From acc890665e4177cc29cb70ac94c322c7ed85082f Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Wed, 26 Apr 2023 16:40:27 -0700 Subject: [PATCH 066/336] Fix workspaceFolderToken -> workspaceFolder (#21095) The UI string is currently using the internal variable name (workspaceFolderToken) instead of the one actually used in the file (workspaceFolder). This PR fixes that. --- src/client/common/utils/localize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 078f16541509..f32c4fec0ac9 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -351,7 +351,7 @@ export namespace DebugConfigStrings { export const enterManagePyPath = { title: l10n.t('Debug Django'), prompt: l10n.t( - "Enter the path to manage.py ('${workspaceFolderToken}' points to the root of the current workspace folder)", + "Enter the path to manage.py ('${workspaceFolder}' points to the root of the current workspace folder)", ), invalid: l10n.t('Enter a valid Python file path'), }; From 42075761a6462464635f56130eb321416156bbb9 Mon Sep 17 00:00:00 2001 From: Tomer Chachamu Date: Thu, 27 Apr 2023 17:48:47 +0100 Subject: [PATCH 067/336] Fix telemetry event when using debug file configuration (#21118) Co-authored-by: paulacamargo25 --- .../debugger/extension/configuration/providers/fileLaunch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/debugger/extension/configuration/providers/fileLaunch.ts b/src/client/debugger/extension/configuration/providers/fileLaunch.ts index 746e33ea86fd..edda7ed7e22d 100644 --- a/src/client/debugger/extension/configuration/providers/fileLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/fileLaunch.ts @@ -24,7 +24,7 @@ export async function buildFileLaunchDebugConfiguration( justMyCode: true, }; sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { - configurationType: DebugConfigurationType.launchFastAPI, + configurationType: DebugConfigurationType.launchFile, }); Object.assign(state.config, config); } From 7e7206701c85cccbb03ece18e10db0ff191ba12f Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Thu, 27 Apr 2023 17:38:44 -0600 Subject: [PATCH 068/336] Add GDPR classification tag for new event (#21135) --- src/client/telemetry/pylance.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 9348b0f39d03..905cacc5fbf2 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -137,6 +137,11 @@ "name" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ +/* __GDPR__ + "language_server/goto_def_inside_string" : { + "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ /* __GDPR__ "language_server/import_heuristic" : { "avgcost" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, From e0558ce5e73240f10de4d1b8105cefe381f682e5 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 1 May 2023 09:08:44 -0700 Subject: [PATCH 069/336] Revert "Default to `--no-cov` when debugging pytest" (#21152) Reverts microsoft/vscode-python#21048 Closes https://github.com/microsoft/vscode-python/issues/21146 Related https://github.com/microsoft/vscode-python/issues/19985 --- src/client/testing/testController/pytest/runner.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/client/testing/testController/pytest/runner.ts b/src/client/testing/testController/pytest/runner.ts index 83da819b861a..2c6cff724398 100644 --- a/src/client/testing/testController/pytest/runner.ts +++ b/src/client/testing/testController/pytest/runner.ts @@ -95,10 +95,6 @@ export class PytestRunner implements ITestsRunner { testArgs.push('--capture', 'no'); } - if (options.debug && !testArgs.some((a) => a.startsWith('--no-cov'))) { - testArgs.push('--no-cov'); - } - // Positional arguments control the tests to be run. const rawData = idToRawData.get(testNode.id); if (!rawData) { From f7479fa30737444432b67b44aa79e5a839b027ed Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 1 May 2023 10:51:26 -0700 Subject: [PATCH 070/336] Show formatter extension install prompt and not the old prompt (#21138) --- src/client/formatters/baseFormatter.ts | 18 ++++++------- .../prompts/installFormatterPrompt.ts | 25 ++++++++++++++----- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts index bffa03a8b1e1..64e7d15a3d45 100644 --- a/src/client/formatters/baseFormatter.ts +++ b/src/client/formatters/baseFormatter.ts @@ -8,7 +8,7 @@ import { IPythonToolExecutionService } from '../common/process/types'; import { IDisposableRegistry, IInstaller, Product } from '../common/types'; import { isNotebookCell } from '../common/utils/misc'; import { IServiceContainer } from '../ioc/types'; -import { traceError, traceLog } from '../logging'; +import { traceError } from '../logging'; import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor'; import { IFormatterHelper } from './types'; import { IInstallFormatterPrompt } from '../providers/prompts/types'; @@ -16,6 +16,7 @@ import { IInstallFormatterPrompt } from '../providers/prompts/types'; export abstract class BaseFormatter { protected readonly workspace: IWorkspaceService; private readonly helper: IFormatterHelper; + private errorShown: boolean = false; constructor(public Id: string, private product: Product, protected serviceContainer: IServiceContainer) { this.helper = serviceContainer.get(IFormatterHelper); @@ -101,23 +102,22 @@ export abstract class BaseFormatter { } protected async handleError(_expectedFileName: string, error: Error, resource?: vscode.Uri) { - let customError = `Formatting with ${this.Id} failed.`; - if (isNotInstalledError(error)) { const prompt = this.serviceContainer.get(IInstallFormatterPrompt); if (!(await prompt.showInstallFormatterPrompt(resource))) { const installer = this.serviceContainer.get(IInstaller); const isInstalled = await installer.isInstalled(this.product, resource); - if (!isInstalled) { - customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`; - installer - .promptToInstall(this.product, resource) - .catch((ex) => traceError('Python Extension: promptToInstall', ex)); + if (!isInstalled && !this.errorShown) { + traceError( + `\nPlease install '${this.Id}' into your environment.`, + "\nIf you don't want to use it you can turn it off or use another formatter in the settings.", + ); + this.errorShown = true; } } } - traceLog(`\n${customError}\n${error}`); + traceError(`Formatting with ${this.Id} failed:\n${error}`); } /** diff --git a/src/client/providers/prompts/installFormatterPrompt.ts b/src/client/providers/prompts/installFormatterPrompt.ts index 5e9ff7f818ef..5743f8402053 100644 --- a/src/client/providers/prompts/installFormatterPrompt.ts +++ b/src/client/providers/prompts/installFormatterPrompt.ts @@ -21,17 +21,21 @@ const SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY = 'showFormatterExtensionInsta @injectable() export class InstallFormatterPrompt implements IInstallFormatterPrompt { - private shownThisSession = false; + private currentlyShown = false; constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} + /* + * This method is called when the user saves a python file or a cell. + * Returns true if an extension was selected. Otherwise returns false. + */ public async showInstallFormatterPrompt(resource?: Uri): Promise { if (!inFormatterExtensionExperiment(this.serviceContainer)) { return false; } const promptState = doNotShowPromptState(SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY, this.serviceContainer); - if (this.shownThisSession || promptState.value) { + if (this.currentlyShown || promptState.value) { return false; } @@ -53,7 +57,7 @@ export class InstallFormatterPrompt implements IInstallFormatterPrompt { let selection: string | undefined; if (black || autopep8) { - this.shownThisSession = true; + this.currentlyShown = true; if (black && autopep8) { selection = await showInformationMessage( ToolsExtensions.selectMultipleFormattersPrompt, @@ -81,7 +85,7 @@ export class InstallFormatterPrompt implements IInstallFormatterPrompt { } } } else if (formatter === 'black' && !black) { - this.shownThisSession = true; + this.currentlyShown = true; selection = await showInformationMessage( ToolsExtensions.installBlackFormatterPrompt, 'Black', @@ -89,7 +93,7 @@ export class InstallFormatterPrompt implements IInstallFormatterPrompt { Common.doNotShowAgain, ); } else if (formatter === 'autopep8' && !autopep8) { - this.shownThisSession = true; + this.currentlyShown = true; selection = await showInformationMessage( ToolsExtensions.installAutopep8FormatterPrompt, 'Black', @@ -98,23 +102,32 @@ export class InstallFormatterPrompt implements IInstallFormatterPrompt { ); } + let userSelectedAnExtension = false; if (selection === 'Black') { if (black) { + userSelectedAnExtension = true; await updateDefaultFormatter(BLACK_EXTENSION, resource); } else { + userSelectedAnExtension = true; await installFormatterExtension(BLACK_EXTENSION, resource); } } else if (selection === 'Autopep8') { if (autopep8) { + userSelectedAnExtension = true; await updateDefaultFormatter(AUTOPEP8_EXTENSION, resource); } else { + userSelectedAnExtension = true; await installFormatterExtension(AUTOPEP8_EXTENSION, resource); } } else if (selection === Common.doNotShowAgain) { + userSelectedAnExtension = false; await promptState.updateValue(true); + } else { + userSelectedAnExtension = false; } - return this.shownThisSession; + this.currentlyShown = false; + return userSelectedAnExtension; } } From 0db657725aa72b4e50db87601d3a015103f2bb17 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 1 May 2023 12:05:20 -0700 Subject: [PATCH 071/336] Release candidate (#21159) --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb01e68b0929..37755e84c724 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.7.0-dev", + "version": "2023.8.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.7.0-dev", + "version": "2023.8.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.77.0-20230309" + "vscode": "^1.78.0-20230421" } }, "node_modules/@azure/abort-controller": { diff --git a/package.json b/package.json index 6ac8ebcfa01c..2b4e78b74e46 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.7.0-dev", + "version": "2023.8.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From 4b5cc8d2cf049974f4ae5799aec4b9802f09df84 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 1 May 2023 13:45:44 -0700 Subject: [PATCH 072/336] Update main to next pre-release version (#21160) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37755e84c724..3131f89b671e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.8.0-rc", + "version": "2023.9.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.8.0-rc", + "version": "2023.9.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 2b4e78b74e46..a14f7074cdfa 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.8.0-rc", + "version": "2023.9.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 73cdbf179fe26d63213d37f6f015f4c35a7798e1 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 1 May 2023 17:03:17 -0700 Subject: [PATCH 073/336] Update extension to node 16 (#21161) For https://github.com/microsoft/vscode-python/issues/21144 --- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- .nvmrc | 2 +- package-lock.json | 53 ++++++------------- package.json | 2 +- src/client/common/process/logger.ts | 3 +- src/client/common/process/rawProcessApis.ts | 2 +- .../installer/poetryInstaller.unit.test.ts | 6 ++- src/test/common/process/proc.unit.test.ts | 12 +++-- src/test/proc.ts | 2 +- .../lowLevel/poetryLocator.unit.test.ts | 6 ++- .../environmentManagers/poetry.unit.test.ts | 17 +++--- 12 files changed, 47 insertions(+), 62 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f26f13ce50f4..805077ffdb46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: - 'release-*' env: - NODE_VERSION: 14.18.2 + NODE_VERSION: 16.17.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index ade1dcd61234..2ac560af995d 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -8,7 +8,7 @@ on: - release* env: - NODE_VERSION: 14.18.2 + NODE_VERSION: 16.17.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). Also enables a reporter which exits the process running the tests if it haven't already. ARTIFACT_NAME_VSIX: ms-python-insiders-vsix diff --git a/.nvmrc b/.nvmrc index ca4a60d1b111..e0325e5adb60 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v14.18.2 +v16.17.1 diff --git a/package-lock.json b/package-lock.json index 3131f89b671e..bfbd316f269d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "@types/md5": "^2.1.32", "@types/mocha": "^9.1.0", "@types/nock": "^10.0.3", - "@types/node": "^14.18.0", + "@types/node": "^16.17.0", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", @@ -1069,9 +1069,9 @@ } }, "node_modules/@types/node": { - "version": "14.18.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz", - "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==", + "version": "16.18.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.25.tgz", + "integrity": "sha512-rUDO6s9Q/El1R1I21HG4qw/LstTHCPO/oQNAwI/4b2f9EWvMnqt4d3HJwPMawfZ3UvodB8516Yg+VAq54YM+eA==", "dev": true }, "node_modules/@types/semver": { @@ -1410,9 +1410,9 @@ } }, "node_modules/@vscode/vsce": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.18.0.tgz", - "integrity": "sha512-tUA3XoKx5xjoi3EDcngk0VUYMhvfXLhS4s7CntpLPh1qtLYtgSCexTIMUHkCy6MqyozRW98bdW3a2yHPEADRnQ==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.19.0.tgz", + "integrity": "sha512-dAlILxC5ggOutcvJY24jxz913wimGiUrHaPkk16Gm9/PGFbz1YezWtrXsTKUtJws4fIlpX2UIlVlVESWq8lkfQ==", "dev": true, "dependencies": { "azure-devops-node-api": "^11.0.1", @@ -1432,7 +1432,7 @@ "tmp": "^0.2.1", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", - "xml2js": "^0.4.23", + "xml2js": "^0.5.0", "yauzl": "^2.3.1", "yazl": "^2.2.2" }, @@ -1491,19 +1491,6 @@ "node": ">=8.17.0" } }, - "node_modules/@vscode/vsce/node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -15965,9 +15952,9 @@ } }, "@types/node": { - "version": "14.18.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz", - "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==", + "version": "16.18.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.25.tgz", + "integrity": "sha512-rUDO6s9Q/El1R1I21HG4qw/LstTHCPO/oQNAwI/4b2f9EWvMnqt4d3HJwPMawfZ3UvodB8516Yg+VAq54YM+eA==", "dev": true }, "@types/semver": { @@ -16206,9 +16193,9 @@ } }, "@vscode/vsce": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.18.0.tgz", - "integrity": "sha512-tUA3XoKx5xjoi3EDcngk0VUYMhvfXLhS4s7CntpLPh1qtLYtgSCexTIMUHkCy6MqyozRW98bdW3a2yHPEADRnQ==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.19.0.tgz", + "integrity": "sha512-dAlILxC5ggOutcvJY24jxz913wimGiUrHaPkk16Gm9/PGFbz1YezWtrXsTKUtJws4fIlpX2UIlVlVESWq8lkfQ==", "dev": true, "requires": { "azure-devops-node-api": "^11.0.1", @@ -16229,7 +16216,7 @@ "tmp": "^0.2.1", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", - "xml2js": "^0.4.23", + "xml2js": "^0.5.0", "yauzl": "^2.3.1", "yazl": "^2.2.2" }, @@ -16266,16 +16253,6 @@ "requires": { "rimraf": "^3.0.0" } - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } } } }, diff --git a/package.json b/package.json index a14f7074cdfa..34af99bb21c6 100644 --- a/package.json +++ b/package.json @@ -1915,7 +1915,7 @@ "@types/md5": "^2.1.32", "@types/mocha": "^9.1.0", "@types/nock": "^10.0.3", - "@types/node": "^14.18.0", + "@types/node": "^16.17.0", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", diff --git a/src/client/common/process/logger.ts b/src/client/common/process/logger.ts index 5c8f04cbec30..1c0b78dd941f 100644 --- a/src/client/common/process/logger.ts +++ b/src/client/common/process/logger.ts @@ -28,7 +28,8 @@ export class ProcessLogger implements IProcessLogger { : fileOrCommand; const info = [`> ${this.getDisplayCommands(command)}`]; if (options?.cwd) { - info.push(`cwd: ${this.getDisplayCommands(options.cwd)}`); + const cwd: string = typeof options?.cwd === 'string' ? options?.cwd : options?.cwd?.toString(); + info.push(`cwd: ${this.getDisplayCommands(cwd)}`); } if (typeof options?.shell === 'string') { info.push(`shell: ${identifyShellFromShellPath(options?.shell)}`); diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts index fe54b3a8be87..025e5b607229 100644 --- a/src/client/common/process/rawProcessApis.ts +++ b/src/client/common/process/rawProcessApis.ts @@ -192,7 +192,7 @@ export function execObservable( let procExited = false; const disposable: IDisposable = { dispose() { - if (proc && !proc.killed && !procExited) { + if (proc && proc.pid && !proc.killed && !procExited) { killPid(proc.pid); } if (proc) { diff --git a/src/test/common/installer/poetryInstaller.unit.test.ts b/src/test/common/installer/poetryInstaller.unit.test.ts index 8c2c3614d184..07d60159138e 100644 --- a/src/test/common/installer/poetryInstaller.unit.test.ts +++ b/src/test/common/installer/poetryInstaller.unit.test.ts @@ -50,12 +50,14 @@ suite('Module Installer - Poetry', () => { switch (command) { case 'poetry env list --full-path': return Promise.resolve>({ stdout: '' }); - case 'poetry env info -p': - if (options.cwd && externalDependencies.arePathsSame(options.cwd, project1)) { + case 'poetry env info -p': { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (cwd && externalDependencies.arePathsSame(cwd, project1)) { return Promise.resolve>({ stdout: `${path.join(project1, '.venv')} \n`, }); } + } } return Promise.reject(new Error('Command failed')); }); diff --git a/src/test/common/process/proc.unit.test.ts b/src/test/common/process/proc.unit.test.ts index 4505ab817bef..38cf450bef57 100644 --- a/src/test/common/process/proc.unit.test.ts +++ b/src/test/common/process/proc.unit.test.ts @@ -36,14 +36,18 @@ suite('Process - Process Service', function () { test('Process is killed', async () => { const proc = spawnProc(); - - ProcessService.kill(proc.proc.pid); + expect(proc.proc.pid !== undefined).to.equal(true, 'invalid pid'); + if (proc.proc.pid) { + ProcessService.kill(proc.proc.pid); + } expect(await proc.exited.promise).to.equal(true, 'process did not die'); }); test('Process is alive', async () => { const proc = spawnProc(); - - expect(ProcessService.isAlive(proc.proc.pid)).to.equal(true, 'process is not alive'); + expect(proc.proc.pid !== undefined).to.equal(true, 'invalid pid'); + if (proc.proc.pid) { + expect(ProcessService.isAlive(proc.proc.pid)).to.equal(true, 'process is not alive'); + } }); }); diff --git a/src/test/proc.ts b/src/test/proc.ts index a25ae1aebfc0..8a21eb379f76 100644 --- a/src/test/proc.ts +++ b/src/test/proc.ts @@ -54,7 +54,7 @@ export class Proc { this.raw = (raw as unknown) as IRawProc; this.output = output; } - public get pid(): number { + public get pid(): number | undefined { return this.raw.pid; } public get exited(): boolean { diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts index 1f30aff26f04..95c1a401df54 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts @@ -36,7 +36,8 @@ suite('Poetry Locator', () => { getOSTypeStub.returns(platformUtils.OSType.Windows); shellExecute.callsFake((command: string, options: ShellOptions) => { if (command === 'poetry env list --full-path') { - if (options.cwd && externalDependencies.arePathsSame(options.cwd, project1)) { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (cwd && externalDependencies.arePathsSame(cwd, project1)) { return Promise.resolve>({ stdout: `${path.join(testPoetryDir, 'poetry-tutorial-project-6hnqYwvD-py3.8')} \n ${path.join(testPoetryDir, 'globalwinproject-9hvDnqYw-py3.11')} (Activated)\r\n @@ -76,7 +77,8 @@ suite('Poetry Locator', () => { getOSTypeStub.returns(platformUtils.OSType.Linux); shellExecute.callsFake((command: string, options: ShellOptions) => { if (command === 'poetry env list --full-path') { - if (options.cwd && externalDependencies.arePathsSame(options.cwd, project2)) { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (cwd && externalDependencies.arePathsSame(cwd, project2)) { return Promise.resolve>({ stdout: `${path.join(testPoetryDir, 'posix1project-9hvDnqYw-py3.4')} (Activated)\n ${path.join(testPoetryDir, 'posix2project-6hnqYwvD-py3.7')}`, diff --git a/src/test/pythonEnvironments/common/environmentManagers/poetry.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/poetry.unit.test.ts index 166b388a11c0..5e40e3454e2b 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/poetry.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/poetry.unit.test.ts @@ -123,10 +123,11 @@ suite('Poetry binary is located correctly', async () => { test('When user has specified a valid poetry path, use it', async () => { getPythonSetting.returns('poetryPath'); shellExecute.callsFake((command: string, options: ShellOptions) => { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); if ( command === `poetryPath env list --full-path` && - options.cwd && - externalDependencies.arePathsSame(options.cwd, project1) + cwd && + externalDependencies.arePathsSame(cwd, project1) ) { return Promise.resolve>({ stdout: '' }); } @@ -141,11 +142,8 @@ suite('Poetry binary is located correctly', async () => { test("When user hasn't specified a path, use poetry on PATH if available", async () => { getPythonSetting.returns('poetry'); // Setting returns the default value shellExecute.callsFake((command: string, options: ShellOptions) => { - if ( - command === `poetry env list --full-path` && - options.cwd && - externalDependencies.arePathsSame(options.cwd, project1) - ) { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (command === `poetry env list --full-path` && cwd && externalDependencies.arePathsSame(cwd, project1)) { return Promise.resolve>({ stdout: '' }); } return Promise.reject(new Error('Command failed')); @@ -168,10 +166,11 @@ suite('Poetry binary is located correctly', async () => { pathExistsSync.callThrough(); getPythonSetting.returns('poetry'); shellExecute.callsFake((command: string, options: ShellOptions) => { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); if ( command === `${defaultPoetry} env list --full-path` && - options.cwd && - externalDependencies.arePathsSame(options.cwd, project1) + cwd && + externalDependencies.arePathsSame(cwd, project1) ) { return Promise.resolve>({ stdout: '' }); } From 68fc3b1d045848fb66c7ddc068d8fb211370bcc1 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 1 May 2023 17:41:55 -0700 Subject: [PATCH 074/336] Migrate azure pipelines to node 16 (#21162) --- build/azure-pipeline.pre-release.yml | 2 +- build/azure-pipeline.stable.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index a5b95c91af9a..5bc61b2fd559 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -25,7 +25,7 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '14.18.2' + versionSpec: '16.17.1' displayName: Select Node version - task: UsePythonVersion@0 diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 76e1e0061ebf..159d856b6c3e 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -28,7 +28,7 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '14.18.2' + versionSpec: '16.17.1' displayName: Select Node version - task: UsePythonVersion@0 From 24c57bdca2bc0789c902aeede5d14ff4cfe9b939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Pi=C3=B1a=20Martinez?= Date: Wed, 3 May 2023 02:20:41 +0200 Subject: [PATCH 075/336] Fix typo in a type declaration of MultiStepInput module (#21156) --- src/client/common/utils/multiStepInput.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/common/utils/multiStepInput.ts b/src/client/common/utils/multiStepInput.ts index 22559e0dbdd1..e44879e8bbbb 100644 --- a/src/client/common/utils/multiStepInput.ts +++ b/src/client/common/utils/multiStepInput.ts @@ -76,7 +76,7 @@ interface InputBoxParameters { validate(value: string): Promise; } -type MultiStepInputQuickPicResponseType = T | (P extends { buttons: (infer I)[] } ? I : never) | undefined; +type MultiStepInputQuickPickResponseType = T | (P extends { buttons: (infer I)[] } ? I : never) | undefined; type MultiStepInputInputBoxResponseType

= string | (P extends { buttons: (infer I)[] } ? I : never) | undefined; export interface IMultiStepInput { run(start: InputStep, state: S): Promise; @@ -88,7 +88,7 @@ export interface IMultiStepInput { activeItem, placeholder, customButtonSetups, - }: P): Promise>; + }: P): Promise>; showInputBox

({ title, step, @@ -126,7 +126,7 @@ export class MultiStepInput implements IMultiStepInput { keepScrollPosition, sortByLabel, initialize, - }: P): Promise> { + }: P): Promise> { const disposables: Disposable[] = []; const input = this.shell.createQuickPick(); input.title = title; From f5eded283e9806d42da9377cc12916ea4b68ac6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Pi=C3=B1a=20Martinez?= Date: Wed, 3 May 2023 19:27:10 +0200 Subject: [PATCH 076/336] Insert debug configuration function on every quickpick item (#21165) Now there is no need to modify twice `pickDebugConfiguration` function. Just adding a new `DebugConfigurationQuickPickItem` element would be sufficient Co-authored-by: paulacamargo25 --- .../debugConfigurationService.ts | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/client/debugger/extension/configuration/debugConfigurationService.ts b/src/client/debugger/extension/configuration/debugConfigurationService.ts index ea0c84f7c3de..80a1e3a8a8c4 100644 --- a/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -114,64 +114,68 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi input: MultiStepInput, state: DebugConfigurationState, ): Promise | void> { - type DebugConfigurationQuickPickItem = QuickPickItem & { type: DebugConfigurationType }; + type DebugConfigurationQuickPickItemFunc = ( + input: MultiStepInput, + state: DebugConfigurationState, + ) => Promise>; + type DebugConfigurationQuickPickItem = QuickPickItem & { + type: DebugConfigurationType; + func: DebugConfigurationQuickPickItemFunc; + }; const items: DebugConfigurationQuickPickItem[] = [ { + func: buildFileLaunchDebugConfiguration, label: DebugConfigStrings.file.selectConfiguration.label, type: DebugConfigurationType.launchFile, description: DebugConfigStrings.file.selectConfiguration.description, }, { + func: buildModuleLaunchConfiguration, label: DebugConfigStrings.module.selectConfiguration.label, type: DebugConfigurationType.launchModule, description: DebugConfigStrings.module.selectConfiguration.description, }, { + func: buildRemoteAttachConfiguration, label: DebugConfigStrings.attach.selectConfiguration.label, type: DebugConfigurationType.remoteAttach, description: DebugConfigStrings.attach.selectConfiguration.description, }, { + func: buildPidAttachConfiguration, label: DebugConfigStrings.attachPid.selectConfiguration.label, type: DebugConfigurationType.pidAttach, description: DebugConfigStrings.attachPid.selectConfiguration.description, }, { + func: buildDjangoLaunchDebugConfiguration, label: DebugConfigStrings.django.selectConfiguration.label, type: DebugConfigurationType.launchDjango, description: DebugConfigStrings.django.selectConfiguration.description, }, { + func: buildFastAPILaunchDebugConfiguration, label: DebugConfigStrings.fastapi.selectConfiguration.label, type: DebugConfigurationType.launchFastAPI, description: DebugConfigStrings.fastapi.selectConfiguration.description, }, { + func: buildFlaskLaunchDebugConfiguration, label: DebugConfigStrings.flask.selectConfiguration.label, type: DebugConfigurationType.launchFlask, description: DebugConfigStrings.flask.selectConfiguration.description, }, { + func: buildPyramidLaunchConfiguration, label: DebugConfigStrings.pyramid.selectConfiguration.label, type: DebugConfigurationType.launchPyramid, description: DebugConfigStrings.pyramid.selectConfiguration.description, }, ]; - const debugConfigurations = new Map< - DebugConfigurationType, - ( - input: MultiStepInput, - state: DebugConfigurationState, - ) => Promise> - >(); - debugConfigurations.set(DebugConfigurationType.launchDjango, buildDjangoLaunchDebugConfiguration); - debugConfigurations.set(DebugConfigurationType.launchFastAPI, buildFastAPILaunchDebugConfiguration); - debugConfigurations.set(DebugConfigurationType.launchFile, buildFileLaunchDebugConfiguration); - debugConfigurations.set(DebugConfigurationType.launchFlask, buildFlaskLaunchDebugConfiguration); - debugConfigurations.set(DebugConfigurationType.launchModule, buildModuleLaunchConfiguration); - debugConfigurations.set(DebugConfigurationType.pidAttach, buildPidAttachConfiguration); - debugConfigurations.set(DebugConfigurationType.remoteAttach, buildRemoteAttachConfiguration); - debugConfigurations.set(DebugConfigurationType.launchPyramid, buildPyramidLaunchConfiguration); + const debugConfigurations = new Map(); + for (const config of items) { + debugConfigurations.set(config.type, config.func); + } state.config = {}; const pick = await input.showQuickPick< From 7dfa63125f02b22b23ea7c805442d4d9974839ca Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 3 May 2023 16:55:56 -0700 Subject: [PATCH 077/336] Add telemetry event when creating environment using button (#21172) A temporary command to understand the usage of the create environment button. Fixes https://github.com/microsoft/vscode-python/issues/21168 --- package.json | 9 +++++++-- src/client/common/constants.ts | 1 + .../pythonEnvironments/creation/createEnvApi.ts | 11 ++++++++++- src/client/telemetry/constants.ts | 1 + src/client/telemetry/index.ts | 7 +++++++ 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 34af99bb21c6..fd6ddcea84ca 100644 --- a/package.json +++ b/package.json @@ -277,6 +277,11 @@ "command": "python.createEnvironment", "title": "%python.command.python.createEnvironment.title%" }, + { + "category": "Python", + "command": "python.createEnvironment-button", + "title": "%python.command.python.createEnvironment.title%" + }, { "category": "Python", "command": "python.enableLinting", @@ -1700,12 +1705,12 @@ "editor/content": [ { "group": "Python", - "command": "python.createEnvironment", + "command": "python.createEnvironment-button", "when": "resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" }, { "group": "Python", - "command": "python.createEnvironment", + "command": "python.createEnvironment-button", "when": "resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" } ], diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 104504650aed..b285667aaa6a 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -37,6 +37,7 @@ export namespace Commands { export const CreateNewFile = 'python.createNewFile'; export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; export const Create_Environment = 'python.createEnvironment'; + export const Create_Environment_Button = 'python.createEnvironment-button'; export const Create_Terminal = 'python.createTerminal'; export const Debug_In_Terminal = 'python.debugInTerminal'; export const Enable_Linter = 'python.enableLinting'; diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts index 6445e0938025..8858fe92de9d 100644 --- a/src/client/pythonEnvironments/creation/createEnvApi.ts +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -4,7 +4,7 @@ import { ConfigurationTarget, Disposable } from 'vscode'; import { Commands } from '../../common/constants'; import { IDisposableRegistry, IInterpreterPathService, IPathUtils } from '../../common/types'; -import { registerCommand } from '../../common/vscodeApis/commandApis'; +import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; import { condaCreationProvider } from './provider/condaCreationProvider'; @@ -18,6 +18,8 @@ import { ProposedCreateEnvironmentAPI, EnvironmentDidCreateEvent, } from './proposed.createEnvApis'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; class CreateEnvironmentProviders { private _createEnvProviders: CreateEnvironmentProvider[] = []; @@ -67,6 +69,13 @@ export function registerCreateEnvironmentFeatures( return handleCreateEnvironmentCommand(providers, options); }, ), + registerCommand( + Commands.Create_Environment_Button, + async (): Promise => { + sendTelemetryEvent(EventName.ENVIRONMENT_BUTTON, undefined, undefined); + await executeCommand(Commands.Create_Environment); + }, + ), registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick)), registerCreateEnvironmentProvider(condaCreationProvider()), onCreateEnvironmentExited(async (e: EnvironmentDidCreateEvent) => { diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 778eb7cc39a0..159f5690e5c5 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -111,6 +111,7 @@ export enum EventName { ENVIRONMENT_INSTALLING_PACKAGES = 'ENVIRONMENT.INSTALLING_PACKAGES', ENVIRONMENT_INSTALLED_PACKAGES = 'ENVIRONMENT.INSTALLED_PACKAGES', ENVIRONMENT_INSTALLING_PACKAGES_FAILED = 'ENVIRONMENT.INSTALLING_PACKAGES_FAILED', + ENVIRONMENT_BUTTON = 'ENVIRONMENT.BUTTON', TOOLS_EXTENSIONS_ALREADY_INSTALLED = 'TOOLS_EXTENSIONS.ALREADY_INSTALLED', TOOLS_EXTENSIONS_PROMPT_SHOWN = 'TOOLS_EXTENSIONS.PROMPT_SHOWN', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 3f6a483ad687..5dff35067196 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2098,6 +2098,13 @@ export interface IEventNamePropertyMapping { environmentType: 'venv' | 'conda' | 'microvenv'; using: 'pipUpgrade' | 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipDownload' | 'pipInstall'; }; + /** + * Telemetry event sent if create environment button was used to trigger the command. + */ + /* __GDPR__ + "environment.button" : {"owner": "karthiknadig" } + */ + [EventName.ENVIRONMENT_BUTTON]: never | undefined; /** * Telemetry event sent when a linter or formatter extension is already installed. */ From 4112b048919ad97a676dfb0b6de992f06a4c5a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Pi=C3=B1a=20Martinez?= Date: Thu, 4 May 2023 02:09:19 +0200 Subject: [PATCH 078/336] Add some more coverage to utils functions (#21026) --- .../terminals/shellDetector.unit.test.ts | 2 +- src/test/common/utils/exec.unit.test.ts | 25 +++++++++ src/test/common/utils/filesystem.unit.test.ts | 50 ++++++++++++++++++ src/test/common/utils/platform.unit.test.ts | 23 +++++++++ src/test/common/utils/text.unit.test.ts | 51 ++++++++++++++++++- 5 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/test/common/utils/exec.unit.test.ts create mode 100644 src/test/common/utils/filesystem.unit.test.ts create mode 100644 src/test/common/utils/platform.unit.test.ts diff --git a/src/test/common/terminals/shellDetector.unit.test.ts b/src/test/common/terminals/shellDetector.unit.test.ts index 7985cf064981..c09560a3ea37 100644 --- a/src/test/common/terminals/shellDetector.unit.test.ts +++ b/src/test/common/terminals/shellDetector.unit.test.ts @@ -34,7 +34,7 @@ suite('Shell Detector', () => { getNamesAndValues(OSType).forEach((os) => { const testSuffix = `(OS ${os.name})`; - test('Test identification of Terminal Shells in order of priority', async () => { + test(`Test identification of Terminal Shells in order of priority ${testSuffix}`, async () => { const callOrder: string[] = []; const nameDetectorIdentify = sandbox.stub(TerminalNameShellDetector.prototype, 'identify'); nameDetectorIdentify.callsFake(() => { diff --git a/src/test/common/utils/exec.unit.test.ts b/src/test/common/utils/exec.unit.test.ts new file mode 100644 index 000000000000..aebfbe7a417d --- /dev/null +++ b/src/test/common/utils/exec.unit.test.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { OSType } from '../../common'; +import { getSearchPathEnvVarNames } from '../../../client/common/utils/exec'; + +suite('Utils for exec - getSearchPathEnvVarNames function', () => { + const testsData = [ + { os: 'Unknown', expected: ['PATH'] }, + { os: 'Windows', expected: ['Path', 'PATH'] }, + { os: 'OSX', expected: ['PATH'] }, + { os: 'Linux', expected: ['PATH'] }, + ]; + + testsData.forEach((testData) => { + test(`getSearchPathEnvVarNames when os is ${testData.os}`, () => { + const pathVariables = getSearchPathEnvVarNames(testData.os as OSType); + + expect(pathVariables).to.deep.equal(testData.expected); + }); + }); +}); diff --git a/src/test/common/utils/filesystem.unit.test.ts b/src/test/common/utils/filesystem.unit.test.ts new file mode 100644 index 000000000000..a1c53edc73e9 --- /dev/null +++ b/src/test/common/utils/filesystem.unit.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { convertFileType } from '../../../client/common/utils/filesystem'; + +class KnowsFileTypeDummyImpl { + private _isFile: boolean; + + private _isDirectory: boolean; + + private _isSymbolicLink: boolean; + + constructor(isFile = false, isDirectory = false, isSymbolicLink = false) { + this._isFile = isFile; + this._isDirectory = isDirectory; + this._isSymbolicLink = isSymbolicLink; + } + + public isFile() { + return this._isFile; + } + + public isDirectory() { + return this._isDirectory; + } + + public isSymbolicLink() { + return this._isSymbolicLink; + } +} + +suite('Utils for filesystem - convertFileType function', () => { + const testsData = [ + { info: new KnowsFileTypeDummyImpl(true, false, false), kind: 'File', expected: 1 }, + { info: new KnowsFileTypeDummyImpl(false, true, false), kind: 'Directory', expected: 2 }, + { info: new KnowsFileTypeDummyImpl(false, false, true), kind: 'Symbolic Link', expected: 64 }, + { info: new KnowsFileTypeDummyImpl(false, false, false), kind: 'Unknown', expected: 0 }, + ]; + + testsData.forEach((testData) => { + test(`convertFileType when info is a ${testData.kind}`, () => { + const fileType = convertFileType(testData.info); + + expect(fileType).equals(testData.expected); + }); + }); +}); diff --git a/src/test/common/utils/platform.unit.test.ts b/src/test/common/utils/platform.unit.test.ts new file mode 100644 index 000000000000..b27708978fc1 --- /dev/null +++ b/src/test/common/utils/platform.unit.test.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { OSType, getOSType } from '../../../client/common/utils/platform'; + +suite('Utils for platform - getOSType function', () => { + const testsData = [ + { platform: 'linux', expected: OSType.Linux }, + { platform: 'darwin', expected: OSType.OSX }, + { platform: 'anunknownplatform', expected: OSType.Unknown }, + { platform: 'windows', expected: OSType.Windows }, + ]; + + testsData.forEach((testData) => { + test(`getOSType when platform is ${testData.platform}`, () => { + const osType = getOSType(testData.platform); + expect(osType).equal(testData.expected); + }); + }); +}); diff --git a/src/test/common/utils/text.unit.test.ts b/src/test/common/utils/text.unit.test.ts index f15816c5159f..7e7a22896e9a 100644 --- a/src/test/common/utils/text.unit.test.ts +++ b/src/test/common/utils/text.unit.test.ts @@ -5,7 +5,7 @@ import { expect } from 'chai'; import { Position, Range } from 'vscode'; -import { parsePosition, parseRange } from '../../../client/common/utils/text'; +import { getDedentedLines, getIndent, parsePosition, parseRange } from '../../../client/common/utils/text'; suite('parseRange()', () => { test('valid strings', async () => { @@ -98,3 +98,52 @@ suite('parsePosition()', () => { } }); }); + +suite('getIndent()', () => { + const testsData = [ + { line: 'text', expected: '' }, + { line: ' text', expected: ' ' }, + { line: ' text', expected: ' ' }, + { line: ' tabulatedtext', expected: '' }, + ]; + + testsData.forEach((testData) => { + test(`getIndent when line is ${testData.line}`, () => { + const indent = getIndent(testData.line); + + expect(indent).equal(testData.expected); + }); + }); +}); + +suite('getDedentedLines()', () => { + const testsData = [ + { text: '', expected: [] }, + { text: '\n', expected: Error, exceptionMessage: 'expected "first" line to not be blank' }, + { text: 'line1\n', expected: Error, exceptionMessage: 'expected actual first line to be blank' }, + { + text: '\n line2\n line3', + expected: Error, + exceptionMessage: 'line 1 has less indent than the "first" line', + }, + { + text: '\n line2\n line3', + expected: ['line2', 'line3'], + }, + { + text: '\n line2\n line3', + expected: ['line2', ' line3'], + }, + ]; + + testsData.forEach((testData) => { + test(`getDedentedLines when line is ${testData.text}`, () => { + if (Array.isArray(testData.expected)) { + const dedentedLines = getDedentedLines(testData.text); + expect(dedentedLines).to.deep.equal(testData.expected); + } else { + expect(() => getDedentedLines(testData.text)).to.throw(testData.expected, testData.exceptionMessage); + } + }); + }); +}); From 6bdada01fe838efad8a9be6c5b43394bf10c2f15 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 4 May 2023 13:51:27 -0700 Subject: [PATCH 079/336] Use `saveEditor` proposed API for running untitled Python files (#21183) Closes https://github.com/microsoft/vscode-python/issues/21182 --- package.json | 3 +- src/client/common/application/types.ts | 10 ++++++ src/client/common/application/workspace.ts | 10 ++++++ src/client/terminals/codeExecution/helper.ts | 10 ++---- .../terminals/codeExecution/helper.test.ts | 23 +++++++------ .../vscode.proposed.saveEditor.d.ts | 34 +++++++++++++++++++ 6 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 typings/vscode-proposed/vscode.proposed.saveEditor.d.ts diff --git a/package.json b/package.json index fd6ddcea84ca..0d64b360e43b 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "envShellEvent", "testObserver", "quickPickItemTooltip", - "envCollectionWorkspace" + "envCollectionWorkspace", + "saveEditor" ], "author": { "name": "Microsoft Corporation" diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 77d7b5af3279..e57bac656d19 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -851,6 +851,16 @@ export interface IWorkspaceService { * @return A promise that resolves to a {@link TextDocument document}. */ openTextDocument(options?: { language?: string; content?: string }): Thenable; + /** + * Saves the editor identified by the given resource to a new file name as provided by the user and + * returns the resulting resource or `undefined` if save was not successful or cancelled. + * + * **Note** that an editor with the provided resource must be opened in order to be saved as. + * + * @param uri the associated uri for the opened editor to save as. + * @return A thenable that resolves when the save-as operation has finished. + */ + saveAs(uri: Uri): Thenable; } export const ITerminalManager = Symbol('ITerminalManager'); diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 0a5fd8d81816..11ed98cf0076 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -112,4 +112,14 @@ export class WorkspaceService implements IWorkspaceService { const enabledSearchExcludes = Object.keys(searchExcludes).filter((key) => searchExcludes.get(key) === true); return `{${enabledSearchExcludes.join(',')}}`; } + + public async saveAs(uri: Uri): Promise { + try { + // This is a proposed API hence putting it inside try...catch. + const result = await workspace.saveAs(uri); + return result; + } catch (ex) { + return undefined; + } + } } diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index eee7d91a0db2..0a1ae9f0be06 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -5,7 +5,7 @@ import '../../common/extensions'; import { inject, injectable } from 'inversify'; import { l10n, Position, Range, TextEditor, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../../common/application/types'; +import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../common/application/types'; import { PYTHON_LANGUAGE } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; import { IProcessServiceFactory } from '../../common/process/types'; @@ -123,12 +123,8 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { public async saveFileIfDirty(file: Uri): Promise { const docs = this.documentManager.textDocuments.filter((d) => d.uri.path === file.path); if (docs.length === 1 && docs[0].isDirty) { - const deferred = createDeferred(); - this.documentManager.onDidSaveTextDocument((e) => deferred.resolve(e.uri)); - const commandManager = this.serviceContainer.get(ICommandManager); - await commandManager.executeCommand('workbench.action.files.save', file); - const savedFileUri = await deferred.promise; - return savedFileUri; + const workspaceService = this.serviceContainer.get(IWorkspaceService); + return workspaceService.saveAs(docs[0].uri); } return undefined; } diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index 57bf51883eb8..684ca22b6c75 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -8,8 +8,13 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import { EventEmitter, Position, Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../../../client/common/application/types'; +import { Position, Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} from '../../../client/common/application/types'; import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../../client/common/constants'; import '../../../client/common/extensions'; import { ProcessService } from '../../../client/common/process/proc'; @@ -38,6 +43,7 @@ suite('Terminal - Code Execution Helper', () => { let processService: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; let commandManager: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; const workingPython: PythonEnvironment = { path: PYTHON_PATH, version: new SemVer('3.6.6-final'), @@ -51,6 +57,7 @@ suite('Terminal - Code Execution Helper', () => { setup(() => { const serviceContainer = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); documentManager = TypeMoq.Mock.ofType(); applicationShell = TypeMoq.Mock.ofType(); const envVariablesProvider = TypeMoq.Mock.ofType(); @@ -69,6 +76,9 @@ suite('Terminal - Code Execution Helper', () => { envVariablesProvider .setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny())) .returns(() => Promise.resolve({})); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())) .returns(() => processServiceFactory.object); @@ -367,20 +377,13 @@ suite('Terminal - Code Execution Helper', () => { .setup((d) => d.textDocuments) .returns(() => [document.object]) .verifiable(TypeMoq.Times.once()); - const saveEmitter = new EventEmitter(); - documentManager.setup((d) => d.onDidSaveTextDocument).returns(() => saveEmitter.event); document.setup((doc) => doc.isUntitled).returns(() => true); document.setup((doc) => doc.isDirty).returns(() => true); document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); const untitledUri = Uri.file('Untitled-1'); document.setup((doc) => doc.uri).returns(() => untitledUri); - const savedDocument = TypeMoq.Mock.ofType(); const expectedSavedUri = Uri.file('one.py'); - savedDocument.setup((doc) => doc.uri).returns(() => expectedSavedUri); - commandManager - .setup((c) => c.executeCommand('workbench.action.files.save', untitledUri)) - .callback(() => saveEmitter.fire(savedDocument.object)) - .returns(() => Promise.resolve()); + workspaceService.setup((w) => w.saveAs(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedSavedUri)); const savedUri = await helper.saveFileIfDirty(untitledUri); diff --git a/typings/vscode-proposed/vscode.proposed.saveEditor.d.ts b/typings/vscode-proposed/vscode.proposed.saveEditor.d.ts new file mode 100644 index 000000000000..9088939a4649 --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.saveEditor.d.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// https://github.com/microsoft/vscode/issues/178713 + +declare module 'vscode' { + + export namespace workspace { + + /** + * Saves the editor identified by the given resource and returns the resulting resource or `undefined` + * if save was not successful. + * + * **Note** that an editor with the provided resource must be opened in order to be saved. + * + * @param uri the associated uri for the opened editor to save. + * @return A thenable that resolves when the save operation has finished. + */ + export function save(uri: Uri): Thenable; + + /** + * Saves the editor identified by the given resource to a new file name as provided by the user and + * returns the resulting resource or `undefined` if save was not successful or cancelled. + * + * **Note** that an editor with the provided resource must be opened in order to be saved as. + * + * @param uri the associated uri for the opened editor to save as. + * @return A thenable that resolves when the save-as operation has finished. + */ + export function saveAs(uri: Uri): Thenable; + } +} From 678f70dbf5d57bde3fee9c6c6e88adef899a752b Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Thu, 4 May 2023 14:00:18 -0700 Subject: [PATCH 080/336] Remove hack to check the vscode version (#21180) Closed: #20769 --- package-lock.json | 2 +- package.json | 2 +- src/test/debuggerTest.ts | 4 ++-- src/test/multiRootTest.ts | 4 ++-- src/test/standardTest.ts | 4 ++-- src/test/utils/vscode.ts | 12 ++---------- 6 files changed, 10 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index bfbd316f269d..f00cbb0a912e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.78.0-20230421" + "vscode": "^1.78.0" } }, "node_modules/@azure/abort-controller": { diff --git a/package.json b/package.json index 0d64b360e43b..34d76ae4d9e8 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.78.0-20230421" + "vscode": "^1.78.0" }, "keywords": [ "python", diff --git a/src/test/debuggerTest.ts b/src/test/debuggerTest.ts index 949f14caee3d..36a0060b9303 100644 --- a/src/test/debuggerTest.ts +++ b/src/test/debuggerTest.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; -import { getChannel } from './utils/vscode'; +import { getVersion } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = '1'; @@ -17,7 +17,7 @@ function start() { extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), launchArgs: [workspacePath], - version: getChannel(), + version: getVersion(), extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Debugger tests (with errors)', ex); diff --git a/src/test/multiRootTest.ts b/src/test/multiRootTest.ts index c8c63b6dabe5..9a6d7cd1b904 100644 --- a/src/test/multiRootTest.ts +++ b/src/test/multiRootTest.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; import { initializeLogger } from './testLogger'; -import { getChannel } from './utils/vscode'; +import { getVersion } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; @@ -17,7 +17,7 @@ function start() { extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), launchArgs: [workspacePath], - version: getChannel(), + version: getVersion(), extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Multiroot tests (with errors)', ex); diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts index 0562d1adf431..c8416ebf1d7d 100644 --- a/src/test/standardTest.ts +++ b/src/test/standardTest.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTests } from '@vscode/test-electron'; import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../client/common/constants'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; -import { getChannel } from './utils/vscode'; +import { getVersion } from './utils/vscode'; // If running smoke tests, we don't have access to this. if (process.env.TEST_FILES_SUFFIX !== 'smoke.test') { @@ -76,7 +76,7 @@ async function installPylanceExtension(vscodeExecutablePath: string) { async function start() { console.log('*'.repeat(100)); console.log('Start Standard tests'); - const channel = getChannel(); + const channel = getVersion(); console.log(`Using ${channel} build of VS Code.`); const vscodeExecutablePath = await downloadAndUnzipVSCode(channel); const baseLaunchArgs = diff --git a/src/test/utils/vscode.ts b/src/test/utils/vscode.ts index a10ceb2e8881..44f745dbaf90 100644 --- a/src/test/utils/vscode.ts +++ b/src/test/utils/vscode.ts @@ -2,22 +2,14 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -const insidersVersion = /^\^(\d+\.\d+\.\d+)-(insider|\d{8})$/; - -export function getChannel(): string { +export function getVersion(): string { if (process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL) { return process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL; } const packageJsonPath = path.join(EXTENSION_ROOT_DIR, 'package.json'); if (fs.pathExistsSync(packageJsonPath)) { const packageJson = fs.readJSONSync(packageJsonPath); - const engineVersion = packageJson.engines.vscode; - if (insidersVersion.test(engineVersion)) { - // Can't pass in the version number for an insiders build; - // https://github.com/microsoft/vscode-test/issues/176 - return 'insiders'; - } - return engineVersion.replace('^', ''); + return packageJson.engines.vscode.replace('^', ''); } return 'stable'; } From f0253e5662aca4d7e8265df0ccc0598f8f3787c4 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 4 May 2023 19:47:34 -0700 Subject: [PATCH 081/336] Use actions from `vscode-github-triage-actions` in all Python automations (#21178) --- .../community-feedback-auto-comment.yml | 21 ++-- .github/workflows/issue-labels.yml | 62 +++-------- .github/workflows/lock-issues.yml | 18 +++- .github/workflows/pr-file-check.yml | 32 ++---- .github/workflows/pr-labels.yml | 16 ++- .../workflows/test-plan-item-validator.yml | 2 + .github/workflows/triage-info-needed.yml | 101 +++++------------- 7 files changed, 86 insertions(+), 166 deletions(-) diff --git a/.github/workflows/community-feedback-auto-comment.yml b/.github/workflows/community-feedback-auto-comment.yml index cbba6db30b50..57bbd97bf430 100644 --- a/.github/workflows/community-feedback-auto-comment.yml +++ b/.github/workflows/community-feedback-auto-comment.yml @@ -13,18 +13,17 @@ jobs: issues: write steps: - - name: Check For Existing Comment - uses: peter-evans/find-comment@v2 - id: finder + - name: Checkout Actions + uses: actions/checkout@v3 with: - issue-number: ${{ github.event.issue.number }} - comment-author: 'github-actions[bot]' - body-includes: Thanks for the feature request! We are going to give the community + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + + - name: Install Actions + run: npm install --production --prefix ./actions - - name: Add Community Feedback Comment - if: steps.finder.outputs.comment-id == '' - uses: peter-evans/create-or-update-comment@v3 + - name: Add Community Feedback Comment if applicable + uses: ./actions/python-community-feedback-auto-comment with: issue-number: ${{ github.event.issue.number }} - body: | - Thanks for the feature request! We are going to give the community 60 days from when this issue was created to provide 7 👍 upvotes on the opening comment to gauge general interest in this idea. If there's enough upvotes then we will consider this feature request in our future planning. If there's unfortunately not enough upvotes then we will close this issue. diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index 520592be2fce..98ac4eaca81d 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -18,55 +18,19 @@ jobs: name: "Add 'triage-needed' and remove unrecognizable labels & assignees" runs-on: ubuntu-latest steps: - - uses: actions/github-script@v6 + - name: Checkout Actions + uses: actions/checkout@v3 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const result = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }) - const labels = result.data.map((label) => label.name) - const hasNeedsOrTPI = labels.some((label) => (label.startsWith('needs') || label === 'testplan-item' || label.startsWith('iteration-plan') || label === 'release-plan')) + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions - if (!hasNeedsOrTPI) { - console.log('This issue is not labeled with a "needs __", "iteration-plan", "release-plan", or the "testplan-item" label; add the "triage-needed" label.') + - name: Install Actions + run: npm install --production --prefix ./actions - github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: ['triage-needed'] - }) - } else { - console.log('This issue already has a "needs __", "iteration-plan", "release-plan", or the "testplan-item" label, do not add the "triage-needed" label.') - } - const knownTriagers = ${{ env.TRIAGERS }} - const currentAssignees = await github.rest.issues - .get({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }) - .then((result) => result.data.assignees.map((a) => a.login)); - console.log('Known triagers:', JSON.stringify(knownTriagers)); - console.log('Current assignees:', JSON.stringify(currentAssignees)); - const assigneesToRemove = currentAssignees.filter(a => !knownTriagers.includes(a)); - console.log('Assignees to remove:', JSON.stringify(assigneesToRemove)); - github.rest.issues.removeAssignees({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - assignees: assigneesToRemove, - }); - const knownLabels = ${{ env.REPO_LABELS }} - for( const label of labels) { - if (!knownLabels.includes(label)) { - await github.rest.issues.deleteLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label, - }) - } - } + - name: "Add 'triage-needed' and remove unrecognizable labels & assignees" + uses: ./actions/python-issue-labels + with: + triagers: ${{ env.TRIAGERS }} + token: ${{secrets.GITHUB_TOKEN}} + repo_labels: ${{ env.REPO_LABELS }} diff --git a/.github/workflows/lock-issues.yml b/.github/workflows/lock-issues.yml index 6417d415fcfe..8c828ff766cb 100644 --- a/.github/workflows/lock-issues.yml +++ b/.github/workflows/lock-issues.yml @@ -15,9 +15,17 @@ jobs: lock-issues: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - name: Checkout Actions + uses: actions/checkout@v3 with: - github-token: ${{ github.token }} - issue-inactive-days: '30' - process-only: 'issues' - log-output: true + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: 'Lock Issues' + uses: ./actions/python-lock-issues + with: + token: ${{ github.token }} diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index 3eb7ad7b6338..258e07daace7 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -15,29 +15,15 @@ jobs: name: 'Check for changed files' runs-on: ubuntu-latest steps: - - name: 'package-lock.json matches package.json' - uses: brettcannon/check-for-changed-files@v1.1.0 + - name: Checkout Actions + uses: actions/checkout@v3 with: - prereq-pattern: 'package.json' - file-pattern: 'package-lock.json' - skip-label: 'skip package*.json' - failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions - - name: 'package.json matches package-lock.json' - uses: brettcannon/check-for-changed-files@v1.1.0 - with: - prereq-pattern: 'package-lock.json' - file-pattern: 'package.json' - skip-label: 'skip package*.json' - failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' + - name: Install Actions + run: npm install --production --prefix ./actions - - name: 'Tests' - uses: brettcannon/check-for-changed-files@v1.1.0 - with: - prereq-pattern: src/**/*.ts - file-pattern: | - src/**/*.test.ts - src/**/*.testvirtualenvs.ts - .github/test_plan.md - skip-label: 'skip tests' - failure-message: 'TypeScript code was edited without also editing a ${file-pattern} file; see the Testing page in our wiki on testing guidelines (the ${skip-label} label can be used to pass this check)' + - name: Check for changed files + uses: ./actions/python-pr-file-check diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index e953f62d2011..7ddb781e2a85 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -13,9 +13,15 @@ jobs: name: 'Classify PR' runs-on: ubuntu-latest steps: - - name: 'PR impact specified' - uses: mheap/github-action-required-labels@v4 + - name: Checkout Actions + uses: actions/checkout@v3 with: - mode: exactly - count: 1 - labels: 'bug, debt, feature-request, no-changelog' + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: Classify PR + uses: ./actions/python-pr-labels diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 43713049f5e8..9d0805a9db9b 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -17,8 +17,10 @@ jobs: repository: 'microsoft/vscode-github-triage-actions' path: ./actions ref: stable + - name: Install Actions run: npm install --production --prefix ./actions + - name: Run Test Plan Item Validator uses: ./actions/test-plan-item-validator with: diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index 51c5b610f03f..1c384d824da5 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -11,86 +11,41 @@ jobs: add_label: runs-on: ubuntu-latest if: contains(github.event.issue.labels.*.name, 'triage-needed') && !contains(github.event.issue.labels.*.name, 'info-needed') - env: - KEYWORDS: '["\\?", "please", "kindly", "let me know", "try", "can you", "could you", "would you", "may I", "provide", "let us know", "tell me", "give me", "send me", "what", "when", "where", "why", "how"]' steps: - - name: Check for author - uses: actions/github-script@v6 + - name: Checkout Actions + uses: actions/checkout@v3 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const issue = await github.rest.issues.get({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - const commentAuthor = context.payload.comment.user.login; - const commentBody = context.payload.comment.body; - const isTeamMember = ${{ env.TRIAGERS }}.includes(commentAuthor); + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions - const keywords = ${{ env.KEYWORDS }}; - const isRequestForInfo = new RegExp(keywords.join('|'), 'i').test(commentBody); + - name: Install Actions + run: npm install --production --prefix ./actions - const shouldAddLabel = isTeamMember && commentAuthor !== issue.data.user.login && isRequestForInfo; - - if (shouldAddLabel) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: ['info-needed'] - }); - } + - name: Add "info-needed" label + uses: ./actions/python-triage-info-needed + with: + triagers: ${{ env.TRIAGERS }} + action: 'add' + token: ${{secrets.GITHUB_TOKEN}} remove_label: if: contains(github.event.issue.labels.*.name, 'info-needed') && contains(github.event.issue.labels.*.name, 'triage-needed') runs-on: ubuntu-latest steps: - - name: Check for author - uses: actions/github-script@v6 + - name: Checkout Actions + uses: actions/checkout@v3 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: Remove "info-needed" label + uses: ./actions/python-triage-info-needed with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const issue = await github.rest.issues.get({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - const commentAuthor = context.payload.comment.user.login; - const issueAuthor = issue.data.user.login; - if (commentAuthor === issueAuthor) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name: 'info-needed' - }); - return; - } - if (${{ env.TRIAGERS }}.includes(commentAuthor)) { - // If one of triagers made a comment, ignore it - return; - } - // Loop through all the comments on the issue in reverse order and find the last username that a TRIAGER mentioned - // If the comment author is the last mentioned username, remove the "info-needed" label - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - for (const comment of comments.data.slice().reverse()) { - if (!${{ env.TRIAGERS }}.includes(comment.user.login)) { - continue; - } - const matches = comment.body.match(/@\w+/g) || []; - const mentionedUsernames = matches.map(match => match.replace('@', '')); - if (mentionedUsernames.includes(commentAuthor)) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name: 'info-needed' - }); - break; - } - } + triagers: ${{ env.TRIAGERS }} + action: 'remove' + token: ${{secrets.GITHUB_TOKEN}} From 17daae4208001c894229bbe794822e5f3d2fcb5e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 9 May 2023 08:18:40 -0700 Subject: [PATCH 082/336] Open separate Python terminals when running different Python files (#21202) Closes https://github.com/microsoft/vscode-python/issues/21097 Closes https://github.com/microsoft/vscode-python/issues/14094 --- src/client/common/terminal/factory.ts | 9 ++++- .../codeExecution/terminalCodeExecution.ts | 37 +++++++++---------- .../common/terminals/factory.unit.test.ts | 4 +- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index 3855cb6cee3c..3cbe123b5629 100644 --- a/src/client/common/terminal/factory.ts +++ b/src/client/common/terminal/factory.ts @@ -3,6 +3,7 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; +import * as path from 'path'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; @@ -26,10 +27,14 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { public getTerminalService(options: TerminalCreationOptions): ITerminalService { const resource = options?.resource; const title = options?.title; - const terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; + let terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; const interpreter = options?.interpreter; const id = this.getTerminalId(terminalTitle, resource, interpreter); if (!this.terminalServices.has(id)) { + if (this.terminalServices.size >= 1 && resource) { + terminalTitle = `${terminalTitle}: ${path.basename(resource.fsPath).replace('.py', '')}`; + } + options.title = terminalTitle; const terminalService = new TerminalService(this.serviceContainer, options); this.terminalServices.set(id, terminalService); } @@ -53,6 +58,6 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource || undefined); - return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}`; + return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${resource?.fsPath || ''}`; } } diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index 9261483b45e1..b604d062e81e 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -19,8 +19,7 @@ import { ICodeExecutionService } from '../../terminals/types'; export class TerminalCodeExecutionProvider implements ICodeExecutionService { private hasRanOutsideCurrentDrive = false; protected terminalTitle!: string; - private _terminalService!: ITerminalService; - private replActive?: Promise; + private replActive = new Map>(); constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) protected readonly configurationService: IConfigurationService, @@ -48,19 +47,27 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { await this.getTerminalService(resource).sendText(code); } public async initializeRepl(resource?: Uri) { - if (this.replActive && (await this.replActive)) { - await this._terminalService.show(); + const terminalService = this.getTerminalService(resource); + let replActive = this.replActive.get(resource?.fsPath || ''); + if (replActive && (await replActive)) { + await terminalService.show(); return; } - this.replActive = new Promise(async (resolve) => { + replActive = new Promise(async (resolve) => { const replCommandArgs = await this.getExecutableInfo(resource); - await this.getTerminalService(resource).sendCommand(replCommandArgs.command, replCommandArgs.args); + terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); // Give python repl time to start before we start sending text. setTimeout(() => resolve(true), 1000); }); + this.replActive.set(resource?.fsPath || '', replActive); + this.disposables.push( + terminalService.onDidCloseTerminal(() => { + this.replActive.delete(resource?.fsPath || ''); + }), + ); - await this.replActive; + await replActive; } public async getExecutableInfo(resource?: Uri, args: string[] = []): Promise { @@ -77,18 +84,10 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { return this.getExecutableInfo(resource, executeArgs); } private getTerminalService(resource?: Uri): ITerminalService { - if (!this._terminalService) { - this._terminalService = this.terminalServiceFactory.getTerminalService({ - resource, - title: this.terminalTitle, - }); - this.disposables.push( - this._terminalService.onDidCloseTerminal(() => { - this.replActive = undefined; - }), - ); - } - return this._terminalService; + return this.terminalServiceFactory.getTerminalService({ + resource, + title: this.terminalTitle, + }); } private async setCwdForFileExecution(file: Uri) { const pythonSettings = this.configurationService.getSettings(file); diff --git a/src/test/common/terminals/factory.unit.test.ts b/src/test/common/terminals/factory.unit.test.ts index ef6b7d8f5b0f..f01d5a85fbb5 100644 --- a/src/test/common/terminals/factory.unit.test.ts +++ b/src/test/common/terminals/factory.unit.test.ts @@ -105,7 +105,7 @@ suite('Terminal Service Factory', () => { expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same'); }); - test('Ensure same terminal is returned when using resources from the same workspace', () => { + test('Ensure different terminal is returned when using different resources from the same workspace', () => { const file1A = Uri.file('1a'); const file2A = Uri.file('2a'); const fileB = Uri.file('b'); @@ -131,7 +131,7 @@ suite('Terminal Service Factory', () => { const terminalForFileB = factory.getTerminalService({ resource: fileB }) as SynchronousTerminalService; const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; - expect(terminalsAreSameForWorkspaceA).to.equal(true, 'Instances are not the same for Workspace A'); + expect(terminalsAreSameForWorkspaceA).to.equal(false, 'Instances are the same for Workspace A'); const terminalsForWorkspaceABAreDifferent = terminalForFile1A.terminalService === terminalForFileB.terminalService; From a85eb3b132a752ddd9a6474f1cf62d29cd13d2da Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 9 May 2023 08:18:56 -0700 Subject: [PATCH 083/336] Fix startup telemetry issue (#21203) Could fix https://github.com/microsoft/vscode-python/issues/20874 based on error trace. --- src/client/startupTelemetry.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/startupTelemetry.ts b/src/client/startupTelemetry.ts index 8a123d6bee36..f4d3fc254b67 100644 --- a/src/client/startupTelemetry.ts +++ b/src/client/startupTelemetry.ts @@ -89,7 +89,9 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer): return { workspaceFolderCount, terminal: terminalShellType }; } const interpreterService = serviceContainer.get(IInterpreterService); - const mainWorkspaceUri = workspaceService.workspaceFolders ? workspaceService.workspaceFolders[0].uri : undefined; + const mainWorkspaceUri = workspaceService.workspaceFolders?.length + ? workspaceService.workspaceFolders[0].uri + : undefined; const hasPythonThree = await interpreterService.hasInterpreters(async (item) => item.version?.major === 3); // If an unknown type environment can be found from windows registry or path env var, // consider them as global type instead of unknown. Such types can only be known after From 8d291f7a5d74d8015d27f762e7c18525f853c8aa Mon Sep 17 00:00:00 2001 From: Peter Law Date: Tue, 9 May 2023 21:15:52 +0100 Subject: [PATCH 084/336] Disable "snippets" expansion in Jedi LSP (#21194) This brings the Jedi based completion experience in line with that provided by Pylance. Completions now insert only the current symbol rather than assuming that the user wants to e.g: call that symbol. This means for example that completing `max` will insert just `max` rather `max(arg1, arg2)`. While for this case this may be seen as less useful, it means that insertions in places where a call is not desired (such as imports and typing contexts) will not be forced to manually remove the parentheses and template arguments which might otherwise be inserted. Users can still use the signature support UI to explore signatures and of course insertion of an opening parenthesis will still insert a closing one. Hopefully this new configuration will be preferable to a majority of users. I've done some light testing to check that this disables the described additional completion, however I'm not massively familiar with JediLSP so I'm not sure what other behaviours this will disable. Fixes https://github.com/microsoft/vscode-python/issues/15858 --- src/client/activation/jedi/analysisOptions.ts | 2 +- src/test/activation/jedi/jediAnalysisOptions.unit.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/activation/jedi/analysisOptions.ts b/src/client/activation/jedi/analysisOptions.ts index 67c9af75937c..4778c4e1523f 100644 --- a/src/client/activation/jedi/analysisOptions.ts +++ b/src/client/activation/jedi/analysisOptions.ts @@ -59,7 +59,7 @@ export class JediLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt markupKindPreferred: 'markdown', completion: { resolveEagerly: false, - disableSnippets: false, + disableSnippets: true, }, diagnostics: { enable: true, diff --git a/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts b/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts index 296d31ba2ddb..8104ed2730b0 100644 --- a/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts +++ b/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts @@ -64,7 +64,7 @@ suite('Jedi LSP - analysis Options', () => { expect(result.initializationOptions.markupKindPreferred).to.deep.equal('markdown'); expect(result.initializationOptions.completion.resolveEagerly).to.deep.equal(false); - expect(result.initializationOptions.completion.disableSnippets).to.deep.equal(false); + expect(result.initializationOptions.completion.disableSnippets).to.deep.equal(true); expect(result.initializationOptions.diagnostics.enable).to.deep.equal(true); expect(result.initializationOptions.diagnostics.didOpen).to.deep.equal(true); expect(result.initializationOptions.diagnostics.didSave).to.deep.equal(true); From 5eef5252d660d7552f01e9233499b31c541d1358 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 9 May 2023 15:50:22 -0700 Subject: [PATCH 085/336] Add logging when interpreter path changes (#21210) For https://github.com/microsoft/vscode-python/issues/21208 --- src/client/common/interpreterPathService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/common/interpreterPathService.ts b/src/client/common/interpreterPathService.ts index 5b27fd5a9d72..9eea1548977c 100644 --- a/src/client/common/interpreterPathService.ts +++ b/src/client/common/interpreterPathService.ts @@ -6,7 +6,7 @@ import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import { ConfigurationChangeEvent, ConfigurationTarget, Event, EventEmitter, Uri } from 'vscode'; -import { traceError } from '../logging'; +import { traceError, traceVerbose } from '../logging'; import { IApplicationEnvironment, IWorkspaceService } from './application/types'; import { PythonSettings } from './configSettings'; import { isTestExecution } from './constants'; @@ -56,6 +56,7 @@ export class InterpreterPathService implements IInterpreterPathService { public async onDidChangeConfiguration(event: ConfigurationChangeEvent) { if (event.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) { this._didChangeInterpreterEmitter.fire({ uri: undefined, configTarget: ConfigurationTarget.Global }); + traceVerbose('Interpreter Path updated', `python.${defaultInterpreterPathSetting}`); } } @@ -129,6 +130,7 @@ export class InterpreterPathService implements IInterpreterPathService { if (persistentSetting.value !== pythonPath) { await persistentSetting.updateValue(pythonPath); this._didChangeInterpreterEmitter.fire({ uri: resource, configTarget }); + traceVerbose('Interpreter Path updated', settingKey, pythonPath); } } From eb9fde3100e9c78363d803b083370315bc202d58 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 10 May 2023 09:03:23 -0700 Subject: [PATCH 086/336] Add `createEnvironment.contentButton` setting (#21212) Closes https://github.com/microsoft/vscode-python/issues/20982 --------- Co-authored-by: Luciana Abud <45497113+luabud@users.noreply.github.com> --- package.json | 17 +- package.nls.json | 1 + src/client/common/vscodeApis/workspaceApis.ts | 8 +- src/client/extensionActivation.ts | 4 +- ...CreateEnv.ts => createEnvButtonContext.ts} | 33 +- .../createEnvButtonContext.unit.test.ts | 346 ++++++++++++++++++ .../pyprojectTomlCreateEnv.unit.test.ts | 216 ----------- 7 files changed, 395 insertions(+), 230 deletions(-) rename src/client/pythonEnvironments/creation/{pyprojectTomlCreateEnv.ts => createEnvButtonContext.ts} (50%) create mode 100644 src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts delete mode 100644 src/test/pythonEnvironments/creation/pyprojectTomlCreateEnv.unit.test.ts diff --git a/package.json b/package.json index 34d76ae4d9e8..fd92481c7f84 100644 --- a/package.json +++ b/package.json @@ -404,6 +404,19 @@ "type": "array", "uniqueItems": true }, + "python.createEnvironment.contentButton": { + "default": "show", + "markdownDescription": "%python.createEnvironment.contentButton.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "show", + "hide" + ], + "tags": [ + "experimental" + ] + }, "python.condaPath": { "default": "", "description": "%python.condaPath.description%", @@ -1707,12 +1720,12 @@ { "group": "Python", "command": "python.createEnvironment-button", - "when": "resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" + "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" }, { "group": "Python", "command": "python.createEnvironment-button", - "when": "resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" + "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" } ], "editor/context": [ diff --git a/package.nls.json b/package.nls.json index cfbbeb7d41d5..187b0d832743 100644 --- a/package.nls.json +++ b/package.nls.json @@ -25,6 +25,7 @@ "python.command.python.analysis.restartLanguageServer.title": "Restart Language Server", "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", + "python.createEnvironment.contentButton.description": "Show or hide Create Environment button in the editor for `requirements.txt` or other dependency files.", "python.menu.createNewFile.title": "Python File", "python.editor.context.submenu.runPython": "Run Python", "python.editor.context.submenu.runPythonInteractive": "Run in Interactive window", diff --git a/src/client/common/vscodeApis/workspaceApis.ts b/src/client/common/vscodeApis/workspaceApis.ts index 74200ba46924..20528c17ec11 100644 --- a/src/client/common/vscodeApis/workspaceApis.ts +++ b/src/client/common/vscodeApis/workspaceApis.ts @@ -48,10 +48,14 @@ export function getOpenTextDocuments(): readonly vscode.TextDocument[] { return vscode.workspace.textDocuments; } -export function onDidOpenTextDocument(handler: (doc: vscode.TextDocument) => void): vscode.Disposable { +export function onDidOpenTextDocument(handler: (doc: vscode.TextDocument) => unknown): vscode.Disposable { return vscode.workspace.onDidOpenTextDocument(handler); } -export function onDidChangeTextDocument(handler: (e: vscode.TextDocumentChangeEvent) => void): vscode.Disposable { +export function onDidChangeTextDocument(handler: (e: vscode.TextDocumentChangeEvent) => unknown): vscode.Disposable { return vscode.workspace.onDidChangeTextDocument(handler); } + +export function onDidChangeConfiguration(handler: (e: vscode.ConfigurationChangeEvent) => unknown): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration(handler); +} diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index ba7bdf61c8f2..542a6ccc3010 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -54,7 +54,7 @@ import { DynamicPythonDebugConfigurationService } from './debugger/extension/con import { registerCreateEnvironmentFeatures } from './pythonEnvironments/creation/createEnvApi'; import { IInterpreterQuickPick } from './interpreter/configuration/types'; import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; -import { registerPyProjectTomlCreateEnvFeatures } from './pythonEnvironments/creation/pyprojectTomlCreateEnv'; +import { registerCreateEnvButtonFeatures } from './pythonEnvironments/creation/createEnvButtonContext'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -98,7 +98,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): ); const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); registerCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService, pathUtils); - registerPyProjectTomlCreateEnvFeatures(ext.disposables); + registerCreateEnvButtonFeatures(ext.disposables); } /// ////////////////////////// diff --git a/src/client/pythonEnvironments/creation/pyprojectTomlCreateEnv.ts b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts similarity index 50% rename from src/client/pythonEnvironments/creation/pyprojectTomlCreateEnv.ts rename to src/client/pythonEnvironments/creation/createEnvButtonContext.ts index 5ead37b80dc9..cd009bc6118a 100644 --- a/src/client/pythonEnvironments/creation/pyprojectTomlCreateEnv.ts +++ b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts @@ -8,6 +8,8 @@ import { onDidOpenTextDocument, onDidChangeTextDocument, getOpenTextDocuments, + getConfiguration, + onDidChangeConfiguration, } from '../../common/vscodeApis/workspaceApis'; import { isPipInstallableToml } from './provider/venvUtils'; @@ -19,7 +21,13 @@ async function setPyProjectTomlContextKey(doc: TextDocument): Promise { } } -export function registerPyProjectTomlCreateEnvFeatures(disposables: IDisposableRegistry): void { +async function setShowCreateEnvButtonContextKey(): Promise { + const config = getConfiguration('python'); + const showCreateEnvButton = config.get('createEnvironment.contentButton', 'show') === 'show'; + await executeCommand('setContext', 'showCreateEnvButton', showCreateEnvButton); +} + +export function registerCreateEnvButtonFeatures(disposables: IDisposableRegistry): void { disposables.push( onDidOpenTextDocument(async (doc: TextDocument) => { if (doc.fileName.endsWith('pyproject.toml')) { @@ -27,15 +35,24 @@ export function registerPyProjectTomlCreateEnvFeatures(disposables: IDisposableR } }), onDidChangeTextDocument(async (e: TextDocumentChangeEvent) => { - if (e.document.fileName.endsWith('pyproject.toml')) { - await setPyProjectTomlContextKey(e.document); + const doc = e.document; + if (doc.fileName.endsWith('pyproject.toml')) { + await setPyProjectTomlContextKey(doc); } }), + onDidChangeConfiguration(async () => { + await setShowCreateEnvButtonContextKey(); + }), ); - getOpenTextDocuments().forEach(async (doc: TextDocument) => { - if (doc.fileName.endsWith('pyproject.toml')) { - await setPyProjectTomlContextKey(doc); - } - }); + setShowCreateEnvButtonContextKey(); + + const docs = getOpenTextDocuments().filter( + (doc) => doc.fileName.endsWith('pyproject.toml') && isPipInstallableToml(doc.getText()), + ); + if (docs.length > 0) { + executeCommand('setContext', 'pipInstallableToml', true); + } else { + executeCommand('setContext', 'pipInstallableToml', false); + } } diff --git a/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts new file mode 100644 index 000000000000..31842420dd59 --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { TextDocument, TextDocumentChangeEvent, WorkspaceConfiguration } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { registerCreateEnvButtonFeatures } from '../../../client/pythonEnvironments/creation/createEnvButtonContext'; + +chaiUse(chaiAsPromised); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +function getInstallableToml(): typemoq.IMock { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + return pyprojectToml; +} + +function getNonInstallableToml(): typemoq.IMock { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n'); + return pyprojectToml; +} + +function getSomeFile(): typemoq.IMock { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); + return someFile; +} + +suite('PyProject.toml Create Env Features', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let getOpenTextDocumentsStub: sinon.SinonStub; + let onDidOpenTextDocumentStub: sinon.SinonStub; + let onDidChangeTextDocumentStub: sinon.SinonStub; + let onDidChangeConfigurationStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); + onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); + onDidChangeTextDocumentStub = sinon.stub(workspaceApis, 'onDidChangeTextDocument'); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + onDidChangeConfigurationStub = sinon.stub(workspaceApis, 'onDidChangeConfiguration'); + + onDidOpenTextDocumentStub.returns(new FakeDisposable()); + onDidChangeTextDocumentStub.returns(new FakeDisposable()); + onDidChangeConfigurationStub.returns(new FakeDisposable()); + + configMock = typemoq.Mock.ofType(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + getConfigurationStub.returns(configMock.object); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('python.createEnvironment.contentButton setting is set to "show", no files open', async () => { + getOpenTextDocumentsStub.returns([]); + + registerCreateEnvButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('python.createEnvironment.contentButton setting is set to "hide", no files open', async () => { + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + getOpenTextDocumentsStub.returns([]); + + registerCreateEnvButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('python.createEnvironment.contentButton setting changed from "hide" to "show"', async () => { + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + getOpenTextDocumentsStub.returns([]); + + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeConfigurationStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + handler(); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + }); + + test('python.createEnvironment.contentButton setting changed from "show" to "hide"', async () => { + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + getOpenTextDocumentsStub.returns([]); + + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeConfigurationStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + handler(); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + }); + + test('Installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerCreateEnvButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getNonInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerCreateEnvButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file open in the editor on extension activate', async () => { + const someFile = getSomeFile(); + getOpenTextDocumentsStub.returns([someFile.object]); + + registerCreateEnvButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', true)); + + handler(pyprojectToml.object); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(someFile.object); + + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocumentChangeEvent) => void = () => { + /* do nothing */ + }; + onDidChangeTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + + handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocumentChangeEvent) => void = () => { + /* do nothing */ + }; + onDidChangeTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Non Installable pyproject.toml is changed to Installable', async () => { + getOpenTextDocumentsStub.returns([]); + + let openHandler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + openHandler = callback; + return new FakeDisposable(); + }); + + let changeHandler: (d: TextDocumentChangeEvent) => void = () => { + /* do nothing */ + }; + onDidChangeTextDocumentStub.callsFake((callback) => { + changeHandler = callback; + return new FakeDisposable(); + }); + + const nonInatallablePyprojectToml = getNonInstallableToml(); + const installablePyprojectToml = getInstallableToml(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + openHandler(nonInatallablePyprojectToml.object); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + changeHandler({ contentChanges: [], document: installablePyprojectToml.object, reason: undefined }); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Some random file is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocumentChangeEvent) => void = () => { + /* do nothing */ + }; + onDidChangeTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler({ contentChanges: [], document: someFile.object, reason: undefined }); + + assert.ok(executeCommandStub.notCalled); + }); +}); diff --git a/src/test/pythonEnvironments/creation/pyprojectTomlCreateEnv.unit.test.ts b/src/test/pythonEnvironments/creation/pyprojectTomlCreateEnv.unit.test.ts deleted file mode 100644 index 3f19aa5775b3..000000000000 --- a/src/test/pythonEnvironments/creation/pyprojectTomlCreateEnv.unit.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import * as typemoq from 'typemoq'; -import { assert, use as chaiUse } from 'chai'; -import { TextDocument, TextDocumentChangeEvent } from 'vscode'; -import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; -import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; -import { IDisposableRegistry } from '../../../client/common/types'; -import { registerPyProjectTomlCreateEnvFeatures } from '../../../client/pythonEnvironments/creation/pyprojectTomlCreateEnv'; - -chaiUse(chaiAsPromised); - -class FakeDisposable { - public dispose() { - // Do nothing - } -} - -function getInstallableToml(): typemoq.IMock { - const pyprojectTomlPath = 'pyproject.toml'; - const pyprojectToml = typemoq.Mock.ofType(); - pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); - pyprojectToml - .setup((p) => p.getText(typemoq.It.isAny())) - .returns( - () => - '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', - ); - return pyprojectToml; -} - -function getNonInstallableToml(): typemoq.IMock { - const pyprojectTomlPath = 'pyproject.toml'; - const pyprojectToml = typemoq.Mock.ofType(); - pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); - pyprojectToml - .setup((p) => p.getText(typemoq.It.isAny())) - .returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n'); - return pyprojectToml; -} - -function getSomeFile(): typemoq.IMock { - const someFilePath = 'something.py'; - const someFile = typemoq.Mock.ofType(); - someFile.setup((p) => p.fileName).returns(() => someFilePath); - someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); - return someFile; -} - -suite('PyProject.toml Create Env Features', () => { - let executeCommandStub: sinon.SinonStub; - const disposables: IDisposableRegistry = []; - let getOpenTextDocumentsStub: sinon.SinonStub; - let onDidOpenTextDocumentStub: sinon.SinonStub; - let onDidChangeTextDocumentStub: sinon.SinonStub; - - setup(() => { - executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); - getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); - onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); - onDidChangeTextDocumentStub = sinon.stub(workspaceApis, 'onDidChangeTextDocument'); - - onDidOpenTextDocumentStub.returns(new FakeDisposable()); - onDidChangeTextDocumentStub.returns(new FakeDisposable()); - }); - - teardown(() => { - sinon.restore(); - disposables.forEach((d) => d.dispose()); - }); - - test('Installable pyproject.toml is already open in the editor on extension activate', async () => { - const pyprojectToml = getInstallableToml(); - getOpenTextDocumentsStub.returns([pyprojectToml.object]); - - registerPyProjectTomlCreateEnvFeatures(disposables); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non installable pyproject.toml is already open in the editor on extension activate', async () => { - const pyprojectToml = getNonInstallableToml(); - getOpenTextDocumentsStub.returns([pyprojectToml.object]); - - registerPyProjectTomlCreateEnvFeatures(disposables); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Some random file open in the editor on extension activate', async () => { - const someFile = getSomeFile(); - getOpenTextDocumentsStub.returns([someFile.object]); - - registerPyProjectTomlCreateEnvFeatures(disposables); - - assert.ok(executeCommandStub.notCalled); - }); - - test('Installable pyproject.toml is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getInstallableToml(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler(pyprojectToml.object); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non Installable pyproject.toml is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getNonInstallableToml(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler(pyprojectToml.object); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Some random file is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const someFile = getSomeFile(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler(someFile.object); - - assert.ok(executeCommandStub.notCalled); - }); - - test('Installable pyproject.toml is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getInstallableToml(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non Installable pyproject.toml is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getNonInstallableToml(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Some random file is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const someFile = getSomeFile(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler({ contentChanges: [], document: someFile.object, reason: undefined }); - - assert.ok(executeCommandStub.notCalled); - }); -}); From 15338189257cdf264a9a00966c8909d6a06123e8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 10 May 2023 16:09:01 -0700 Subject: [PATCH 087/336] Added option to run multiple Python files in separate terminals (#21223) Closes https://github.com/microsoft/vscode-python/issues/21215 https://github.com/microsoft/vscode-python/issues/14094 Added the option to assign a dedicated terminal for each Python file: ![image](https://github.com/microsoft/vscode-python/assets/13199757/b01248e4-c826-4de0-b15f-cde959965e68) --- package.json | 19 +++++++ package.nls.json | 1 + src/client/common/constants.ts | 1 + src/client/common/terminal/factory.ts | 16 ++++-- src/client/common/terminal/types.ts | 2 +- src/client/telemetry/index.ts | 6 ++ .../codeExecution/codeExecutionManager.ts | 56 ++++++++++++------- .../codeExecution/terminalCodeExecution.ts | 19 +++---- src/client/terminals/types.ts | 2 +- .../common/terminals/factory.unit.test.ts | 47 +++++++++++++++- .../codeExecutionManager.unit.test.ts | 25 ++++++--- 11 files changed, 147 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index fd92481c7f84..df393d7934d2 100644 --- a/package.json +++ b/package.json @@ -304,6 +304,12 @@ "icon": "$(play)", "title": "%python.command.python.execInTerminalIcon.title%" }, + { + "category": "Python", + "command": "python.execInNewTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInNewTerminal.title%" + }, { "category": "Python", "command": "python.debugInTerminal", @@ -1626,6 +1632,13 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "false && editorLangId == python" }, + { + "category": "Python", + "command": "python.execInNewTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInNewTerminal.title%", + "when": "false && editorLangId == python" + }, { "category": "Python", "command": "python.debugInTerminal", @@ -1784,6 +1797,12 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, + { + "command": "python.execInNewTerminal", + "group": "navigation@0", + "title": "%python.command.python.execInNewTerminal.title%", + "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" + }, { "command": "python.debugInTerminal", "group": "navigation@1", diff --git a/package.nls.json b/package.nls.json index 187b0d832743..e8e0bb803d10 100644 --- a/package.nls.json +++ b/package.nls.json @@ -7,6 +7,7 @@ "python.command.python.execInTerminal.title": "Run Python File in Terminal", "python.command.python.debugInTerminal.title": "Debug Python File", "python.command.python.execInTerminalIcon.title": "Run Python File", + "python.command.python.execInNewTerminal.title": "Run Python File in Separate Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index b285667aaa6a..aae792ec2e39 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -44,6 +44,7 @@ export namespace Commands { export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; + export const Exec_In_Separate_Terminal = 'python.execInNewTerminal'; export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index 3cbe123b5629..39cc88c4b024 100644 --- a/src/client/common/terminal/factory.ts +++ b/src/client/common/terminal/factory.ts @@ -24,14 +24,14 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { ) { this.terminalServices = new Map(); } - public getTerminalService(options: TerminalCreationOptions): ITerminalService { + public getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService { const resource = options?.resource; const title = options?.title; let terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; const interpreter = options?.interpreter; - const id = this.getTerminalId(terminalTitle, resource, interpreter); + const id = this.getTerminalId(terminalTitle, resource, interpreter, options.newTerminalPerFile); if (!this.terminalServices.has(id)) { - if (this.terminalServices.size >= 1 && resource) { + if (resource && options.newTerminalPerFile) { terminalTitle = `${terminalTitle}: ${path.basename(resource.fsPath).replace('.py', '')}`; } options.title = terminalTitle; @@ -51,13 +51,19 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { title = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; return new TerminalService(this.serviceContainer, { resource, title }); } - private getTerminalId(title: string, resource?: Uri, interpreter?: PythonEnvironment): string { + private getTerminalId( + title: string, + resource?: Uri, + interpreter?: PythonEnvironment, + newTerminalPerFile?: boolean, + ): string { if (!resource && !interpreter) { return title; } const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource || undefined); - return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${resource?.fsPath || ''}`; + const fileId = resource && newTerminalPerFile ? resource.fsPath : ''; + return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${fileId}`; } } diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index 880bf0dd72fb..303188682378 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -97,7 +97,7 @@ export interface ITerminalServiceFactory { * @returns {ITerminalService} * @memberof ITerminalServiceFactory */ - getTerminalService(options: TerminalCreationOptions): ITerminalService; + getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService; createTerminalService(resource?: Uri, title?: string): ITerminalService; } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 5dff35067196..d0b9d463c070 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -825,6 +825,12 @@ export interface IEventNamePropertyMapping { * @type {('command' | 'icon')} */ trigger?: 'command' | 'icon'; + /** + * Whether user chose to execute this Python file in a separate terminal or not. + * + * @type {boolean} + */ + newTerminalPerFile?: boolean; }; /** * Telemetry Event sent when user executes code against Django Shell. diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index ed671f2846a2..9f1ba6e90d90 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -36,25 +36,31 @@ export class CodeExecutionManager implements ICodeExecutionManager { } public registerCommands() { - [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon].forEach((cmd) => { - this.disposableRegistry.push( - this.commandManager.registerCommand(cmd as any, async (file: Resource) => { - const interpreterService = this.serviceContainer.get(IInterpreterService); - const interpreter = await interpreterService.getActiveInterpreter(file); - if (!interpreter) { - this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); - return; - } - const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; - await this.executeFileInTerminal(file, trigger) - .then(() => { - if (this.shouldTerminalFocusOnStart(file)) - this.commandManager.executeCommand('workbench.action.terminal.focus'); + [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon, Commands.Exec_In_Separate_Terminal].forEach( + (cmd) => { + this.disposableRegistry.push( + this.commandManager.registerCommand(cmd as any, async (file: Resource) => { + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager + .executeCommand(Commands.TriggerEnvironmentSelection, file) + .then(noop, noop); + return; + } + const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; + await this.executeFileInTerminal(file, trigger, { + newTerminalPerFile: cmd === Commands.Exec_In_Separate_Terminal, }) - .catch((ex) => traceError('Failed to execute file in terminal', ex)); - }), - ); - }); + .then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }) + .catch((ex) => traceError('Failed to execute file in terminal', ex)); + }), + ); + }, + ); this.disposableRegistry.push( this.commandManager.registerCommand(Commands.Exec_Selection_In_Terminal as any, async (file: Resource) => { const interpreterService = this.serviceContainer.get(IInterpreterService); @@ -87,8 +93,16 @@ export class CodeExecutionManager implements ICodeExecutionManager { ), ); } - private async executeFileInTerminal(file: Resource, trigger: 'command' | 'icon') { - sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'file', trigger }); + private async executeFileInTerminal( + file: Resource, + trigger: 'command' | 'icon', + options?: { newTerminalPerFile: boolean }, + ): Promise { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { + scope: 'file', + trigger, + newTerminalPerFile: options?.newTerminalPerFile, + }); const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); file = file instanceof Uri ? file : undefined; let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); @@ -110,7 +124,7 @@ export class CodeExecutionManager implements ICodeExecutionManager { } const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); - await executionService.executeFile(fileToExecute); + await executionService.executeFile(fileToExecute, options); } @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index b604d062e81e..ca7488530f75 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -19,7 +19,7 @@ import { ICodeExecutionService } from '../../terminals/types'; export class TerminalCodeExecutionProvider implements ICodeExecutionService { private hasRanOutsideCurrentDrive = false; protected terminalTitle!: string; - private replActive = new Map>(); + private replActive?: Promise; constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) protected readonly configurationService: IConfigurationService, @@ -29,13 +29,13 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { @inject(IInterpreterService) protected readonly interpreterService: IInterpreterService, ) {} - public async executeFile(file: Uri) { + public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) { await this.setCwdForFileExecution(file); const { command, args } = await this.getExecuteFileArgs(file, [ file.fsPath.fileToCommandArgumentForPythonExt(), ]); - await this.getTerminalService(file).sendCommand(command, args); + await this.getTerminalService(file, options).sendCommand(command, args); } public async execute(code: string, resource?: Uri): Promise { @@ -48,26 +48,24 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { } public async initializeRepl(resource?: Uri) { const terminalService = this.getTerminalService(resource); - let replActive = this.replActive.get(resource?.fsPath || ''); - if (replActive && (await replActive)) { + if (this.replActive && (await this.replActive)) { await terminalService.show(); return; } - replActive = new Promise(async (resolve) => { + this.replActive = new Promise(async (resolve) => { const replCommandArgs = await this.getExecutableInfo(resource); terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); // Give python repl time to start before we start sending text. setTimeout(() => resolve(true), 1000); }); - this.replActive.set(resource?.fsPath || '', replActive); this.disposables.push( terminalService.onDidCloseTerminal(() => { - this.replActive.delete(resource?.fsPath || ''); + this.replActive = undefined; }), ); - await replActive; + await this.replActive; } public async getExecutableInfo(resource?: Uri, args: string[] = []): Promise { @@ -83,10 +81,11 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { return this.getExecutableInfo(resource, executeArgs); } - private getTerminalService(resource?: Uri): ITerminalService { + private getTerminalService(resource?: Uri, options?: { newTerminalPerFile: boolean }): ITerminalService { return this.terminalServiceFactory.getTerminalService({ resource, title: this.terminalTitle, + newTerminalPerFile: options?.newTerminalPerFile, }); } private async setCwdForFileExecution(file: Uri) { diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index cf31f4ef1dd0..47ac16d9e08b 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -8,7 +8,7 @@ export const ICodeExecutionService = Symbol('ICodeExecutionService'); export interface ICodeExecutionService { execute(code: string, resource?: Uri): Promise; - executeFile(file: Uri): Promise; + executeFile(file: Uri, options?: { newTerminalPerFile: boolean }): Promise; initializeRepl(resource?: Uri): Promise; } diff --git a/src/test/common/terminals/factory.unit.test.ts b/src/test/common/terminals/factory.unit.test.ts index f01d5a85fbb5..5ad2da8e793a 100644 --- a/src/test/common/terminals/factory.unit.test.ts +++ b/src/test/common/terminals/factory.unit.test.ts @@ -105,7 +105,7 @@ suite('Terminal Service Factory', () => { expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same'); }); - test('Ensure different terminal is returned when using different resources from the same workspace', () => { + test('Ensure same terminal is returned when using different resources from the same workspace', () => { const file1A = Uri.file('1a'); const file2A = Uri.file('2a'); const fileB = Uri.file('b'); @@ -130,6 +130,51 @@ suite('Terminal Service Factory', () => { const terminalForFile2A = factory.getTerminalService({ resource: file2A }) as SynchronousTerminalService; const terminalForFileB = factory.getTerminalService({ resource: fileB }) as SynchronousTerminalService; + const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; + expect(terminalsAreSameForWorkspaceA).to.equal(true, 'Instances are not the same for Workspace A'); + + const terminalsForWorkspaceABAreDifferent = + terminalForFile1A.terminalService === terminalForFileB.terminalService; + expect(terminalsForWorkspaceABAreDifferent).to.equal( + false, + 'Instances should be different for different workspaces', + ); + }); + + test('When `newTerminalPerFile` is true, ensure different terminal is returned when using different resources from the same workspace', () => { + const file1A = Uri.file('1a'); + const file2A = Uri.file('2a'); + const fileB = Uri.file('b'); + const workspaceUriA = Uri.file('A'); + const workspaceUriB = Uri.file('B'); + const workspaceFolderA = TypeMoq.Mock.ofType(); + workspaceFolderA.setup((w) => w.uri).returns(() => workspaceUriA); + const workspaceFolderB = TypeMoq.Mock.ofType(); + workspaceFolderB.setup((w) => w.uri).returns(() => workspaceUriB); + + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file1A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file2A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(fileB))) + .returns(() => workspaceFolderB.object); + + const terminalForFile1A = factory.getTerminalService({ + resource: file1A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFile2A = factory.getTerminalService({ + resource: file2A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFileB = factory.getTerminalService({ + resource: fileB, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; expect(terminalsAreSameForWorkspaceA).to.equal(false, 'Instances are the same for Workspace A'); diff --git a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 3676834873a0..30f95c94d217 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -77,12 +77,15 @@ suite('Terminal - Code Execution Manager', () => { executionManager.registerCommands(); const sorted = registered.sort(); - expect(sorted).to.deep.equal([ - Commands.Exec_In_Terminal, - Commands.Exec_In_Terminal_Icon, - Commands.Exec_Selection_In_Django_Shell, - Commands.Exec_Selection_In_Terminal, - ]); + expect(sorted).to.deep.equal( + [ + Commands.Exec_In_Separate_Terminal, + Commands.Exec_In_Terminal, + Commands.Exec_In_Terminal_Icon, + Commands.Exec_Selection_In_Django_Shell, + Commands.Exec_Selection_In_Terminal, + ].sort(), + ); }); test('Ensure executeFileInterTerminal will do nothing if no file is avialble', async () => { @@ -135,7 +138,10 @@ suite('Terminal - Code Execution Manager', () => { const fileToExecute = Uri.file('x'); await commandHandler!(fileToExecute); helper.verify(async (h) => h.getFileToExecute(), TypeMoq.Times.never()); - executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); test('Ensure executeFileInterTerminal will use active file', async () => { @@ -164,7 +170,10 @@ suite('Terminal - Code Execution Manager', () => { .returns(() => executionService.object); await commandHandler!(fileToExecute); - executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { From 0c4fa40d28bcdd2848c36499be4be61a1a90a4e5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 11 May 2023 10:21:04 -0700 Subject: [PATCH 088/336] Change name of command to run Python files in separate terminals (#21229) Closes https://github.com/microsoft/vscode-python/issues/14094 --- package.json | 12 ++++++------ package.nls.json | 2 +- src/client/common/constants.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index df393d7934d2..2109483b817f 100644 --- a/package.json +++ b/package.json @@ -306,9 +306,9 @@ }, { "category": "Python", - "command": "python.execInNewTerminal", + "command": "python.execInDedicatedTerminal", "icon": "$(play)", - "title": "%python.command.python.execInNewTerminal.title%" + "title": "%python.command.python.execInDedicatedTerminal.title%" }, { "category": "Python", @@ -1634,9 +1634,9 @@ }, { "category": "Python", - "command": "python.execInNewTerminal", + "command": "python.execInDedicatedTerminal", "icon": "$(play)", - "title": "%python.command.python.execInNewTerminal.title%", + "title": "%python.command.python.execInDedicatedTerminal.title%", "when": "false && editorLangId == python" }, { @@ -1798,9 +1798,9 @@ "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, { - "command": "python.execInNewTerminal", + "command": "python.execInDedicatedTerminal", "group": "navigation@0", - "title": "%python.command.python.execInNewTerminal.title%", + "title": "%python.command.python.execInDedicatedTerminal.title%", "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, { diff --git a/package.nls.json b/package.nls.json index e8e0bb803d10..18254fd05468 100644 --- a/package.nls.json +++ b/package.nls.json @@ -7,7 +7,7 @@ "python.command.python.execInTerminal.title": "Run Python File in Terminal", "python.command.python.debugInTerminal.title": "Debug Python File", "python.command.python.execInTerminalIcon.title": "Run Python File", - "python.command.python.execInNewTerminal.title": "Run Python File in Separate Terminal", + "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index aae792ec2e39..bea0ef9e235c 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -44,7 +44,7 @@ export namespace Commands { export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; - export const Exec_In_Separate_Terminal = 'python.execInNewTerminal'; + export const Exec_In_Separate_Terminal = 'python.execInDedicatedTerminal'; export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; From b0da28cd3d595728944034773a8dc68ea425e3c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Pi=C3=B1a=20Martinez?= Date: Thu, 11 May 2023 22:06:08 +0200 Subject: [PATCH 089/336] Remove IS_WINDOWS constant in favor of PlatformService (#21157) Solves partially #8542 --- src/client/common/configSettings.ts | 4 ++-- src/client/common/platform/constants.ts | 7 ------- src/client/common/platform/platformService.ts | 7 ++++++- src/client/common/serviceRegistry.ts | 4 ++-- src/client/linters/pydocstyle.ts | 4 ++-- src/test/common/configSettings.test.ts | 4 ++-- src/test/serviceRegistry.ts | 5 ++--- 7 files changed, 16 insertions(+), 19 deletions(-) delete mode 100644 src/client/common/platform/constants.ts diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 3e3525d5b2a4..db5944cc794b 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -23,7 +23,6 @@ import { ITestingSettings } from '../testing/configuration/types'; import { IWorkspaceService } from './application/types'; import { WorkspaceService } from './application/workspace'; import { DEFAULT_INTERPRETER_SETTING, isTestExecution } from './constants'; -import { IS_WINDOWS } from './platform/constants'; import { IAutoCompleteSettings, IDefaultLanguageServer, @@ -41,6 +40,7 @@ import { import { debounceSync } from './utils/decorators'; import { SystemVariables } from './variables/systemVariables'; import { getOSType, OSType } from './utils/platform'; +import { isWindows } from './platform/platformService'; const untildify = require('untildify'); @@ -654,7 +654,7 @@ function getPythonExecutable(pythonPath: string): string { for (let executableName of KnownPythonExecutables) { // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'. - if (IS_WINDOWS) { + if (isWindows()) { executableName = `${executableName}.exe`; if (isValidPythonPath(path.join(pythonPath, executableName))) { return path.join(pythonPath, executableName); diff --git a/src/client/common/platform/constants.ts b/src/client/common/platform/constants.ts deleted file mode 100644 index 808a63188c1d..000000000000 --- a/src/client/common/platform/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// TODO : Drop all these in favor of IPlatformService. -// See https://github.com/microsoft/vscode-python/issues/8542. - -export const IS_WINDOWS = /^win/.test(process.platform); diff --git a/src/client/common/platform/platformService.ts b/src/client/common/platform/platformService.ts index 0277c1bcd2a2..aa139eeeebc0 100644 --- a/src/client/common/platform/platformService.ts +++ b/src/client/common/platform/platformService.ts @@ -50,8 +50,9 @@ export class PlatformService implements IPlatformService { } } + // eslint-disable-next-line class-methods-use-this public get isWindows(): boolean { - return this.osType === OSType.Windows; + return isWindows(); } public get isMac(): boolean { @@ -72,3 +73,7 @@ export class PlatformService implements IPlatformService { return getArchitecture() === Architecture.x64; } } + +export function isWindows(): boolean { + return getOSType() === OSType.Windows; +} diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 5b527499460a..be0559496ace 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -57,7 +57,6 @@ import { ProductInstaller } from './installer/productInstaller'; import { InterpreterPathService } from './interpreterPathService'; import { BrowserService } from './net/browser'; import { PersistentStateFactory } from './persistentState'; -import { IS_WINDOWS } from './platform/constants'; import { PathUtils } from './platform/pathUtils'; import { CurrentProcess } from './process/currentProcess'; import { ProcessLogger } from './process/logger'; @@ -91,9 +90,10 @@ import { Random } from './utils/random'; import { ContextKeyManager } from './application/contextKeyManager'; import { CreatePythonFileCommandHandler } from './application/commands/createPythonFile'; import { RequireJupyterPrompt } from '../jupyter/requireJupyterPrompt'; +import { isWindows } from './platform/platformService'; export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); + serviceManager.addSingletonInstance(IsWindows, isWindows()); serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); diff --git a/src/client/linters/pydocstyle.ts b/src/client/linters/pydocstyle.ts index 93c059440fe7..4851190a92ac 100644 --- a/src/client/linters/pydocstyle.ts +++ b/src/client/linters/pydocstyle.ts @@ -4,9 +4,9 @@ import '../common/extensions'; import { Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { traceError } from '../logging'; -import { IS_WINDOWS } from '../common/platform/constants'; import { BaseLinter } from './baseLinter'; import { ILintMessage, LintMessageSeverity } from './types'; +import { isWindows } from '../common/platform/platformService'; export class PyDocStyle extends BaseLinter { constructor(serviceContainer: IServiceContainer) { @@ -47,7 +47,7 @@ export class PyDocStyle extends BaseLinter { .filter((value, index) => index < maxLines && value.indexOf(':') >= 0) .map((line) => { // Windows will have a : after the drive letter (e.g. c:\). - if (IS_WINDOWS) { + if (isWindows()) { return line.substring(line.indexOf(`${baseFileName}:`) + baseFileName.length + 1).trim(); } return line.substring(line.indexOf(':') + 1).trim(); diff --git a/src/test/common/configSettings.test.ts b/src/test/common/configSettings.test.ts index 75c20f512bbe..8630835081e2 100644 --- a/src/test/common/configSettings.test.ts +++ b/src/test/common/configSettings.test.ts @@ -1,10 +1,10 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; -import { IS_WINDOWS } from '../../client/common/platform/constants'; import { SystemVariables } from '../../client/common/variables/systemVariables'; import { getExtensionSettings } from '../extensionSettings'; import { initialize } from './../initialize'; +import { isWindows } from '../../client/common/platform/platformService'; const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); @@ -27,7 +27,7 @@ suite('Configuration Settings', () => { } const pythonSettingValue = (pythonSettings as any)[key] as string; - if (key.endsWith('Path') && IS_WINDOWS) { + if (key.endsWith('Path') && isWindows()) { assert.strictEqual( settingValue.toUpperCase(), pythonSettingValue.toUpperCase(), diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index 1b8a9d78d580..c20a84b1e25a 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -5,10 +5,9 @@ import { Container } from 'inversify'; import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Disposable, Memento } from 'vscode'; -import { IS_WINDOWS } from '../client/common/platform/constants'; import { FileSystem } from '../client/common/platform/fileSystem'; import { PathUtils } from '../client/common/platform/pathUtils'; -import { PlatformService } from '../client/common/platform/platformService'; +import { PlatformService, isWindows } from '../client/common/platform/platformService'; import { RegistryImplementation } from '../client/common/platform/registry'; import { registerTypes as platformRegisterTypes } from '../client/common/platform/serviceRegistry'; import { IFileSystem, IPlatformService, IRegistry } from '../client/common/platform/types'; @@ -195,7 +194,7 @@ export class IocContainer { } public registerMockProcess(): void { - this.serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); + this.serviceManager.addSingletonInstance(IsWindows, isWindows()); this.serviceManager.addSingleton(IPathUtils, PathUtils); this.serviceManager.addSingleton(ICurrentProcess, MockProcess); From b3d43e5f7ee97d30c578593d52720b805d10e0f7 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 11 May 2023 15:48:43 -0700 Subject: [PATCH 090/336] Do not open "save as" window when running existing Python files (#21232) Closes https://github.com/microsoft/vscode-python/issues/21209 --- src/client/common/application/types.ts | 12 ++++++------ src/client/common/application/workspace.ts | 4 ++-- src/client/terminals/codeExecution/helper.ts | 4 ++-- src/test/terminals/codeExecution/helper.test.ts | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index e57bac656d19..72cd357d62d8 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -852,15 +852,15 @@ export interface IWorkspaceService { */ openTextDocument(options?: { language?: string; content?: string }): Thenable; /** - * Saves the editor identified by the given resource to a new file name as provided by the user and - * returns the resulting resource or `undefined` if save was not successful or cancelled. + * Saves the editor identified by the given resource and returns the resulting resource or `undefined` + * if save was not successful. * - * **Note** that an editor with the provided resource must be opened in order to be saved as. + * **Note** that an editor with the provided resource must be opened in order to be saved. * - * @param uri the associated uri for the opened editor to save as. - * @return A thenable that resolves when the save-as operation has finished. + * @param uri the associated uri for the opened editor to save. + * @return A thenable that resolves when the save operation has finished. */ - saveAs(uri: Uri): Thenable; + save(uri: Uri): Thenable; } export const ITerminalManager = Symbol('ITerminalManager'); diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 11ed98cf0076..a76a78777bef 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -113,10 +113,10 @@ export class WorkspaceService implements IWorkspaceService { return `{${enabledSearchExcludes.join(',')}}`; } - public async saveAs(uri: Uri): Promise { + public async save(uri: Uri): Promise { try { // This is a proposed API hence putting it inside try...catch. - const result = await workspace.saveAs(uri); + const result = await workspace.save(uri); return result; } catch (ex) { return undefined; diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index 0a1ae9f0be06..0d5694b4a28d 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -122,9 +122,9 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { public async saveFileIfDirty(file: Uri): Promise { const docs = this.documentManager.textDocuments.filter((d) => d.uri.path === file.path); - if (docs.length === 1 && docs[0].isDirty) { + if (docs.length === 1 && (docs[0].isDirty || docs[0].isUntitled)) { const workspaceService = this.serviceContainer.get(IWorkspaceService); - return workspaceService.saveAs(docs[0].uri); + return workspaceService.save(docs[0].uri); } return undefined; } diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index 684ca22b6c75..ac47037a2344 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -383,7 +383,7 @@ suite('Terminal - Code Execution Helper', () => { const untitledUri = Uri.file('Untitled-1'); document.setup((doc) => doc.uri).returns(() => untitledUri); const expectedSavedUri = Uri.file('one.py'); - workspaceService.setup((w) => w.saveAs(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedSavedUri)); + workspaceService.setup((w) => w.save(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedSavedUri)); const savedUri = await helper.saveFileIfDirty(untitledUri); From fcfc54c43e01942d8d7bd89214a5e94c0c34d469 Mon Sep 17 00:00:00 2001 From: Jonathan Rayner Date: Fri, 12 May 2023 23:42:24 +0100 Subject: [PATCH 091/336] Add option for pyenv interpreters when creating environments with venv (#21219) Resolves #20881 . Testing: Behaves as expected when testing with Extension Development Host: ![image](https://github.com/microsoft/vscode-python/assets/30149293/d114d9ab-f2d8-4273-877b-d7dd030cfe76) --- .../creation/provider/venvCreationProvider.ts | 3 ++- src/client/pythonEnvironments/info/index.ts | 4 +++- src/client/pythonEnvironments/legacyIOC.ts | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index e18f2fa79fde..b00682c3cb5d 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -162,7 +162,8 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { EnvironmentType.System, EnvironmentType.MicrosoftStore, EnvironmentType.Global, - ].includes(i.envType), + EnvironmentType.Pyenv, + ].includes(i.envType) && i.type === undefined, // only global intepreters { skipRecommended: true, showBackButton: true, diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index 70abbb0fad76..60628f61314e 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -68,10 +68,11 @@ export type InterpreterInformation = { * * @prop companyDisplayName - the user-facing name of the distro publisher * @prop displayName - the user-facing name for the environment - * @prop type - the kind of Python environment + * @prop envType - the kind of Python environment * @prop envName - the environment's name, if applicable (else `envPath` is set) * @prop envPath - the environment's root dir, if applicable (else `envName`) * @prop cachedEntry - whether or not the info came from a cache + * @prop type - the type of Python environment, if applicable */ // Note that "cachedEntry" is specific to the caching machinery // and doesn't really belong here. @@ -84,6 +85,7 @@ export type PythonEnvironment = InterpreterInformation & { envName?: string; envPath?: string; cachedEntry?: boolean; + type?: string; }; /** diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index 0c80c3414728..f116bdb63a89 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -75,6 +75,7 @@ function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { } env.displayName = info.display; env.detailedDisplayName = info.detailedDisplayName; + env.type = info.type; // We do not worry about using distro.defaultDisplayName. return env; From b4a47bbc2d27ae476db14f4f406f166c49214930 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Mon, 15 May 2023 14:53:09 -0700 Subject: [PATCH 092/336] Add reload flag on fastApi provider (#21241) --- .../configuration/dynamicdebugConfigurationService.ts | 2 +- .../extension/configuration/providers/fastapiLaunch.ts | 6 +++--- .../configuration/providers/fastapiLaunch.unit.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts b/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts index 2d80f0e3d6e8..e79f201d9367 100644 --- a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts @@ -69,7 +69,7 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf type: DebuggerTypeName, request: 'launch', module: 'uvicorn', - args: [`${fastApiPath}:app`], + args: [`${fastApiPath}:app`, '--reload'], jinja: true, justMyCode: true, }); diff --git a/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts b/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts index 25aaf3d25c08..38a9b7ccf1a2 100644 --- a/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts @@ -25,12 +25,12 @@ export async function buildFastAPILaunchDebugConfiguration( type: DebuggerTypeName, request: 'launch', module: 'uvicorn', - args: ['main:app'], + args: ['main:app', '--reload'], jinja: true, justMyCode: true, }; - if (!application) { + if (!application && config.args) { const selectedPath = await input.showInputBox({ title: DebugConfigStrings.fastapi.enterAppPathOrNamePath.title, value: 'main.py', @@ -44,7 +44,7 @@ export async function buildFastAPILaunchDebugConfiguration( }); if (selectedPath) { manuallyEnteredAValue = true; - config.args = [`${path.basename(selectedPath, '.py').replace('/', '.')}:app`]; + config.args[0] = `${path.basename(selectedPath, '.py').replace('/', '.')}:app`; } } diff --git a/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts index f6c20985e4da..80ce37167024 100644 --- a/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts +++ b/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts @@ -53,7 +53,7 @@ suite('Debugging - Configuration Provider FastAPI', () => { type: DebuggerTypeName, request: 'launch', module: 'uvicorn', - args: ['main:app'], + args: ['main:app', '--reload'], jinja: true, justMyCode: true, }; @@ -73,7 +73,7 @@ suite('Debugging - Configuration Provider FastAPI', () => { type: DebuggerTypeName, request: 'launch', module: 'uvicorn', - args: ['main:app'], + args: ['main:app', '--reload'], jinja: true, justMyCode: true, }; From be9662f0316856b8e119a3441d5502d75cf5cc11 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 16 May 2023 08:38:46 -0700 Subject: [PATCH 093/336] revert testing to using socket (#21242) switch back to using a socket instead of an output file for use in the plugin communication during testing. This should work now that we resolved the issue with python path for windows. --- pythonFiles/tests/pytestadapter/helpers.py | 83 ++++++++++++---------- pythonFiles/vscode_pytest/__init__.py | 40 ++++------- 2 files changed, 59 insertions(+), 64 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index b078439f6eac..e84c46037c2f 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -10,6 +10,7 @@ import socket import subprocess import sys +import threading import uuid from typing import Any, Dict, List, Optional, Union @@ -17,20 +18,6 @@ from typing_extensions import TypedDict -@contextlib.contextmanager -def test_output_file(root: pathlib.Path, ext: str = ".txt"): - """Creates a temporary python file with a random name.""" - basename = ( - "".join(random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(9)) + ext - ) - fullpath = root / basename - try: - fullpath.write_text("", encoding="utf-8") - yield fullpath - finally: - os.unlink(str(fullpath)) - - def create_server( host: str = "127.0.0.1", port: int = 0, @@ -116,31 +103,51 @@ def runner(args: List[str]) -> Optional[Dict[str, Any]]: "-p", "vscode_pytest", ] + args + listener: socket.socket = create_server() + _, port = listener.getsockname() + listener.listen() + + env = os.environ.copy() + env.update( + { + "TEST_UUID": str(uuid.uuid4()), + "TEST_PORT": str(port), + "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), + } + ) + + result: list = [] + t1: threading.Thread = threading.Thread( + target=_listen_on_socket, args=(listener, result) + ) + t1.start() + + t2 = threading.Thread( + target=lambda proc_args, proc_env, proc_cwd: subprocess.run( + proc_args, env=proc_env, cwd=proc_cwd + ), + args=(process_args, env, TEST_DATA_PATH), + ) + t2.start() + + t1.join() + t2.join() + + return process_rpc_json(result[0]) if result else None + - with test_output_file(TEST_DATA_PATH) as output_path: - env = os.environ.copy() - env.update( - { - "TEST_UUID": str(uuid.uuid4()), - "TEST_PORT": str(12345), # port is not used for tests - "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), - "TEST_OUTPUT_FILE": os.fspath(output_path), - } - ) - - result = subprocess.run( - process_args, - env=env, - cwd=os.fspath(TEST_DATA_PATH), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - if result.returncode != 0: - print("Subprocess Run failed with:") - print(result.stdout.decode(encoding="utf-8")) - print(result.stderr.decode(encoding="utf-8")) - - return process_rpc_json(output_path.read_text(encoding="utf-8")) +def _listen_on_socket(listener: socket.socket, result: List[str]): + """Listen on the socket for the JSON data from the server. + Created as a seperate function for clarity in threading. + """ + sock, (other_host, other_port) = listener.accept() + all_data: list = [] + while True: + data: bytes = sock.recv(1024 * 1024) + if not data: + break + all_data.append(data.decode("utf-8")) + result.append("".join(all_data)) def find_test_line_number(test_name: str, test_file_path) -> str: diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 6063e4113d55..39330f4e8a38 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -437,19 +437,13 @@ def execution_post( Request-uuid: {testuuid} {data}""" - test_output_file: Optional[str] = os.getenv("TEST_OUTPUT_FILE", None) - if test_output_file == "stdout": - print(request) - elif test_output_file: - pathlib.Path(test_output_file).write_text(request, encoding="utf-8") - else: - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") + try: + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) + except Exception as e: + print(f"Plugin error connection error[vscode-pytest]: {e}") + print(f"[vscode-pytest] data: {request}") def post_response(cwd: str, session_node: TestNode) -> None: @@ -477,16 +471,10 @@ def post_response(cwd: str, session_node: TestNode) -> None: Request-uuid: {testuuid} {data}""" - test_output_file: Optional[str] = os.getenv("TEST_OUTPUT_FILE", None) - if test_output_file == "stdout": - print(request) - elif test_output_file: - pathlib.Path(test_output_file).write_text(request, encoding="utf-8") - else: - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") + try: + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) + except Exception as e: + print(f"Plugin error connection error[vscode-pytest]: {e}") + print(f"[vscode-pytest] data: {request}") From b0ebc9ba50c3f89a8be713acc4fb49190f390d2f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 16 May 2023 09:16:48 -0700 Subject: [PATCH 094/336] Enable debug pytest (#21228) fixes https://github.com/microsoft/vscode-python/issues/21147 --------- Co-authored-by: Aidos Kanapyanov <65722512+aidoskanapyanov@users.noreply.github.com> Co-authored-by: Karthik Nadig --- .vscode/launch.json | 3 +- pythonFiles/vscode_pytest/__init__.py | 7 ++- src/client/testing/common/debugLauncher.ts | 48 ++++++++++++++----- src/client/testing/common/types.ts | 2 + .../testing/testController/common/types.ts | 3 +- .../testing/testController/controller.ts | 2 + .../pytest/pytestExecutionAdapter.ts | 37 ++++++++++---- .../testController/workspaceTestAdapter.ts | 3 ++ 8 files changed, 80 insertions(+), 25 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 82981a93305d..1ca0db3dc858 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,8 @@ // Enable this to log telemetry to the output during debugging "XVSC_PYTHON_LOG_TELEMETRY": "1", // Enable this to log debugger output. Directory must exist ahead of time - "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex" + "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex", + "ENABLE_PYTHON_TESTING_REWRITE": "1" } }, { diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 39330f4e8a38..0f0bcbd1d323 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -179,6 +179,12 @@ def pytest_sessionfinish(session, exitstatus): 4: Pytest encountered an internal error or exception during test execution. 5: Pytest was unable to find any tests to run. """ + print( + "pytest session has finished, exit status: ", + exitstatus, + "in discovery? ", + IS_DISCOVERY, + ) cwd = pathlib.Path.cwd() if IS_DISCOVERY: try: @@ -209,7 +215,6 @@ def pytest_sessionfinish(session, exitstatus): f"Pytest exited with error status: {exitstatus}, {ERROR_MESSAGE_CONST[exitstatus]}" ) exitstatus_bool = "error" - execution_post( os.fsdecode(cwd), exitstatus_bool, diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 36432c0bd831..5b39bd97a740 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -15,6 +15,7 @@ import { ITestDebugLauncher, LaunchOptions } from './types'; import { getConfigurationsForWorkspace } from '../../debugger/extension/configuration/launch.json/launchJsonReader'; import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; import { showErrorMessage } from '../../common/vscodeApis/windowApis'; +import { createDeferred } from '../../common/utils/async'; @injectable() export class DebugLauncher implements ITestDebugLauncher { @@ -42,16 +43,12 @@ export class DebugLauncher implements ITestDebugLauncher { ); const debugManager = this.serviceContainer.get(IDebugService); - return debugManager.startDebugging(workspaceFolder, launchArgs).then( - // Wait for debug session to be complete. - () => - new Promise((resolve) => { - debugManager.onDidTerminateDebugSession(() => { - resolve(); - }); - }), - (ex) => traceError('Failed to start debugging tests', ex), - ); + const deferred = createDeferred(); + debugManager.onDidTerminateDebugSession(() => { + deferred.resolve(); + }); + debugManager.startDebugging(workspaceFolder, launchArgs); + return deferred.promise; } private static resolveWorkspaceFolder(cwd: string): WorkspaceFolder { @@ -181,6 +178,12 @@ export class DebugLauncher implements ITestDebugLauncher { const args = script(testArgs); const [program] = args; configArgs.program = program; + // if the test provider is pytest, then use the pytest module instead of using a program + const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; + if (options.testProvider === 'pytest' && rewriteTestingEnabled) { + configArgs.module = 'pytest'; + configArgs.program = undefined; + } configArgs.args = args.slice(1); // We leave configArgs.request as "test" so it will be sent in telemetry. @@ -201,6 +204,21 @@ export class DebugLauncher implements ITestDebugLauncher { throw Error(`Invalid debug config "${debugConfig.name}"`); } launchArgs.request = 'launch'; + if (options.testProvider === 'pytest' && rewriteTestingEnabled) { + if (options.pytestPort && options.pytestUUID) { + launchArgs.env = { + ...launchArgs.env, + TEST_PORT: options.pytestPort, + TEST_UUID: options.pytestUUID, + }; + } else { + throw Error( + `Missing value for debug setup, both port and uuid need to be defined. port: "${options.pytestPort}" uuid: "${options.pytestUUID}"`, + ); + } + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + launchArgs.env.PYTHONPATH = pluginPath; + } // Clear out purpose so we can detect if the configuration was used to // run via F5 style debugging. @@ -210,13 +228,19 @@ export class DebugLauncher implements ITestDebugLauncher { } private static getTestLauncherScript(testProvider: TestProvider) { + const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; switch (testProvider) { case 'unittest': { + if (rewriteTestingEnabled) { + return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger + } return internalScripts.visualstudio_py_testlauncher; // old way unittest execution, debugger - // return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger } case 'pytest': { - return internalScripts.testlauncher; + if (rewriteTestingEnabled) { + return (testArgs: string[]) => testArgs; + } + return internalScripts.testlauncher; // old way pytest execution, debugger } default: { throw new Error(`Unknown test provider '${testProvider}'`); diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index b1476e74435f..a5d263058525 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -25,6 +25,8 @@ export type LaunchOptions = { testProvider: TestProvider; token?: CancellationToken; outChannel?: OutputChannel; + pytestPort?: string; + pytestUUID?: string; }; export type ParserOptions = TestDiscoveryOptions; diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 52c6c787040c..4336fee3a4b6 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -12,7 +12,7 @@ import { Uri, WorkspaceFolder, } from 'vscode'; -import { TestDiscoveryOptions } from '../../common/types'; +import { ITestDebugLauncher, TestDiscoveryOptions } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; export type TestRunInstanceOptions = TestRunOptions & { @@ -193,6 +193,7 @@ export interface ITestExecutionAdapter { testIds: string[], debugBool?: boolean, executionFactory?: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, ): Promise; } diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index fb176a30af88..58eaa9c890d6 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -409,6 +409,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc token, request.profile?.kind === TestRunProfileKind.Debug, this.pythonExecFactory, + this.debugLauncher, ); } return this.pytest.runTests( @@ -438,6 +439,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc testItems, token, request.profile?.kind === TestRunProfileKind.Debug, + this.pythonExecFactory, ); } // below is old way of running unittest execution diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index d2cbd3151e6f..623fd1ff3a8c 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -12,11 +12,13 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { EXTENSION_ROOT_DIR } from '../../../constants'; import { removePositionalFoldersAndFiles } from './arguments'; +import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; +import { PYTEST_PROVIDER } from '../../common/constants'; +import { EXTENSION_ROOT_DIR } from '../../../common/constants'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -(global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; +// (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; /** * Wrapper Class for pytest test execution. This is where we call `runTestCommand`? */ @@ -47,11 +49,12 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testIds: string[], debugBool?: boolean, executionFactory?: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, ): Promise { traceVerbose(uri, testIds, debugBool); if (executionFactory !== undefined) { // ** new version of run tests. - return this.runTestsNew(uri, testIds, debugBool, executionFactory); + return this.runTestsNew(uri, testIds, debugBool, executionFactory, debugLauncher); } // if executionFactory is undefined, we are using the old method signature of run tests. this.outputChannel.appendLine('Running tests.'); @@ -64,6 +67,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testIds: string[], debugBool?: boolean, executionFactory?: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, ): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; @@ -106,16 +110,29 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testArgs.splice(0, 0, '--rootdir', uri.fsPath); } + // why is this needed? if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } - - console.debug(`Running test with arguments: ${testArgs.join(' ')}\r\n`); - console.debug(`Current working directory: ${uri.fsPath}\r\n`); - - const argArray = ['-m', 'pytest', '-p', 'vscode_pytest'].concat(testArgs).concat(testIds); - console.debug('argArray', argArray); - execService?.exec(argArray, spawnOptions); + const pluginArgs = ['-p', 'vscode_pytest', '-v'].concat(testArgs).concat(testIds); + if (debugBool) { + const pytestPort = this.testServer.getPort().toString(); + const pytestUUID = uuid.toString(); + const launchOptions: LaunchOptions = { + cwd: uri.fsPath, + args: pluginArgs, + token: spawnOptions.token, + testProvider: PYTEST_PROVIDER, + pytestPort, + pytestUUID, + }; + console.debug(`Running debug test with arguments: ${pluginArgs.join(' ')}\r\n`); + await debugLauncher!.launchDebugger(launchOptions); + } else { + const runArgs = ['-m', 'pytest'].concat(pluginArgs); + console.debug(`Running test with arguments: ${runArgs.join(' ')}\r\n`); + execService?.exec(runArgs, spawnOptions); + } } catch (ex) { console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 39efc67f7c7e..b22fee69d295 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -38,6 +38,7 @@ import { } from './common/types'; import { fixLogLines } from './common/utils'; import { IPythonExecutionFactory } from '../../common/process/types'; +import { ITestDebugLauncher } from '../common/types'; /** * This class exposes a test-provider-agnostic way of discovering tests. @@ -77,6 +78,7 @@ export class WorkspaceTestAdapter { token?: CancellationToken, debugBool?: boolean, executionFactory?: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, ): Promise { if (this.executing) { return this.executing.promise; @@ -110,6 +112,7 @@ export class WorkspaceTestAdapter { testCaseIds, debugBool, executionFactory, + debugLauncher, ); traceVerbose('executionFactory defined'); } else { From a74f1d15b58e2a835f5564449918452ff30d62d6 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 16 May 2023 11:18:18 -0700 Subject: [PATCH 095/336] Detect installed packages in the selected environment (#21231) Fixes https://github.com/microsoft/vscode-python/issues/21140 --- package.json | 10 +- pythonFiles/installed_check.py | 129 +++++++ pythonFiles/tests/test_data/missing-deps.data | 121 +++++++ .../tests/test_data/no-missing-deps.data | 13 + .../test_data/pyproject-missing-deps.data | 9 + .../test_data/pyproject-no-missing-deps.data | 9 + pythonFiles/tests/test_installed_check.py | 90 +++++ requirements.in | 5 + requirements.txt | 20 +- .../common/process/internal/scripts/index.ts | 5 + src/client/common/vscodeApis/languageApis.ts | 12 + src/client/common/vscodeApis/windowApis.ts | 4 + src/client/common/vscodeApis/workspaceApis.ts | 12 +- src/client/extensionActivation.ts | 6 +- .../creation/common/installCheckUtils.ts | 60 ++++ .../creation/createEnvButtonContext.ts | 40 +-- .../creation/installedPackagesDiagnostic.ts | 88 +++++ .../creation/pyProjectTomlContext.ts | 44 +++ .../creation/registrations.ts | 21 ++ .../common/installCheckUtils.unit.test.ts | 65 ++++ .../createEnvButtonContext.unit.test.ts | 255 +------------- .../installedPackagesDiagnostics.unit.test.ts | 333 ++++++++++++++++++ .../pyProjectTomlContext.unit.test.ts | 266 ++++++++++++++ 23 files changed, 1314 insertions(+), 303 deletions(-) create mode 100644 pythonFiles/installed_check.py create mode 100644 pythonFiles/tests/test_data/missing-deps.data create mode 100644 pythonFiles/tests/test_data/no-missing-deps.data create mode 100644 pythonFiles/tests/test_data/pyproject-missing-deps.data create mode 100644 pythonFiles/tests/test_data/pyproject-no-missing-deps.data create mode 100644 pythonFiles/tests/test_installed_check.py create mode 100644 src/client/common/vscodeApis/languageApis.ts create mode 100644 src/client/pythonEnvironments/creation/common/installCheckUtils.ts create mode 100644 src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts create mode 100644 src/client/pythonEnvironments/creation/pyProjectTomlContext.ts create mode 100644 src/client/pythonEnvironments/creation/registrations.ts create mode 100644 src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts create mode 100644 src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts create mode 100644 src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts diff --git a/package.json b/package.json index 2109483b817f..8b364b5d60e6 100644 --- a/package.json +++ b/package.json @@ -1526,10 +1526,8 @@ ], "configuration": "./languages/pip-requirements.json", "filenamePatterns": [ - "**/*-requirements.{txt, in}", - "**/*-constraints.txt", - "**/requirements-*.{txt, in}", - "**/constraints-*.txt", + "**/*requirements*.{txt, in}", + "**/*constraints*.txt", "**/requirements/*.{txt,in}", "**/constraints/*.txt" ], @@ -1733,12 +1731,12 @@ { "group": "Python", "command": "python.createEnvironment-button", - "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" + "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && pythonDepsNotInstalled" }, { "group": "Python", "command": "python.createEnvironment-button", - "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" + "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && pythonDepsNotInstalled" } ], "editor/context": [ diff --git a/pythonFiles/installed_check.py b/pythonFiles/installed_check.py new file mode 100644 index 000000000000..f0e1c268d270 --- /dev/null +++ b/pythonFiles/installed_check.py @@ -0,0 +1,129 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import json +import os +import pathlib +import sys +from typing import Dict, List, Optional, Sequence, Tuple, Union + +LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python" +sys.path.insert(0, os.fspath(LIB_ROOT)) + +import tomli +from importlib_metadata import metadata +from packaging.requirements import Requirement + +DEFAULT_SEVERITY = 3 + + +def parse_args(argv: Optional[Sequence[str]] = None): + if argv is None: + argv = sys.argv[1:] + parser = argparse.ArgumentParser( + description="Check for installed packages against requirements" + ) + parser.add_argument("FILEPATH", type=str, help="Path to requirements.[txt, in]") + + return parser.parse_args(argv) + + +def parse_requirements(line: str) -> Optional[Requirement]: + try: + req = Requirement(line.strip("\\")) + if req.marker is None: + return req + elif req.marker.evaluate(): + return req + except: + return None + + +def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + for n, line in enumerate(req_file.read_text(encoding="utf-8").splitlines()): + if line.startswith(("#", "-", " ")) or line == "": + continue + + req = parse_requirements(line) + if req: + try: + # Check if package is installed + metadata(req.name) + except: + diagnostics.append( + { + "line": n, + "character": 0, + "endLine": n, + "endCharacter": len(req.name), + "package": req.name, + "code": "not-installed", + "severity": DEFAULT_SEVERITY, + } + ) + return diagnostics + + +def get_pos(lines: List[str], text: str) -> Tuple[int, int, int, int]: + for n, line in enumerate(lines): + index = line.find(text) + if index >= 0: + return n, index, n, index + len(text) + return (0, 0, 0, 0) + + +def process_pyproject(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + try: + raw_text = req_file.read_text(encoding="utf-8") + pyproject = tomli.loads(raw_text) + except: + return diagnostics + + lines = raw_text.splitlines() + reqs = pyproject.get("project", {}).get("dependencies", []) + for raw_req in reqs: + req = parse_requirements(raw_req) + n, start, _, end = get_pos(lines, raw_req) + if req: + try: + # Check if package is installed + metadata(req.name) + except: + diagnostics.append( + { + "line": n, + "character": start, + "endLine": n, + "endCharacter": end, + "package": req.name, + "code": "not-installed", + "severity": DEFAULT_SEVERITY, + } + ) + return diagnostics + + +def get_diagnostics(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + if not req_file.exists(): + return diagnostics + + if req_file.name == "pyproject.toml": + diagnostics = process_pyproject(req_file) + else: + diagnostics = process_requirements(req_file) + + return diagnostics + + +def main(): + args = parse_args() + diagnostics = get_diagnostics(pathlib.Path(args.FILEPATH)) + print(json.dumps(diagnostics, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/pythonFiles/tests/test_data/missing-deps.data b/pythonFiles/tests/test_data/missing-deps.data new file mode 100644 index 000000000000..c42d23c7dd67 --- /dev/null +++ b/pythonFiles/tests/test_data/missing-deps.data @@ -0,0 +1,121 @@ +# +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: +# +# pip-compile --generate-hashes --resolver=backtracking requirements-test.in +# +flake8-csv==0.2.0 \ + --hash=sha256:246e07207fefbf8f80a59ff7e878f153635f562ebaf20cf796a2b00b1528ea9a \ + --hash=sha256:bf3ac6aecbaebe36a2c7d5d275f310996fcc33b7370cdd81feec04b79af2e07c + # via -r requirements-test.in +levenshtein==0.21.0 \ + --hash=sha256:01dd427cf72b4978b09558e3d36e3f92c8eef467e3eb4653c3fdccd8d70aaa08 \ + --hash=sha256:0236c8ff4648c50ebd81ac3692430d2241b134936ac9d86d7ca32ba6ab4a4e63 \ + --hash=sha256:023ca95c833ca548280e444e9a4c34fdecb3be3851e96af95bad290ae0c708b9 \ + --hash=sha256:024302c82d49fc1f1d044794997ef7aa9d01b509a9040e222480b64a01cd4b80 \ + --hash=sha256:04046878a57129da4e2352c032df7c1fceaa54870916d12772cad505ef998290 \ + --hash=sha256:04850a0719e503014acb3fee6d4ec7d7f170a2c7375ffbc5833c7256b7cd10ee \ + --hash=sha256:0cc3679978cd0250bf002963cf2e08855b93f70fa0fc9f74956115c343983fbb \ + --hash=sha256:0f42b8dba2cce257cd34efd1ce9678d06f3248cb0bb2a92a5db8402e1e4a6f30 \ + --hash=sha256:13e8a5b1b58de49befea555bb913dc394614f2d3553bc5b86bc672c69ef1a85a \ + --hash=sha256:1f19fe25ea0dd845d0f48505e8947f6080728e10b7642ba0dad34e9b48c81130 \ + --hash=sha256:1fde464f937878e6f5c30c234b95ce2cb969331a175b3089367e077113428062 \ + --hash=sha256:2290732763e3b75979888364b26acce79d72b8677441b5762a4e97b3630cc3d9 \ + --hash=sha256:24843f28cbbdcbcfc18b08e7d3409dbaad7896fb7113442592fa978590a7bbf0 \ + --hash=sha256:25576ad9c337ecb342306fe87166b54b2f49e713d4ff592c752cc98e0046296e \ + --hash=sha256:26c6fb012538a245d78adea786d2cfe3c1506b835762c1c523a4ed6b9e08dc0b \ + --hash=sha256:31cb59d86a5f99147cd4a67ebced8d6df574b5d763dcb63c033a642e29568746 \ + --hash=sha256:32dfda2e64d0c50553e47d0ab2956413970f940253351c196827ad46f17916d5 \ + --hash=sha256:3305262cb85ff78ace9e2d8d2dfc029b34dc5f93aa2d24fd20b6ed723e2ad501 \ + --hash=sha256:37a99d858fa1d88b1a917b4059a186becd728534e5e889d583086482356b7ca1 \ + --hash=sha256:3c6858cfd84568bc1df3ad545553b5c27af6ed3346973e8f4b57d23c318cf8f4 \ + --hash=sha256:3e1723d515ab287b9b2c2e4a111894dc6b474f5d28826fff379647486cae98d2 \ + --hash=sha256:3e22d31375d5fea5797c9b7aa0f8cc36579c31dcf5754e9931ca86c27d9011f8 \ + --hash=sha256:426883be613d912495cf6ee2a776d2ab84aa6b3de5a8d82c43a994267ea6e0e3 \ + --hash=sha256:4357bf8146cbadb10016ad3a950bba16e042f79015362a575f966181d95b4bc7 \ + --hash=sha256:4515f9511cb91c66d254ee30154206aad76b57d8b25f64ba1402aad43efdb251 \ + --hash=sha256:457442911df185e28a32fd8b788b14ca22ab3a552256b556e7687173d5f18bc4 \ + --hash=sha256:46dab8c6e8fae563ca77acfaeb3824c4dd4b599996328b8a081b06f16befa6a0 \ + --hash=sha256:4b2156f32e46d16b74a055ccb4f64ee3c64399372a6aaf1ee98f6dccfadecee1 \ + --hash=sha256:4bbceef2caba4b2ae613b0e853a7aaab990c1a13bddb9054ba1328a84bccdbf7 \ + --hash=sha256:4c8eaaa6f0df2838437d1d8739629486b145f7a3405d3ef0874301a9f5bc7dcd \ + --hash=sha256:4dc79033140f82acaca40712a6d26ed190cc2dd403e104020a87c24f2771aa72 \ + --hash=sha256:4ec2ef9836a34a3bb009a81e5efe4d9d43515455fb5f182c5d2cf8ae61c79496 \ + --hash=sha256:5369827ace536c6df04e0e670d782999bc17bf9eb111e77435fdcdaecb10c2a3 \ + --hash=sha256:5378a8139ba61d7271c0f9350201259c11eb90bfed0ac45539c4aeaed3907230 \ + --hash=sha256:545635d9e857711d049dcdb0b8609fb707b34b032517376c531ca159fcd46265 \ + --hash=sha256:587ad51770de41eb491bea1bfb676abc7ff9a94dbec0e2bc51fc6a25abef99c4 \ + --hash=sha256:5cfbc4ed7ee2965e305bf81388fea377b795dabc82ee07f04f31d1fb8677a885 \ + --hash=sha256:5e748c2349719cb1bc90f802d9d7f07310633dcf166d468a5bd821f78ed17698 \ + --hash=sha256:608beb1683508c3cdbfff669c1c872ea02b47965e1bbb8a630de548e2490f96a \ + --hash=sha256:6338a47b6f8c7f1ee8b5636cc8b245ad2d1d0ee47f7bb6f33f38a522ef0219cc \ + --hash=sha256:668ea30b311944c643f866ce5e45edf346f05e920075c0056f2ba7f74dde6071 \ + --hash=sha256:66d303cd485710fe6d62108209219b7a695bdd10a722f4e86abdaf26f4bf2202 \ + --hash=sha256:6ebabcf982ae161534f8729d13fe05eebc977b497ac34936551f97cf8b07dd9e \ + --hash=sha256:6ede583155f24c8b2456a7720fbbfa5d9c1154ae04b4da3cf63368e2406ea099 \ + --hash=sha256:709a727f58d31a5ee1e5e83b247972fe55ef0014f6222256c9692c5efa471785 \ + --hash=sha256:742b785c93d16c63289902607219c200bd2b6077dafc788073c74337cae382fb \ + --hash=sha256:76d5d34a8e21de8073c66ae801f053520f946d499fa533fbba654712775f8132 \ + --hash=sha256:7bc550d0986ace95bde003b8a60e622449baf2bdf24d8412f7a50f401a289ec3 \ + --hash=sha256:7c2d67220867d640e36931b3d63b8349369b485d52cf6f4a2635bec8da92d678 \ + --hash=sha256:7ce3f14a8e006fb7e3fc7bab965ab7da5817f48fc48d25cf735fcec8f1d2e39a \ + --hash=sha256:7e40a4bac848c9a8883225f926cfa7b2bc9f651e989a8b7006cdb596edc7ac9b \ + --hash=sha256:80e67bd73a05592ecd52aede4afa8ea49575de70f9d5bfbe2c52ebd3541b20be \ + --hash=sha256:8446f8da38857482ec0cfd616fe5e7dcd3695fd323cc65f37366a9ff6a31c9cb \ + --hash=sha256:8476862a5c3150b8d63a7475563a4bff6dc50bbc0447894eb6b6a116ced0809d \ + --hash=sha256:84b55b732e311629a8308ad2778a0f9824e29e3c35987eb35610fc52eb6d4634 \ + --hash=sha256:88ccdc8dc20c16e8059ace00fb58d353346a04fd24c0733b009678b2554801d2 \ + --hash=sha256:8aa92b05156dfa2e248c3743670d5deb41a45b5789416d5fa31be009f4f043ab \ + --hash=sha256:8ac4ed77d3263eac7f9b6ed89d451644332aecd55cda921201e348803a1e5c57 \ + --hash=sha256:8bdbcd1570340b07549f71e8a5ba3f0a6d84408bf86c4051dc7b70a29ae342bb \ + --hash=sha256:8c031cbe3685b0343f5cc2dcf2172fd21b82f8ccc5c487179a895009bf0e4ea8 \ + --hash=sha256:8c27a5178ce322b56527a451185b4224217aa81955d9b0dad6f5a8de81ffe80f \ + --hash=sha256:8cf87a5e2962431d7260dd81dc1ca0697f61aad81036145d3666f4c0d514ce3a \ + --hash=sha256:8d4ba0df46bb41d660d77e7cc6b4d38c8d5b6f977d51c48ed1217db6a8474cde \ + --hash=sha256:8dd8ef4239b24fb1c9f0b536e48e55194d5966d351d349af23e67c9eb3875c68 \ + --hash=sha256:92bf2370b01d7a4862abf411f8f60f39f064cebebce176e3e9ee14e744db8288 \ + --hash=sha256:9485f2a5c88113410153256657072bc93b81bf5c8690d47e4cc3df58135dbadb \ + --hash=sha256:9ff1255c499fcb41ba37a578ad8c1b8dab5c44f78941b8e1c1d7fab5b5e831bc \ + --hash=sha256:a18c8e4d1aae3f9950797d049020c64a8a63cc8b4e43afcca91ec400bf6304c5 \ + --hash=sha256:a68b05614d25cc2a5fbcc4d2fd124be7668d075fd5ac3d82f292eec573157361 \ + --hash=sha256:a7adaabe07c5ceb6228332b9184f06eb9cda89c227d198a1b8a6f78c05b3c672 \ + --hash=sha256:aa39bb773915e4df330d311bb6c100a8613e265cc50d5b25b015c8db824e1c47 \ + --hash=sha256:ac8b6266799645827980ab1af4e0bfae209c1f747a10bdf6e5da96a6ebe511a2 \ + --hash=sha256:b0ba9723c7d67a61e160b3457259552f7d679d74aaa144b892eb68b7e2a5ebb6 \ + --hash=sha256:b167b32b3e336c5ec5e0212f025587f9248344ae6e73ed668270eba5c6a506e5 \ + --hash=sha256:b646ace5085a60d4f89b28c81301c9d9e8cd6a9bdda908181b2fa3dfac7fc10d \ + --hash=sha256:bd0bfa71b1441be359e99e77709885b79c22857bf9bb7f4e84c09e501f6c5fad \ + --hash=sha256:be038321695267a8faa5ae1b1a83deb3748827f0b6f72471e0beed36afcbd72a \ + --hash=sha256:be87998ffcbb5fb0c37a76d100f63b4811f48527192677da0ec3624b49ab8a64 \ + --hash=sha256:c270487d60b33102efea73be6dcd5835f3ddc3dc06e77499f0963df6cba2ec71 \ + --hash=sha256:c290a7211f1b4f87c300df4424cc46b7379cead3b6f37fa8d3e7e6c6212ccd39 \ + --hash=sha256:cc36ba40027b4f8821155c9e3e0afadffccdccbe955556039d1d1169dfc659c9 \ + --hash=sha256:ce7e76c6341abb498368d42b8081f2f45c245ac2a221af6a0394349d41302c08 \ + --hash=sha256:cefd5a668f6d7af1279aca10104b43882fdd83f9bdc68933ba5429257a628abe \ + --hash=sha256:cf2dee0f8c71598f8be51e3feceb9142ac01576277b9e691e25740987761c86e \ + --hash=sha256:d23c647b03acbb5783f9bdfd51cfa5365d51f7df9f4029717a35eff5cc32bbcc \ + --hash=sha256:d647f1e0c30c7a73f70f4de7376ed7dafc2b856b67fe480d32a81af133edbaeb \ + --hash=sha256:d932cb21e40beb93cfc8973de7f25fbf25ba4a07d1dccac3b9ba977164cf9887 \ + --hash=sha256:db7567997ffbc2feb999e30002a92461a76f17a596a142bdb463b5f7037f160c \ + --hash=sha256:de2dfd6498454c7d89036d56a53c0a01fd9bcf1c2970253e469b5e8bb938b69f \ + --hash=sha256:df9b0f8f511270ad259c7bfba22ab6d5a0c33d81cd594461668e67cd80dd9052 \ + --hash=sha256:e043b79e39f165026bc941c95582bfc4bfdd297a1de6f13ace0d0a7abf486288 \ + --hash=sha256:e2686c37d22faf27d02a19e83b55812d248b32b7ba3aa638e768d0ea032e1f3c \ + --hash=sha256:e9a6251818b9eb6d519bffd7a0b745f3a99b3e99563a4c9d3cad26e34f6ac880 \ + --hash=sha256:eab6c253983a6659e749f4c44fcc2215194c2e00bf7b1c5e90fe683ea3b7b00f \ + --hash=sha256:ec64b7b3fb95bc9c20c72548277794b81281a6ba9da85eda2c87324c218441ff \ + --hash=sha256:ee62ec5882a857b252faffeb7867679f7e418052ca6bf7d6b56099f6498a2b0e \ + --hash=sha256:ee757fd36bad66ad8b961958840894021ecaad22194f65219a666432739393ff \ + --hash=sha256:f55623094b665d79a3b82ba77386ac34fa85049163edfe65387063e5127d4184 \ + --hash=sha256:f622f542bd065ffec7d26b26d44d0c9a25c9c1295fd8ba6e4d77778e2293a12c \ + --hash=sha256:f873af54014cac12082c7f5ccec6bbbeb5b57f63466e7f9c61a34588621313fb \ + --hash=sha256:fae24c875c4ecc8c5f34a9715eb2a459743b4ca21d35c51819b640ee2f71cb51 \ + --hash=sha256:fb26e69fc6c12534fbaa1657efed3b6482f1a166ba8e31227fa6f6f062a59070 + # via -r requirements-test.in +pytest==7.3.1 \ + --hash=sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362 \ + --hash=sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3 + +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f diff --git a/pythonFiles/tests/test_data/no-missing-deps.data b/pythonFiles/tests/test_data/no-missing-deps.data new file mode 100644 index 000000000000..5c2f1178bbdf --- /dev/null +++ b/pythonFiles/tests/test_data/no-missing-deps.data @@ -0,0 +1,13 @@ +# +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: +# +# pip-compile --generate-hashes --resolver=backtracking requirements-test.in +# +pytest==7.3.1 \ + --hash=sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362 \ + --hash=sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3 + +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f diff --git a/pythonFiles/tests/test_data/pyproject-missing-deps.data b/pythonFiles/tests/test_data/pyproject-missing-deps.data new file mode 100644 index 000000000000..f217a0bdade6 --- /dev/null +++ b/pythonFiles/tests/test_data/pyproject-missing-deps.data @@ -0,0 +1,9 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "something" +version = "2023.0.0" +requires-python = ">=3.7" +dependencies = ["pytest==7.3.1", "flake8-csv"] diff --git a/pythonFiles/tests/test_data/pyproject-no-missing-deps.data b/pythonFiles/tests/test_data/pyproject-no-missing-deps.data new file mode 100644 index 000000000000..729bc9169e6f --- /dev/null +++ b/pythonFiles/tests/test_data/pyproject-no-missing-deps.data @@ -0,0 +1,9 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "something" +version = "2023.0.0" +requires-python = ">=3.7" +dependencies = [jedi-language-server"] diff --git a/pythonFiles/tests/test_installed_check.py b/pythonFiles/tests/test_installed_check.py new file mode 100644 index 000000000000..f76070d197be --- /dev/null +++ b/pythonFiles/tests/test_installed_check.py @@ -0,0 +1,90 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import contextlib +import json +import os +import pathlib +import subprocess +import sys + +import pytest +from typing import Dict, List, Union + +SCRIPT_PATH = pathlib.Path(__file__).parent.parent / "installed_check.py" +TEST_DATA = pathlib.Path(__file__).parent / "test_data" +DEFAULT_SEVERITY = 3 + + +@contextlib.contextmanager +def generate_file(base_file: pathlib.Path): + basename = "pyproject.toml" if "pyproject" in base_file.name else "requirements.txt" + fullpath = base_file.parent / basename + if fullpath.exists(): + os.unlink(os.fspath(fullpath)) + fullpath.write_text(base_file.read_text(encoding="utf-8")) + try: + yield fullpath + finally: + os.unlink(str(fullpath)) + + +def run_on_file(file_path: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + result = subprocess.run( + [ + sys.executable, + os.fspath(SCRIPT_PATH), + os.fspath(file_path), + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + assert result.returncode == 0 + assert result.stderr == b"" + return json.loads(result.stdout) + + +EXPECTED_DATA = { + "missing-deps": [ + { + "line": 6, + "character": 0, + "endLine": 6, + "endCharacter": 10, + "package": "flake8-csv", + "code": "not-installed", + "severity": 3, + }, + { + "line": 10, + "character": 0, + "endLine": 10, + "endCharacter": 11, + "package": "levenshtein", + "code": "not-installed", + "severity": 3, + }, + ], + "no-missing-deps": [], + "pyproject-missing-deps": [ + { + "line": 8, + "character": 34, + "endLine": 8, + "endCharacter": 44, + "package": "flake8-csv", + "code": "not-installed", + "severity": 3, + } + ], + "pyproject-no-missing-deps": [], +} + + +@pytest.mark.parametrize("test_name", EXPECTED_DATA.keys()) +def test_installed_check(test_name: str): + base_file = TEST_DATA / f"{test_name}.data" + with generate_file(base_file) as file_path: + result = run_on_file(file_path) + assert result == EXPECTED_DATA[test_name] diff --git a/requirements.in b/requirements.in index 8b76e392917e..bdd5e10dfc47 100644 --- a/requirements.in +++ b/requirements.in @@ -8,3 +8,8 @@ typing-extensions==4.5.0 # Fallback env creator for debian microvenv + +# Checker for installed packages +importlib_metadata +packaging +tomli diff --git a/requirements.txt b/requirements.txt index 07145e1832d5..1419e0528ccc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,29 @@ # # pip-compile --generate-hashes requirements.in # +importlib-metadata==6.6.0 \ + --hash=sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed \ + --hash=sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705 + # via -r requirements.in microvenv==2023.2.0 \ --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ --hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3 # via -r requirements.in +packaging==23.1 \ + --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ + --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f + # via -r requirements.in +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via -r requirements.in typing-extensions==4.5.0 \ --hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \ --hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4 - # via -r requirements.in + # via + # -r requirements.in + # importlib-metadata +zipp==3.15.0 \ + --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ + --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 + # via importlib-metadata diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts index c52983d9910b..7240d7be67f8 100644 --- a/src/client/common/process/internal/scripts/index.ts +++ b/src/client/common/process/internal/scripts/index.ts @@ -149,3 +149,8 @@ export function createCondaScript(): string { const script = path.join(SCRIPTS_DIR, 'create_conda.py'); return script; } + +export function installedCheckScript(): string { + const script = path.join(SCRIPTS_DIR, 'installed_check.py'); + return script; +} diff --git a/src/client/common/vscodeApis/languageApis.ts b/src/client/common/vscodeApis/languageApis.ts new file mode 100644 index 000000000000..87681507693d --- /dev/null +++ b/src/client/common/vscodeApis/languageApis.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { DiagnosticChangeEvent, DiagnosticCollection, Disposable, languages } from 'vscode'; + +export function createDiagnosticCollection(name: string): DiagnosticCollection { + return languages.createDiagnosticCollection(name); +} + +export function onDidChangeDiagnostics(handler: (e: DiagnosticChangeEvent) => void): Disposable { + return languages.onDidChangeDiagnostics(handler); +} diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts index 5c279b890a9f..1c242314cb87 100644 --- a/src/client/common/vscodeApis/windowApis.ts +++ b/src/client/common/vscodeApis/windowApis.ts @@ -77,6 +77,10 @@ export function getActiveTextEditor(): TextEditor | undefined { return activeTextEditor; } +export function onDidChangeActiveTextEditor(handler: (e: TextEditor | undefined) => void): Disposable { + return window.onDidChangeActiveTextEditor(handler); +} + export enum MultiStepAction { Back = 'Back', Cancel = 'Cancel', diff --git a/src/client/common/vscodeApis/workspaceApis.ts b/src/client/common/vscodeApis/workspaceApis.ts index 20528c17ec11..0e860743a32d 100644 --- a/src/client/common/vscodeApis/workspaceApis.ts +++ b/src/client/common/vscodeApis/workspaceApis.ts @@ -36,12 +36,12 @@ export function findFiles( return vscode.workspace.findFiles(include, exclude, maxResults, token); } -export function onDidSaveTextDocument( - listener: (e: vscode.TextDocument) => unknown, - thisArgs?: unknown, - disposables?: vscode.Disposable[], -): vscode.Disposable { - return vscode.workspace.onDidSaveTextDocument(listener, thisArgs, disposables); +export function onDidCloseTextDocument(handler: (e: vscode.TextDocument) => unknown): vscode.Disposable { + return vscode.workspace.onDidCloseTextDocument(handler); +} + +export function onDidSaveTextDocument(handler: (e: vscode.TextDocument) => unknown): vscode.Disposable { + return vscode.workspace.onDidSaveTextDocument(handler); } export function getOpenTextDocuments(): readonly vscode.TextDocument[] { diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 542a6ccc3010..8dcea063676a 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -51,10 +51,9 @@ import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHan import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { WorkspaceService } from './common/application/workspace'; import { DynamicPythonDebugConfigurationService } from './debugger/extension/configuration/dynamicdebugConfigurationService'; -import { registerCreateEnvironmentFeatures } from './pythonEnvironments/creation/createEnvApi'; import { IInterpreterQuickPick } from './interpreter/configuration/types'; import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; -import { registerCreateEnvButtonFeatures } from './pythonEnvironments/creation/createEnvButtonContext'; +import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -97,8 +96,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): IInterpreterPathService, ); const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); - registerCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService, pathUtils); - registerCreateEnvButtonFeatures(ext.disposables); + registerAllCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService, pathUtils); } /// ////////////////////////// diff --git a/src/client/pythonEnvironments/creation/common/installCheckUtils.ts b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts new file mode 100644 index 000000000000..8c9817f83b7a --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Diagnostic, DiagnosticSeverity, l10n, Range, TextDocument, Uri } from 'vscode'; +import { installedCheckScript } from '../../../common/process/internal/scripts'; +import { plainExec } from '../../../common/process/rawProcessApis'; +import { IInterpreterPathService } from '../../../common/types'; +import { traceInfo, traceVerbose, traceError } from '../../../logging'; + +interface PackageDiagnostic { + package: string; + line: number; + character: number; + endLine: number; + endCharacter: number; + code: string; + severity: DiagnosticSeverity; +} + +export const INSTALL_CHECKER_SOURCE = 'Python-InstalledPackagesChecker'; + +function parseDiagnostics(data: string): Diagnostic[] { + let diagnostics: Diagnostic[] = []; + try { + const raw = JSON.parse(data) as PackageDiagnostic[]; + diagnostics = raw.map((item) => { + const d = new Diagnostic( + new Range(item.line, item.character, item.endLine, item.endCharacter), + l10n.t(`Package \`${item.package}\` is not installed in the selected environment.`), + item.severity, + ); + d.code = { value: item.code, target: Uri.parse(`https://pypi.org/p/${item.package}`) }; + d.source = INSTALL_CHECKER_SOURCE; + return d; + }); + } catch { + diagnostics = []; + } + return diagnostics; +} + +export async function getInstalledPackagesDiagnostics( + interpreterPathService: IInterpreterPathService, + doc: TextDocument, +): Promise { + const interpreter = interpreterPathService.get(doc.uri); + const scriptPath = installedCheckScript(); + try { + traceInfo('Running installed packages checker: ', interpreter, scriptPath, doc.uri.fsPath); + const result = await plainExec(interpreter, [scriptPath, doc.uri.fsPath]); + traceVerbose('Installed packages check result:\n', result.stdout); + if (result.stderr) { + traceError('Installed packages check error:\n', result.stderr); + } + return parseDiagnostics(result.stdout); + } catch (ex) { + traceError('Error while getting installed packages check result:\n', ex); + } + return []; +} diff --git a/src/client/pythonEnvironments/creation/createEnvButtonContext.ts b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts index cd009bc6118a..4ce7d07ad69d 100644 --- a/src/client/pythonEnvironments/creation/createEnvButtonContext.ts +++ b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts @@ -1,25 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { TextDocument, TextDocumentChangeEvent } from 'vscode'; import { IDisposableRegistry } from '../../common/types'; import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { - onDidOpenTextDocument, - onDidChangeTextDocument, - getOpenTextDocuments, - getConfiguration, - onDidChangeConfiguration, -} from '../../common/vscodeApis/workspaceApis'; -import { isPipInstallableToml } from './provider/venvUtils'; - -async function setPyProjectTomlContextKey(doc: TextDocument): Promise { - if (isPipInstallableToml(doc.getText())) { - await executeCommand('setContext', 'pipInstallableToml', true); - } else { - await executeCommand('setContext', 'pipInstallableToml', false); - } -} +import { getConfiguration, onDidChangeConfiguration } from '../../common/vscodeApis/workspaceApis'; async function setShowCreateEnvButtonContextKey(): Promise { const config = getConfiguration('python'); @@ -27,32 +11,12 @@ async function setShowCreateEnvButtonContextKey(): Promise { await executeCommand('setContext', 'showCreateEnvButton', showCreateEnvButton); } -export function registerCreateEnvButtonFeatures(disposables: IDisposableRegistry): void { +export function registerCreateEnvironmentButtonFeatures(disposables: IDisposableRegistry): void { disposables.push( - onDidOpenTextDocument(async (doc: TextDocument) => { - if (doc.fileName.endsWith('pyproject.toml')) { - await setPyProjectTomlContextKey(doc); - } - }), - onDidChangeTextDocument(async (e: TextDocumentChangeEvent) => { - const doc = e.document; - if (doc.fileName.endsWith('pyproject.toml')) { - await setPyProjectTomlContextKey(doc); - } - }), onDidChangeConfiguration(async () => { await setShowCreateEnvButtonContextKey(); }), ); setShowCreateEnvButtonContextKey(); - - const docs = getOpenTextDocuments().filter( - (doc) => doc.fileName.endsWith('pyproject.toml') && isPipInstallableToml(doc.getText()), - ); - if (docs.length > 0) { - executeCommand('setContext', 'pipInstallableToml', true); - } else { - executeCommand('setContext', 'pipInstallableToml', false); - } } diff --git a/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts b/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts new file mode 100644 index 000000000000..a46a32ce8276 --- /dev/null +++ b/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Diagnostic, DiagnosticCollection, TextDocument, Uri } from 'vscode'; +import { IDisposableRegistry, IInterpreterPathService } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { createDiagnosticCollection, onDidChangeDiagnostics } from '../../common/vscodeApis/languageApis'; +import { getActiveTextEditor, onDidChangeActiveTextEditor } from '../../common/vscodeApis/windowApis'; +import { + getOpenTextDocuments, + onDidCloseTextDocument, + onDidOpenTextDocument, + onDidSaveTextDocument, +} from '../../common/vscodeApis/workspaceApis'; +import { traceVerbose } from '../../logging'; +import { getInstalledPackagesDiagnostics, INSTALL_CHECKER_SOURCE } from './common/installCheckUtils'; + +export const DEPS_NOT_INSTALLED_KEY = 'pythonDepsNotInstalled'; + +async function setContextForActiveEditor(diagnosticCollection: DiagnosticCollection): Promise { + const doc = getActiveTextEditor()?.document; + if (doc && (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml'))) { + const diagnostics = diagnosticCollection.get(doc.uri); + if (diagnostics && diagnostics.length > 0) { + traceVerbose(`Setting context for python dependencies not installed: ${doc.uri.fsPath}`); + await executeCommand('setContext', DEPS_NOT_INSTALLED_KEY, true); + return; + } + } + + // undefined here in the logs means no file was selected + traceVerbose(`Clearing context for python dependencies not installed: ${doc?.uri.fsPath}`); + await executeCommand('setContext', DEPS_NOT_INSTALLED_KEY, false); +} + +export function registerInstalledPackagesDiagnosticsProvider( + disposables: IDisposableRegistry, + interpreterPathService: IInterpreterPathService, +): void { + const diagnosticCollection = createDiagnosticCollection(INSTALL_CHECKER_SOURCE); + const updateDiagnostics = (uri: Uri, diagnostics: Diagnostic[]) => { + if (diagnostics.length > 0) { + diagnosticCollection.set(uri, diagnostics); + } else if (diagnosticCollection.has(uri)) { + diagnosticCollection.delete(uri); + } + }; + + disposables.push(diagnosticCollection); + disposables.push( + onDidOpenTextDocument(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterPathService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }), + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterPathService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }), + onDidCloseTextDocument((e: TextDocument) => { + updateDiagnostics(e.uri, []); + }), + onDidChangeDiagnostics(async () => { + await setContextForActiveEditor(diagnosticCollection); + }), + onDidChangeActiveTextEditor(async () => { + await setContextForActiveEditor(diagnosticCollection); + }), + interpreterPathService.onDidChange(() => { + getOpenTextDocuments().forEach(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterPathService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }); + }), + ); + + getOpenTextDocuments().forEach(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterPathService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }); +} diff --git a/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts b/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts new file mode 100644 index 000000000000..5925b7641f45 --- /dev/null +++ b/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TextDocument } from 'vscode'; +import { IDisposableRegistry } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { + onDidOpenTextDocument, + onDidSaveTextDocument, + getOpenTextDocuments, +} from '../../common/vscodeApis/workspaceApis'; +import { isPipInstallableToml } from './provider/venvUtils'; + +async function setPyProjectTomlContextKey(doc: TextDocument): Promise { + if (isPipInstallableToml(doc.getText())) { + await executeCommand('setContext', 'pipInstallableToml', true); + } else { + await executeCommand('setContext', 'pipInstallableToml', false); + } +} + +export function registerPyProjectTomlFeatures(disposables: IDisposableRegistry): void { + disposables.push( + onDidOpenTextDocument(async (doc: TextDocument) => { + if (doc.fileName.endsWith('pyproject.toml')) { + await setPyProjectTomlContextKey(doc); + } + }), + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.fileName.endsWith('pyproject.toml')) { + await setPyProjectTomlContextKey(doc); + } + }), + ); + + const docs = getOpenTextDocuments().filter( + (doc) => doc.fileName.endsWith('pyproject.toml') && isPipInstallableToml(doc.getText()), + ); + if (docs.length > 0) { + executeCommand('setContext', 'pipInstallableToml', true); + } else { + executeCommand('setContext', 'pipInstallableToml', false); + } +} diff --git a/src/client/pythonEnvironments/creation/registrations.ts b/src/client/pythonEnvironments/creation/registrations.ts new file mode 100644 index 000000000000..eeb04036bc1b --- /dev/null +++ b/src/client/pythonEnvironments/creation/registrations.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposableRegistry, IInterpreterPathService, IPathUtils } from '../../common/types'; +import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; +import { registerCreateEnvironmentFeatures } from './createEnvApi'; +import { registerCreateEnvironmentButtonFeatures } from './createEnvButtonContext'; +import { registerInstalledPackagesDiagnosticsProvider } from './installedPackagesDiagnostic'; +import { registerPyProjectTomlFeatures } from './pyProjectTomlContext'; + +export function registerAllCreateEnvironmentFeatures( + disposables: IDisposableRegistry, + interpreterQuickPick: IInterpreterQuickPick, + interpreterPathService: IInterpreterPathService, + pathUtils: IPathUtils, +): void { + registerCreateEnvironmentFeatures(disposables, interpreterQuickPick, interpreterPathService, pathUtils); + registerCreateEnvironmentButtonFeatures(disposables); + registerPyProjectTomlFeatures(disposables); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService); +} diff --git a/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts new file mode 100644 index 000000000000..de8e263fc3fe --- /dev/null +++ b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { Diagnostic, TextDocument, Range, Uri } from 'vscode'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { getInstalledPackagesDiagnostics } from '../../../../client/pythonEnvironments/creation/common/installCheckUtils'; +import { IInterpreterPathService } from '../../../../client/common/types'; + +chaiUse(chaiAsPromised); + +function getSomeRequirementFile(): typemoq.IMock { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'pip-requirements'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'flake8-csv'); + return someFile; +} + +const MISSING_PACKAGES_STR = + '[{"line": 8, "character": 34, "endLine": 8, "endCharacter": 44, "package": "flake8-csv", "code": "not-installed", "severity": 3}]'; +const MISSING_PACKAGES: Diagnostic[] = [ + { + range: new Range(8, 34, 8, 44), + message: 'Package `flake8-csv` is not installed in the selected environment.', + source: 'Python-InstalledPackagesChecker', + code: { value: 'not-installed', target: Uri.parse(`https://pypi.org/p/flake8-csv`) }, + severity: 3, + relatedInformation: [], + }, +]; + +suite('Install check diagnostics tests', () => { + let plainExecStub: sinon.SinonStub; + let interpreterPathService: typemoq.IMock; + + setup(() => { + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + interpreterPathService = typemoq.Mock.ofType(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Test parse diagnostics', async () => { + plainExecStub.resolves({ stdout: MISSING_PACKAGES_STR, stderr: '' }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object); + + assert.deepStrictEqual(result, MISSING_PACKAGES); + }); + + test('Test parse empty diagnostics', async () => { + plainExecStub.resolves({ stdout: '', stderr: '' }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object); + + assert.deepStrictEqual(result, []); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts index 31842420dd59..eec2d066aadb 100644 --- a/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts +++ b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts @@ -1,16 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -/* eslint-disable @typescript-eslint/no-explicit-any */ import * as chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; -import { TextDocument, TextDocumentChangeEvent, WorkspaceConfiguration } from 'vscode'; +import { WorkspaceConfiguration } from 'vscode'; import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; import { IDisposableRegistry } from '../../../client/common/types'; -import { registerCreateEnvButtonFeatures } from '../../../client/pythonEnvironments/creation/createEnvButtonContext'; +import { registerCreateEnvironmentButtonFeatures } from '../../../client/pythonEnvironments/creation/createEnvButtonContext'; chaiUse(chaiAsPromised); @@ -20,57 +19,17 @@ class FakeDisposable { } } -function getInstallableToml(): typemoq.IMock { - const pyprojectTomlPath = 'pyproject.toml'; - const pyprojectToml = typemoq.Mock.ofType(); - pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); - pyprojectToml - .setup((p) => p.getText(typemoq.It.isAny())) - .returns( - () => - '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', - ); - return pyprojectToml; -} - -function getNonInstallableToml(): typemoq.IMock { - const pyprojectTomlPath = 'pyproject.toml'; - const pyprojectToml = typemoq.Mock.ofType(); - pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); - pyprojectToml - .setup((p) => p.getText(typemoq.It.isAny())) - .returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n'); - return pyprojectToml; -} - -function getSomeFile(): typemoq.IMock { - const someFilePath = 'something.py'; - const someFile = typemoq.Mock.ofType(); - someFile.setup((p) => p.fileName).returns(() => someFilePath); - someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); - return someFile; -} - -suite('PyProject.toml Create Env Features', () => { +suite('Create Env content button settings tests', () => { let executeCommandStub: sinon.SinonStub; const disposables: IDisposableRegistry = []; - let getOpenTextDocumentsStub: sinon.SinonStub; - let onDidOpenTextDocumentStub: sinon.SinonStub; - let onDidChangeTextDocumentStub: sinon.SinonStub; let onDidChangeConfigurationStub: sinon.SinonStub; let getConfigurationStub: sinon.SinonStub; let configMock: typemoq.IMock; setup(() => { executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); - getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); - onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); - onDidChangeTextDocumentStub = sinon.stub(workspaceApis, 'onDidChangeTextDocument'); getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); onDidChangeConfigurationStub = sinon.stub(workspaceApis, 'onDidChangeConfiguration'); - - onDidOpenTextDocumentStub.returns(new FakeDisposable()); - onDidChangeTextDocumentStub.returns(new FakeDisposable()); onDidChangeConfigurationStub.returns(new FakeDisposable()); configMock = typemoq.Mock.ofType(); @@ -84,29 +43,23 @@ suite('PyProject.toml Create Env Features', () => { }); test('python.createEnvironment.contentButton setting is set to "show", no files open', async () => { - getOpenTextDocumentsStub.returns([]); - - registerCreateEnvButtonFeatures(disposables); + registerCreateEnvironmentButtonFeatures(disposables); assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); }); test('python.createEnvironment.contentButton setting is set to "hide", no files open', async () => { configMock.reset(); configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); - getOpenTextDocumentsStub.returns([]); - registerCreateEnvButtonFeatures(disposables); + registerCreateEnvironmentButtonFeatures(disposables); assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); }); test('python.createEnvironment.contentButton setting changed from "hide" to "show"', async () => { configMock.reset(); configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); - getOpenTextDocumentsStub.returns([]); let handler: () => void = () => { /* do nothing */ @@ -116,9 +69,8 @@ suite('PyProject.toml Create Env Features', () => { return new FakeDisposable(); }); - registerCreateEnvButtonFeatures(disposables); + registerCreateEnvironmentButtonFeatures(disposables); assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); executeCommandStub.reset(); configMock.reset(); @@ -131,7 +83,6 @@ suite('PyProject.toml Create Env Features', () => { test('python.createEnvironment.contentButton setting changed from "show" to "hide"', async () => { configMock.reset(); configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); - getOpenTextDocumentsStub.returns([]); let handler: () => void = () => { /* do nothing */ @@ -141,9 +92,8 @@ suite('PyProject.toml Create Env Features', () => { return new FakeDisposable(); }); - registerCreateEnvButtonFeatures(disposables); + registerCreateEnvironmentButtonFeatures(disposables); assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); executeCommandStub.reset(); configMock.reset(); @@ -152,195 +102,4 @@ suite('PyProject.toml Create Env Features', () => { assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); }); - - test('Installable pyproject.toml is already open in the editor on extension activate', async () => { - const pyprojectToml = getInstallableToml(); - getOpenTextDocumentsStub.returns([pyprojectToml.object]); - - registerCreateEnvButtonFeatures(disposables); - - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non installable pyproject.toml is already open in the editor on extension activate', async () => { - const pyprojectToml = getNonInstallableToml(); - getOpenTextDocumentsStub.returns([pyprojectToml.object]); - - registerCreateEnvButtonFeatures(disposables); - - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Some random file open in the editor on extension activate', async () => { - const someFile = getSomeFile(); - getOpenTextDocumentsStub.returns([someFile.object]); - - registerCreateEnvButtonFeatures(disposables); - - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Installable pyproject.toml is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getInstallableToml(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', true)); - - handler(pyprojectToml.object); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non Installable pyproject.toml is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getNonInstallableToml(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - handler(pyprojectToml.object); - - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Some random file is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const someFile = getSomeFile(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - handler(someFile.object); - - assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', false)); - }); - - test('Installable pyproject.toml is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getInstallableToml(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - - handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); - - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non Installable pyproject.toml is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getNonInstallableToml(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Non Installable pyproject.toml is changed to Installable', async () => { - getOpenTextDocumentsStub.returns([]); - - let openHandler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - openHandler = callback; - return new FakeDisposable(); - }); - - let changeHandler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - changeHandler = callback; - return new FakeDisposable(); - }); - - const nonInatallablePyprojectToml = getNonInstallableToml(); - const installablePyprojectToml = getInstallableToml(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - openHandler(nonInatallablePyprojectToml.object); - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - changeHandler({ contentChanges: [], document: installablePyprojectToml.object, reason: undefined }); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Some random file is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const someFile = getSomeFile(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - handler({ contentChanges: [], document: someFile.object, reason: undefined }); - - assert.ok(executeCommandStub.notCalled); - }); }); diff --git a/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts new file mode 100644 index 000000000000..10fe06bba442 --- /dev/null +++ b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts @@ -0,0 +1,333 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { Diagnostic, DiagnosticCollection, TextEditor, Range, Uri, TextDocument } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as languageApis from '../../../client/common/vscodeApis/languageApis'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { IDisposableRegistry, IInterpreterPathService } from '../../../client/common/types'; +import * as installUtils from '../../../client/pythonEnvironments/creation/common/installCheckUtils'; +import { + DEPS_NOT_INSTALLED_KEY, + registerInstalledPackagesDiagnosticsProvider, +} from '../../../client/pythonEnvironments/creation/installedPackagesDiagnostic'; + +chaiUse(chaiAsPromised); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +const MISSING_PACKAGES: Diagnostic[] = [ + { + range: new Range(8, 34, 8, 44), + message: 'Package `flake8-csv` is not installed in the selected environment.', + source: 'Python-InstalledPackagesChecker', + code: { value: 'not-installed', target: Uri.parse(`https://pypi.org/p/flake8-csv`) }, + severity: 3, + relatedInformation: [], + }, +]; + +function getSomeFile(): typemoq.IMock { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); + return someFile; +} + +function getSomeRequirementFile(): typemoq.IMock { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'pip-requirements'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'flake8-csv'); + return someFile; +} + +function getPyProjectTomlFile(): typemoq.IMock { + const someFilePath = 'pyproject.toml'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'toml'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[project]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + ); + return someFile; +} + +function getSomeTomlFile(): typemoq.IMock { + const someFilePath = 'something.toml'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'toml'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[something]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + ); + return someFile; +} + +suite('Create Env content button settings tests', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let getOpenTextDocumentsStub: sinon.SinonStub; + let onDidOpenTextDocumentStub: sinon.SinonStub; + let onDidSaveTextDocumentStub: sinon.SinonStub; + let onDidCloseTextDocumentStub: sinon.SinonStub; + let onDidChangeDiagnosticsStub: sinon.SinonStub; + let onDidChangeActiveTextEditorStub: sinon.SinonStub; + let createDiagnosticCollectionStub: sinon.SinonStub; + let diagnosticCollection: typemoq.IMock; + let getActiveTextEditorStub: sinon.SinonStub; + let textEditor: typemoq.IMock; + let getInstalledPackagesDiagnosticsStub: sinon.SinonStub; + let interpreterPathService: typemoq.IMock; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); + getOpenTextDocumentsStub.returns([]); + + onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); + onDidSaveTextDocumentStub = sinon.stub(workspaceApis, 'onDidSaveTextDocument'); + onDidCloseTextDocumentStub = sinon.stub(workspaceApis, 'onDidCloseTextDocument'); + onDidOpenTextDocumentStub.returns(new FakeDisposable()); + onDidSaveTextDocumentStub.returns(new FakeDisposable()); + onDidCloseTextDocumentStub.returns(new FakeDisposable()); + + onDidChangeDiagnosticsStub = sinon.stub(languageApis, 'onDidChangeDiagnostics'); + onDidChangeDiagnosticsStub.returns(new FakeDisposable()); + createDiagnosticCollectionStub = sinon.stub(languageApis, 'createDiagnosticCollection'); + diagnosticCollection = typemoq.Mock.ofType(); + diagnosticCollection.setup((d) => d.set(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => undefined); + diagnosticCollection.setup((d) => d.clear()).returns(() => undefined); + diagnosticCollection.setup((d) => d.delete(typemoq.It.isAny())).returns(() => undefined); + diagnosticCollection.setup((d) => d.has(typemoq.It.isAny())).returns(() => false); + createDiagnosticCollectionStub.returns(diagnosticCollection.object); + + onDidChangeActiveTextEditorStub = sinon.stub(windowApis, 'onDidChangeActiveTextEditor'); + onDidChangeActiveTextEditorStub.returns(new FakeDisposable()); + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + textEditor = typemoq.Mock.ofType(); + getActiveTextEditorStub.returns(textEditor.object); + + getInstalledPackagesDiagnosticsStub = sinon.stub(installUtils, 'getInstalledPackagesDiagnostics'); + interpreterPathService = typemoq.Mock.ofType(); + interpreterPathService + .setup((i) => i.onDidChange(typemoq.It.isAny(), undefined, undefined)) + .returns(() => new FakeDisposable()); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Ensure nothing is run if there are no open documents', () => { + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + assert.ok(executeCommandStub.notCalled); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test('Should not run packages check if opened files are not dep files', () => { + const someFile = getSomeFile(); + const someTomlFile = getSomeTomlFile(); + getOpenTextDocumentsStub.returns([someFile.object, someTomlFile.object]); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + assert.ok(executeCommandStub.notCalled); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test('Should run packages check if opened files are dep files', () => { + const reqFile = getSomeRequirementFile(); + const tomlFile = getPyProjectTomlFile(); + getOpenTextDocumentsStub.returns([reqFile.object, tomlFile.object]); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + assert.ok(getInstalledPackagesDiagnosticsStub.calledTwice); + }); + + [getSomeRequirementFile().object, getPyProjectTomlFile().object].forEach((file) => { + test(`Should run packages check on open of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.calledOnce); + }); + + test(`Should run packages check on save of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.calledOnce); + }); + + test(`Should run packages check on close of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidCloseTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + + diagnosticCollection.reset(); + diagnosticCollection.setup((d) => d.delete(typemoq.It.isAny())).verifiable(typemoq.Times.once()); + diagnosticCollection + .setup((d) => d.has(typemoq.It.isAny())) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + handler(file); + diagnosticCollection.verifyAll(); + }); + + test(`Should trigger a context update on active editor switch to dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeActiveTextEditorStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => MISSING_PACKAGES); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, true)); + }); + + test(`Should trigger a context update to true on diagnostic change to dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeDiagnosticsStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => MISSING_PACKAGES); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, true)); + }); + }); + + [getSomeFile().object, getSomeTomlFile().object].forEach((file) => { + test(`Should not run packages check on open of a non dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test(`Should not run packages check on save of a non dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test(`Should trigger a context update on active editor switch to non-dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeActiveTextEditorStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => []); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, false)); + }); + + test(`Should trigger a context update to false on diagnostic change to non-dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeDiagnosticsStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => []); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, false)); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts new file mode 100644 index 000000000000..7106ee64162f --- /dev/null +++ b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { TextDocument } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { registerPyProjectTomlFeatures } from '../../../client/pythonEnvironments/creation/pyProjectTomlContext'; + +chaiUse(chaiAsPromised); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +function getInstallableToml(): typemoq.IMock { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + return pyprojectToml; +} + +function getNonInstallableToml(): typemoq.IMock { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n'); + return pyprojectToml; +} + +function getSomeFile(): typemoq.IMock { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); + return someFile; +} + +suite('PyProject.toml Create Env Features', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let getOpenTextDocumentsStub: sinon.SinonStub; + let onDidOpenTextDocumentStub: sinon.SinonStub; + let onDidSaveTextDocumentStub: sinon.SinonStub; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); + onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); + onDidSaveTextDocumentStub = sinon.stub(workspaceApis, 'onDidSaveTextDocument'); + + onDidOpenTextDocumentStub.returns(new FakeDisposable()); + onDidSaveTextDocumentStub.returns(new FakeDisposable()); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getNonInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file open in the editor on extension activate', async () => { + const someFile = getSomeFile(); + getOpenTextDocumentsStub.returns([someFile.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', true)); + + handler(pyprojectToml.object); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(someFile.object); + + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Non Installable pyproject.toml is changed to Installable', async () => { + getOpenTextDocumentsStub.returns([]); + + let openHandler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + openHandler = callback; + return new FakeDisposable(); + }); + + let changeHandler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + changeHandler = callback; + return new FakeDisposable(); + }); + + const nonInatallablePyprojectToml = getNonInstallableToml(); + const installablePyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + openHandler(nonInatallablePyprojectToml.object); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + changeHandler(installablePyprojectToml.object); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Some random file is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(someFile.object); + + assert.ok(executeCommandStub.notCalled); + }); +}); From c9a7268a7e6ae6f968ed595d4aaa4e0b187b7407 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 24 May 2023 10:57:25 -0700 Subject: [PATCH 096/336] Revert "Remove hack to check the vscode version" (#21294) Reverts microsoft/vscode-python#21180 For https://github.com/microsoft/vscode-python/issues/20769 --- package-lock.json | 2 +- package.json | 2 +- src/test/debuggerTest.ts | 4 ++-- src/test/multiRootTest.ts | 4 ++-- src/test/standardTest.ts | 4 ++-- src/test/utils/vscode.ts | 12 ++++++++++-- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index f00cbb0a912e..bfbd316f269d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.78.0" + "vscode": "^1.78.0-20230421" } }, "node_modules/@azure/abort-controller": { diff --git a/package.json b/package.json index 8b364b5d60e6..a92875a230d4 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.78.0" + "vscode": "^1.78.0-20230421" }, "keywords": [ "python", diff --git a/src/test/debuggerTest.ts b/src/test/debuggerTest.ts index 36a0060b9303..949f14caee3d 100644 --- a/src/test/debuggerTest.ts +++ b/src/test/debuggerTest.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; -import { getVersion } from './utils/vscode'; +import { getChannel } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = '1'; @@ -17,7 +17,7 @@ function start() { extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), launchArgs: [workspacePath], - version: getVersion(), + version: getChannel(), extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Debugger tests (with errors)', ex); diff --git a/src/test/multiRootTest.ts b/src/test/multiRootTest.ts index 9a6d7cd1b904..c8c63b6dabe5 100644 --- a/src/test/multiRootTest.ts +++ b/src/test/multiRootTest.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; import { initializeLogger } from './testLogger'; -import { getVersion } from './utils/vscode'; +import { getChannel } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; @@ -17,7 +17,7 @@ function start() { extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), launchArgs: [workspacePath], - version: getVersion(), + version: getChannel(), extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Multiroot tests (with errors)', ex); diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts index c8416ebf1d7d..0562d1adf431 100644 --- a/src/test/standardTest.ts +++ b/src/test/standardTest.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTests } from '@vscode/test-electron'; import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../client/common/constants'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; -import { getVersion } from './utils/vscode'; +import { getChannel } from './utils/vscode'; // If running smoke tests, we don't have access to this. if (process.env.TEST_FILES_SUFFIX !== 'smoke.test') { @@ -76,7 +76,7 @@ async function installPylanceExtension(vscodeExecutablePath: string) { async function start() { console.log('*'.repeat(100)); console.log('Start Standard tests'); - const channel = getVersion(); + const channel = getChannel(); console.log(`Using ${channel} build of VS Code.`); const vscodeExecutablePath = await downloadAndUnzipVSCode(channel); const baseLaunchArgs = diff --git a/src/test/utils/vscode.ts b/src/test/utils/vscode.ts index 44f745dbaf90..a10ceb2e8881 100644 --- a/src/test/utils/vscode.ts +++ b/src/test/utils/vscode.ts @@ -2,14 +2,22 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -export function getVersion(): string { +const insidersVersion = /^\^(\d+\.\d+\.\d+)-(insider|\d{8})$/; + +export function getChannel(): string { if (process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL) { return process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL; } const packageJsonPath = path.join(EXTENSION_ROOT_DIR, 'package.json'); if (fs.pathExistsSync(packageJsonPath)) { const packageJson = fs.readJSONSync(packageJsonPath); - return packageJson.engines.vscode.replace('^', ''); + const engineVersion = packageJson.engines.vscode; + if (insidersVersion.test(engineVersion)) { + // Can't pass in the version number for an insiders build; + // https://github.com/microsoft/vscode-test/issues/176 + return 'insiders'; + } + return engineVersion.replace('^', ''); } return 'stable'; } From 4b4e5b7f5ce637cdc626a5d473f8723cb72dccf7 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Wed, 24 May 2023 13:01:41 -0700 Subject: [PATCH 097/336] Update pyright version (#21296) Fix error in tests, updating pyright version --- .github/workflows/build.yml | 1 + .github/workflows/pr-check.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 805077ffdb46..f418a802ff8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,6 +103,7 @@ jobs: - name: Run Pyright uses: jakebailey/pyright-action@v1 with: + version: 1.1.308 working-directory: 'pythonFiles' ### Non-smoke tests diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 2ac560af995d..84903463e204 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -77,6 +77,7 @@ jobs: - name: Run Pyright uses: jakebailey/pyright-action@v1 with: + version: 1.1.308 working-directory: 'pythonFiles' ### Non-smoke tests From f2f5fe26c41d7fff79b56b5a0490cd1c3b49f451 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Wed, 24 May 2023 13:31:26 -0700 Subject: [PATCH 098/336] Check config type in the ChildProcessAttachEvents (#21272) --- .../hooks/childProcessAttachHandler.ts | 3 ++- .../childProcessAttachHandler.unit.test.ts | 21 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts index 6851e54a8723..23602ffce086 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts @@ -9,6 +9,7 @@ import { swallowExceptions } from '../../../common/utils/decorators'; import { AttachRequestArguments } from '../../types'; import { DebuggerEvents } from './constants'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from './types'; +import { DebuggerTypeName } from '../../constants'; /** * This class is responsible for automatically attaching the debugger to any @@ -25,7 +26,7 @@ export class ChildProcessAttachEventHandler implements IDebugSessionEventHandler @swallowExceptions('Handle child process launch') public async handleCustomEvent(event: DebugSessionCustomEvent): Promise { - if (!event) { + if (!event || event.session.configuration.type !== DebuggerTypeName) { return; } diff --git a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts index ee9a59c8e6aa..b1053def2eba 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts @@ -9,6 +9,7 @@ import { ChildProcessAttachEventHandler } from '../../../../client/debugger/exte import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; import { DebuggerEvents } from '../../../../client/debugger/extension/hooks/constants'; import { AttachRequestArguments } from '../../../../client/debugger/types'; +import { DebuggerTypeName } from '../../../../client/debugger/constants'; suite('Debug - Child Process', () => { test('Do not attach if the event is undefined', async () => { @@ -21,7 +22,15 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; + await handler.handleCustomEvent({ event: 'abc', body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Do not attach to child process if debugger type is different', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: any = {}; + const session: any = { configuration: { type: 'other-type' } }; await handler.handleCustomEvent({ event: 'abc', body, session }); verify(attachService.attach(body, session)).never(); }); @@ -29,7 +38,7 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; await handler.handleCustomEvent({ event: DebuggerEvents.PtvsdAttachToSubprocess, body, session }); verify(attachService.attach(body, session)).never(); }); @@ -37,7 +46,7 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); verify(attachService.attach(body, session)).never(); }); @@ -51,9 +60,11 @@ suite('Debug - Child Process', () => { port: 1234, subProcessId: 2, }; - const session: any = {}; + const session: any = { + configuration: { type: DebuggerTypeName }, + }; when(attachService.attach(body, session)).thenThrow(new Error('Kaboom')); - await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session: {} as any }); + await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); verify(attachService.attach(body, anything())).once(); const [, secondArg] = capture(attachService.attach).last(); expect(secondArg).to.deep.equal(session); From e2a9cecf9cec0f3a42d2723927a83d486c9b5bd8 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 24 May 2023 16:42:24 -0700 Subject: [PATCH 099/336] allow large scale testing (#21269) allows new testing rewrite to handle 500+ tests and load and run these tests. High limit tested was 10,000 tests. --- .vscode/launch.json | 3 +- pythonFiles/unittestadapter/discovery.py | 8 +- pythonFiles/unittestadapter/utils.py | 9 +- .../vscode_pytest/run_pytest_script.py | 90 +++++++++++++++++++ src/client/common/process/types.ts | 1 + .../testing/testController/common/server.ts | 32 ++++--- .../testing/testController/common/utils.ts | 37 ++++++++ .../pytest/pytestExecutionAdapter.ts | 54 +++++++++-- .../testController/workspaceTestAdapter.ts | 5 +- 9 files changed, 217 insertions(+), 22 deletions(-) create mode 100644 pythonFiles/vscode_pytest/run_pytest_script.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 1ca0db3dc858..82981a93305d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,8 +22,7 @@ // Enable this to log telemetry to the output during debugging "XVSC_PYTHON_LOG_TELEMETRY": "1", // Enable this to log debugger output. Directory must exist ahead of time - "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex", - "ENABLE_PYTHON_TESTING_REWRITE": "1" + "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex" } }, { diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index dc0a139ed5a2..bcc2fd967f78 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -8,7 +8,13 @@ import sys import traceback import unittest -from typing import List, Literal, Optional, Tuple, Union +from typing import List, Optional, Tuple, Union + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + +from typing_extensions import Literal # Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/pythonFiles/unittestadapter/utils.py b/pythonFiles/unittestadapter/utils.py index 568ff30ee92d..9c8b896a8d6e 100644 --- a/pythonFiles/unittestadapter/utils.py +++ b/pythonFiles/unittestadapter/utils.py @@ -6,8 +6,15 @@ import inspect import os import pathlib +import sys import unittest -from typing import List, Tuple, TypedDict, Union +from typing import List, Tuple, Union + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + +from typing_extensions import TypedDict # Types diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py new file mode 100644 index 000000000000..f6d6bdcafd5f --- /dev/null +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -0,0 +1,90 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +import io +import json +import os +import pathlib +import socket +import sys +from typing import List + +import pytest + +CONTENT_LENGTH: str = "Content-Length:" + + +def process_rpc_json(data: str) -> List[str]: + """Process the JSON data which comes from the server which runs the pytest discovery.""" + str_stream: io.StringIO = io.StringIO(data) + + length: int = 0 + + while True: + line: str = str_stream.readline() + if CONTENT_LENGTH.lower() in line.lower(): + length = int(line[len(CONTENT_LENGTH) :]) + break + + if not line or line.isspace(): + raise ValueError("Header does not contain Content-Length") + + while True: + line: str = str_stream.readline() + if not line or line.isspace(): + break + + raw_json: str = str_stream.read(length) + return json.loads(raw_json) + + +# This script handles running pytest via pytest.main(). It is called via run in the +# pytest execution adapter and gets the test_ids to run via stdin and the rest of the +# args through sys.argv. It then runs pytest.main() with the args and test_ids. + +if __name__ == "__main__": + # Add the root directory to the path so that we can import the plugin. + directory_path = pathlib.Path(__file__).parent.parent + sys.path.append(os.fspath(directory_path)) + # Get the rest of the args to run with pytest. + args = sys.argv[1:] + run_test_ids_port = os.environ.get("RUN_TEST_IDS_PORT") + run_test_ids_port_int = ( + int(run_test_ids_port) if run_test_ids_port is not None else 0 + ) + test_ids_from_buffer = [] + try: + client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client_socket.connect(("localhost", run_test_ids_port_int)) + print(f"CLIENT: Server listening on port {run_test_ids_port_int}...") + buffer = b"" + + while True: + # Receive the data from the client + data = client_socket.recv(1024 * 1024) + if not data: + break + + # Append the received data to the buffer + buffer += data + + try: + # Try to parse the buffer as JSON + test_ids_from_buffer = process_rpc_json(buffer.decode("utf-8")) + # Clear the buffer as complete JSON object is received + buffer = b"" + + # Process the JSON data + print(f"Received JSON data: {test_ids_from_buffer}") + break + except json.JSONDecodeError: + # JSON decoding error, the complete JSON object is not yet received + continue + except socket.error as e: + print(f"Error: Could not connect to runTestIdsPort: {e}") + print("Error: Could not connect to runTestIdsPort") + try: + if test_ids_from_buffer: + arg_array = ["-p", "vscode_pytest"] + args + test_ids_from_buffer + pytest.main(arg_array) + except json.JSONDecodeError: + print("Error: Could not parse test ids from stdin") diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 8298957285e8..62e787b694b5 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -25,6 +25,7 @@ export type SpawnOptions = ChildProcessSpawnOptions & { throwOnStdErr?: boolean; extraVariables?: NodeJS.ProcessEnv; outputChannel?: OutputChannel; + stdinStr?: string; }; export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 6849f0f8969a..a00623aa33c7 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -26,23 +26,35 @@ export class PythonTestServer implements ITestServer, Disposable { constructor(private executionFactory: IPythonExecutionFactory, private debugLauncher: ITestDebugLauncher) { this.server = net.createServer((socket: net.Socket) => { + let buffer: Buffer = Buffer.alloc(0); // Buffer to accumulate received data socket.on('data', (data: Buffer) => { try { let rawData: string = data.toString(); - - while (rawData.length > 0) { - const rpcHeaders = jsonRPCHeaders(rawData); + buffer = Buffer.concat([buffer, data]); + while (buffer.length > 0) { + const rpcHeaders = jsonRPCHeaders(buffer.toString()); const uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); + const totalContentLength = rpcHeaders.headers.get('Content-Length'); + if (!uuid) { + traceLog('On data received: Error occurred because payload UUID is undefined'); + this._onDataReceived.fire({ uuid: '', data: '' }); + return; + } + if (!this.uuids.includes(uuid)) { + traceLog('On data received: Error occurred because the payload UUID is not recognized'); + this._onDataReceived.fire({ uuid: '', data: '' }); + return; + } rawData = rpcHeaders.remainingRawData; - if (uuid && this.uuids.includes(uuid)) { - const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); - rawData = rpcContent.remainingRawData; - this._onDataReceived.fire({ uuid, data: rpcContent.extractedJSON }); + const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); + const extractedData = rpcContent.extractedJSON; + if (extractedData.length === Number(totalContentLength)) { + // do not send until we have the full content + this._onDataReceived.fire({ uuid, data: extractedData }); this.uuids = this.uuids.filter((u) => u !== uuid); + buffer = Buffer.alloc(0); } else { - traceLog(`Error processing test server request: uuid not found`); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; + break; } } } catch (ex) { diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index e0bad383d695..88e3450d35dc 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -1,5 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as net from 'net'; +import { traceLog } from '../../../logging'; export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); @@ -50,3 +52,38 @@ export function jsonRPCContent(headers: Map, rawData: string): I remainingRawData, }; } +export const startServer = (testIds: string): Promise => + new Promise((resolve, reject) => { + const server = net.createServer((socket: net.Socket) => { + // Convert the test_ids array to JSON + const testData = JSON.stringify(testIds); + + // Create the headers + const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; + + // Create the payload by concatenating the headers and the test data + const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + + // Send the payload to the socket + socket.write(payload); + + // Handle socket events + socket.on('data', (data) => { + traceLog('Received data:', data.toString()); + }); + + socket.on('end', () => { + traceLog('Client disconnected'); + }); + }); + + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + traceLog(`Server listening on port ${port}`); + resolve(port); + }); + + server.on('error', (error: Error) => { + reject(error); + }); + }); diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 623fd1ff3a8c..4c6dcd9cdbee 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -3,9 +3,10 @@ import { Uri } from 'vscode'; import * as path from 'path'; +import * as net from 'net'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; -import { traceVerbose } from '../../../logging'; +import { traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, @@ -90,6 +91,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { TEST_PORT: this.testServer.getPort().toString(), }, outputChannel: this.outputChannel, + stdinStr: testIds.toString(), }; // Create the Python environment in which to execute the command. @@ -114,7 +116,48 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } - const pluginArgs = ['-p', 'vscode_pytest', '-v'].concat(testArgs).concat(testIds); + const pluginArgs = ['-p', 'vscode_pytest'].concat(testArgs).concat(testIds); + const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); + const runArgs = [scriptPath, ...testArgs]; + + const testData = JSON.stringify(testIds); + const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; + const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + + const startServer = (): Promise => + new Promise((resolve, reject) => { + const server = net.createServer((socket: net.Socket) => { + socket.on('end', () => { + traceLog('Client disconnected'); + }); + }); + + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + traceLog(`Server listening on port ${port}`); + resolve(port); + }); + + server.on('error', (error: Error) => { + reject(error); + }); + server.on('connection', (socket: net.Socket) => { + socket.write(payload); + traceLog('payload sent', payload); + }); + }); + + // Start the server and wait until it is listening + await startServer() + .then((assignedPort) => { + traceLog(`Server started and listening on port ${assignedPort}`); + if (spawnOptions.extraVariables) + spawnOptions.extraVariables.RUN_TEST_IDS_PORT = assignedPort.toString(); + }) + .catch((error) => { + console.error('Error starting server:', error); + }); + if (debugBool) { const pytestPort = this.testServer.getPort().toString(); const pytestUUID = uuid.toString(); @@ -129,9 +172,10 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { console.debug(`Running debug test with arguments: ${pluginArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions); } else { - const runArgs = ['-m', 'pytest'].concat(pluginArgs); - console.debug(`Running test with arguments: ${runArgs.join(' ')}\r\n`); - execService?.exec(runArgs, spawnOptions); + await execService?.exec(runArgs, spawnOptions).catch((ex) => { + console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); + return Promise.reject(ex); + }); } } catch (ex) { console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index b22fee69d295..0b19d4d87d6f 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -348,12 +348,11 @@ export class WorkspaceTestAdapter { const testingErrorConst = this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; const { errors } = rawTestData; - traceError(testingErrorConst, '\r\n', errors!.join('\r\n\r\n')); - + traceError(testingErrorConst, '\r\n', errors?.join('\r\n\r\n')); let errorNode = testController.items.get(`DiscoveryError:${workspacePath}`); const message = util.format( `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, - errors!.join('\r\n\r\n'), + errors?.join('\r\n\r\n'), ); if (errorNode === undefined) { From b916981ed6a9a6a8267477637843e6fb4b66d7f3 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 24 May 2023 16:42:44 -0700 Subject: [PATCH 100/336] remove unneeded multiroot code (#21295) removed extra steps to wrap data since this creates duplicate folders in the controller and only keeps the most recent instead of all the roots from different workspaces. --- .../testing/testController/controller.ts | 5 +-- .../testController/workspaceTestAdapter.ts | 43 +------------------ 2 files changed, 2 insertions(+), 46 deletions(-) diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 58eaa9c890d6..20fcfa49bb69 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -254,8 +254,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc testAdapter.discoverTests( this.testController, this.refreshCancellation.token, - this.testAdapters.size > 1, - this.workspaceService.workspaceFile?.fsPath, this.pythonExecFactory, ); } else { @@ -274,8 +272,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc testAdapter.discoverTests( this.testController, this.refreshCancellation.token, - this.testAdapters.size > 1, - this.workspaceService.workspaceFile?.fsPath, + this.pythonExecFactory, ); } else { // else use OLD test discovery mechanism diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 0b19d4d87d6f..ceb6a0b60afb 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -29,13 +29,7 @@ import { getTestCaseNodes, RunTestTag, } from './common/testItemUtilities'; -import { - DiscoveredTestItem, - DiscoveredTestNode, - DiscoveredTestType, - ITestDiscoveryAdapter, - ITestExecutionAdapter, -} from './common/types'; +import { DiscoveredTestItem, DiscoveredTestNode, ITestDiscoveryAdapter, ITestExecutionAdapter } from './common/types'; import { fixLogLines } from './common/utils'; import { IPythonExecutionFactory } from '../../common/process/types'; import { ITestDebugLauncher } from '../common/types'; @@ -288,8 +282,6 @@ export class WorkspaceTestAdapter { public async discoverTests( testController: TestController, token?: CancellationToken, - isMultiroot?: boolean, - workspaceFilePath?: string, executionFactory?: IPythonExecutionFactory, ): Promise { sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); @@ -366,39 +358,6 @@ export class WorkspaceTestAdapter { // then parse and insert test data. testController.items.delete(`DiscoveryError:${workspacePath}`); - // Wrap the data under a root node named after the test provider. - const wrappedTests = rawTestData.tests; - - // If we are in a multiroot workspace scenario, wrap the current folder's test result in a tree under the overall root + the current folder name. - let rootPath = workspacePath; - let childrenRootPath = rootPath; - let childrenRootName = path.basename(rootPath); - - if (isMultiroot) { - rootPath = workspaceFilePath!; - childrenRootPath = workspacePath; - childrenRootName = path.basename(workspacePath); - } - - const children = [ - { - path: childrenRootPath, - name: childrenRootName, - type_: 'folder' as DiscoveredTestType, - id_: childrenRootPath, - children: wrappedTests ? [wrappedTests] : [], - }, - ]; - - // Update the raw test data with the wrapped data. - rawTestData.tests = { - path: rootPath, - name: this.testProvider, - type_: 'folder', - id_: rootPath, - children, - }; - if (rawTestData.tests) { // If the test root for this folder exists: Workspace refresh, update its children. // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. From 4109228af1b3d151b01aeb6f11a7b83491599682 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 25 May 2023 10:08:31 -0700 Subject: [PATCH 101/336] fix debugging with new pytest run script (#21299) fix debugging for run_pytest_script.py setup --- .../common/process/internal/scripts/index.ts | 7 +++++ src/client/testing/common/debugLauncher.ts | 13 +++++----- src/client/testing/common/types.ts | 1 + .../pytest/pytestExecutionAdapter.ts | 26 +++++++++++-------- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts index 7240d7be67f8..f719844ef80b 100644 --- a/src/client/common/process/internal/scripts/index.ts +++ b/src/client/common/process/internal/scripts/index.ts @@ -110,6 +110,13 @@ export function testlauncher(testArgs: string[]): string[] { return [script, ...testArgs]; } +// run_pytest_script.py +export function pytestlauncher(testArgs: string[]): string[] { + const script = path.join(SCRIPTS_DIR, 'vscode_pytest', 'run_pytest_script.py'); + // There is no output to parse, so we do not return a function. + return [script, ...testArgs]; +} + // visualstudio_py_testlauncher.py // eslint-disable-next-line camelcase diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 5b39bd97a740..af233132768c 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -178,12 +178,7 @@ export class DebugLauncher implements ITestDebugLauncher { const args = script(testArgs); const [program] = args; configArgs.program = program; - // if the test provider is pytest, then use the pytest module instead of using a program - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; - if (options.testProvider === 'pytest' && rewriteTestingEnabled) { - configArgs.module = 'pytest'; - configArgs.program = undefined; - } + configArgs.args = args.slice(1); // We leave configArgs.request as "test" so it will be sent in telemetry. @@ -204,12 +199,16 @@ export class DebugLauncher implements ITestDebugLauncher { throw Error(`Invalid debug config "${debugConfig.name}"`); } launchArgs.request = 'launch'; + + // If we are in the pytest rewrite then we must send additional environment variables. + const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; if (options.testProvider === 'pytest' && rewriteTestingEnabled) { if (options.pytestPort && options.pytestUUID) { launchArgs.env = { ...launchArgs.env, TEST_PORT: options.pytestPort, TEST_UUID: options.pytestUUID, + RUN_TEST_IDS_PORT: options.pytestRunTestIdsPort, }; } else { throw Error( @@ -238,7 +237,7 @@ export class DebugLauncher implements ITestDebugLauncher { } case 'pytest': { if (rewriteTestingEnabled) { - return (testArgs: string[]) => testArgs; + return internalScripts.pytestlauncher; // this is the new way to run pytest execution, debugger } return internalScripts.testlauncher; // old way pytest execution, debugger } diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index a5d263058525..443a2c2ef20e 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -27,6 +27,7 @@ export type LaunchOptions = { outChannel?: OutputChannel; pytestPort?: string; pytestUUID?: string; + pytestRunTestIdsPort?: string; }; export type ParserOptions = TestDiscoveryOptions; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 4c6dcd9cdbee..39b7f8c787b9 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import * as net from 'net'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; -import { traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, @@ -112,18 +112,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testArgs.splice(0, 0, '--rootdir', uri.fsPath); } - // why is this needed? if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } - const pluginArgs = ['-p', 'vscode_pytest'].concat(testArgs).concat(testIds); - const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); - const runArgs = [scriptPath, ...testArgs]; + // create payload with testIds to send to run pytest script const testData = JSON.stringify(testIds); const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + let pytestRunTestIdsPort: string | undefined; const startServer = (): Promise => new Promise((resolve, reject) => { const server = net.createServer((socket: net.Socket) => { @@ -151,11 +149,12 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { await startServer() .then((assignedPort) => { traceLog(`Server started and listening on port ${assignedPort}`); + pytestRunTestIdsPort = assignedPort.toString(); if (spawnOptions.extraVariables) - spawnOptions.extraVariables.RUN_TEST_IDS_PORT = assignedPort.toString(); + spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort; }) .catch((error) => { - console.error('Error starting server:', error); + traceError('Error starting server:', error); }); if (debugBool) { @@ -163,22 +162,27 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const pytestUUID = uuid.toString(); const launchOptions: LaunchOptions = { cwd: uri.fsPath, - args: pluginArgs, + args: testArgs, token: spawnOptions.token, testProvider: PYTEST_PROVIDER, pytestPort, pytestUUID, + pytestRunTestIdsPort, }; - console.debug(`Running debug test with arguments: ${pluginArgs.join(' ')}\r\n`); + traceVerbose(`Running debug test with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions); } else { + // combine path to run script with run args + const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); + const runArgs = [scriptPath, ...testArgs]; + await execService?.exec(runArgs, spawnOptions).catch((ex) => { - console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); + traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); }); } } catch (ex) { - console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); + traceVerbose(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); } From 72f7ef8113536d35fa000c8a75f7aae598575990 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 25 May 2023 13:41:55 -0700 Subject: [PATCH 102/336] Set up testing rewrite experiment (#21258) is the beginning of this issue: https://github.com/microsoft/vscode-python/issues/21150, in that it will start the process of implementing the setting in the extension --- src/client/common/experiments/groups.ts | 4 +++ src/client/testing/common/debugLauncher.ts | 17 +++++----- .../testing/testController/common/utils.ts | 10 ++++++ .../testing/testController/controller.ts | 21 ++++++------ .../testController/pytest/pytestController.ts | 31 +++++++++-------- .../unittest/unittestController.ts | 33 +++++++++++-------- .../testing/common/debugLauncher.unit.test.ts | 29 +++++++++------- 7 files changed, 88 insertions(+), 57 deletions(-) diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts index 5884aafd122d..1ee06469095c 100644 --- a/src/client/common/experiments/groups.ts +++ b/src/client/common/experiments/groups.ts @@ -14,3 +14,7 @@ export enum TerminalEnvVarActivation { export enum ShowFormatterExtensionPrompt { experiment = 'pythonPromptNewFormatterExt', } +// Experiment to enable the new testing rewrite. +export enum EnableTestAdapterRewrite { + experiment = 'pythonTestAdapter', +} diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index af233132768c..0c13671b6e0d 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -16,6 +16,7 @@ import { getConfigurationsForWorkspace } from '../../debugger/extension/configur import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; import { showErrorMessage } from '../../common/vscodeApis/windowApis'; import { createDeferred } from '../../common/utils/async'; +import { pythonTestAdapterRewriteEnabled } from '../testController/common/utils'; @injectable() export class DebugLauncher implements ITestDebugLauncher { @@ -87,6 +88,7 @@ export class DebugLauncher implements ITestDebugLauncher { path: path.join(EXTENSION_ROOT_DIR, 'pythonFiles'), include: false, }); + DebugLauncher.applyDefaults(debugConfig!, workspaceFolder, configSettings); return this.convertConfigToArgs(debugConfig!, workspaceFolder, options); @@ -171,10 +173,11 @@ export class DebugLauncher implements ITestDebugLauncher { workspaceFolder: WorkspaceFolder, options: LaunchOptions, ): Promise { + const pythonTestAdapterRewriteExperiment = pythonTestAdapterRewriteEnabled(this.serviceContainer); const configArgs = debugConfig as LaunchRequestArguments; const testArgs = options.testProvider === 'unittest' ? options.args.filter((item) => item !== '--debug') : options.args; - const script = DebugLauncher.getTestLauncherScript(options.testProvider); + const script = DebugLauncher.getTestLauncherScript(options.testProvider, pythonTestAdapterRewriteExperiment); const args = script(testArgs); const [program] = args; configArgs.program = program; @@ -199,10 +202,7 @@ export class DebugLauncher implements ITestDebugLauncher { throw Error(`Invalid debug config "${debugConfig.name}"`); } launchArgs.request = 'launch'; - - // If we are in the pytest rewrite then we must send additional environment variables. - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; - if (options.testProvider === 'pytest' && rewriteTestingEnabled) { + if (options.testProvider === 'pytest' && pythonTestAdapterRewriteExperiment) { if (options.pytestPort && options.pytestUUID) { launchArgs.env = { ...launchArgs.env, @@ -226,17 +226,16 @@ export class DebugLauncher implements ITestDebugLauncher { return launchArgs; } - private static getTestLauncherScript(testProvider: TestProvider) { - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; + private static getTestLauncherScript(testProvider: TestProvider, pythonTestAdapterRewriteExperiment?: boolean) { switch (testProvider) { case 'unittest': { - if (rewriteTestingEnabled) { + if (pythonTestAdapterRewriteExperiment) { return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger } return internalScripts.visualstudio_py_testlauncher; // old way unittest execution, debugger } case 'pytest': { - if (rewriteTestingEnabled) { + if (pythonTestAdapterRewriteExperiment) { return internalScripts.pytestlauncher; // this is the new way to run pytest execution, debugger } return internalScripts.testlauncher; // old way pytest execution, debugger diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 88e3450d35dc..1bf31e80e11a 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -3,6 +3,10 @@ import * as net from 'net'; import { traceLog } from '../../../logging'; +import { EnableTestAdapterRewrite } from '../../../common/experiments/groups'; +import { IExperimentService } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; + export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}\r\n`; @@ -52,6 +56,12 @@ export function jsonRPCContent(headers: Map, rawData: string): I remainingRawData, }; } + +export function pythonTestAdapterRewriteEnabled(serviceContainer: IServiceContainer): boolean { + const experiment = serviceContainer.get(IExperimentService); + return experiment.inExperimentSync(EnableTestAdapterRewrite.experiment); +} + export const startServer = (testIds: string): Promise => new Promise((resolve, reject) => { const server = net.createServer((socket: net.Socket) => { diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 20fcfa49bb69..6c6b9f409e5f 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -44,6 +44,8 @@ import { PytestTestDiscoveryAdapter } from './pytest/pytestDiscoveryAdapter'; import { PytestTestExecutionAdapter } from './pytest/pytestExecutionAdapter'; import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { ITestDebugLauncher } from '../common/types'; +import { pythonTestAdapterRewriteEnabled } from './common/utils'; +import { IServiceContainer } from '../../ioc/types'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; @@ -93,6 +95,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, @inject(ITestOutputChannel) private readonly testOutputChannel: ITestOutputChannel, + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, ) { this.refreshCancellation = new CancellationTokenSource(); @@ -241,12 +244,11 @@ export class PythonTestController implements ITestController, IExtensionSingleAc if (uri) { const settings = this.configSettings.getSettings(uri); traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; if (settings.testing.pytestEnabled) { // Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; - if (rewriteTestingEnabled) { - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + // ** experiment to roll out NEW test discovery mechanism + if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { const workspace = this.workspaceService.getWorkspaceFolder(uri); traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); const testAdapter = @@ -263,8 +265,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc } else if (settings.testing.unittestEnabled) { // ** Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; - if (rewriteTestingEnabled) { - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + // ** experiment to roll out NEW test discovery mechanism + if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { const workspace = this.workspaceService.getWorkspaceFolder(uri); traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); const testAdapter = @@ -388,14 +390,13 @@ export class PythonTestController implements ITestController, IExtensionSingleAc const settings = this.configSettings.getSettings(workspace.uri); if (testItems.length > 0) { - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; if (settings.testing.pytestEnabled) { sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { tool: 'pytest', debugging: request.profile?.kind === TestRunProfileKind.Debug, }); - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism - if (rewriteTestingEnabled) { + // ** experiment to roll out NEW test discovery mechanism + if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { const testAdapter = this.testAdapters.get(workspace.uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); @@ -425,8 +426,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc tool: 'unittest', debugging: request.profile?.kind === TestRunProfileKind.Debug, }); - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism - if (rewriteTestingEnabled) { + // ** experiment to roll out NEW test discovery mechanism + if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { const testAdapter = this.testAdapters.get(workspace.uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); diff --git a/src/client/testing/testController/pytest/pytestController.ts b/src/client/testing/testController/pytest/pytestController.ts index 793170231210..997e3e29b7ec 100644 --- a/src/client/testing/testController/pytest/pytestController.ts +++ b/src/client/testing/testController/pytest/pytestController.ts @@ -285,19 +285,24 @@ export class PytestController implements ITestFrameworkController { public runTests(testRun: ITestRun, workspace: WorkspaceFolder, token: CancellationToken): Promise { const settings = this.configService.getSettings(workspace.uri); - return this.runner.runTests( - testRun, - { - workspaceFolder: workspace.uri, - cwd: - settings.testing.cwd && settings.testing.cwd.length > 0 - ? settings.testing.cwd - : workspace.uri.fsPath, - token, - args: settings.testing.pytestArgs, - }, - this.idToRawData, - ); + try { + return this.runner.runTests( + testRun, + { + workspaceFolder: workspace.uri, + cwd: + settings.testing.cwd && settings.testing.cwd.length > 0 + ? settings.testing.cwd + : workspace.uri.fsPath, + token, + args: settings.testing.pytestArgs, + }, + this.idToRawData, + ); + } catch (ex) { + sendTelemetryEvent(EventName.UNITTEST_RUN_ALL_FAILED, undefined); + throw new Error(`Failed to run tests: ${ex}`); + } } } diff --git a/src/client/testing/testController/unittest/unittestController.ts b/src/client/testing/testController/unittest/unittestController.ts index ee79103c4e3e..a795620f3ca0 100644 --- a/src/client/testing/testController/unittest/unittestController.ts +++ b/src/client/testing/testController/unittest/unittestController.ts @@ -251,20 +251,25 @@ export class UnittestController implements ITestFrameworkController { testController?: TestController, ): Promise { const settings = this.configService.getSettings(workspace.uri); - return this.runner.runTests( - testRun, - { - workspaceFolder: workspace.uri, - cwd: - settings.testing.cwd && settings.testing.cwd.length > 0 - ? settings.testing.cwd - : workspace.uri.fsPath, - token, - args: settings.testing.unittestArgs, - }, - this.idToRawData, - testController, - ); + try { + return this.runner.runTests( + testRun, + { + workspaceFolder: workspace.uri, + cwd: + settings.testing.cwd && settings.testing.cwd.length > 0 + ? settings.testing.cwd + : workspace.uri.fsPath, + token, + args: settings.testing.unittestArgs, + }, + this.idToRawData, + testController, + ); + } catch (ex) { + sendTelemetryEvent(EventName.UNITTEST_RUN_ALL_FAILED, undefined); + throw new Error(`Failed to run tests: ${ex}`); + } } } diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index b8b7d5c55130..41b95c66040e 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -29,6 +29,7 @@ import { ITestingSettings } from '../../../client/testing/configuration/types'; import { TestProvider } from '../../../client/testing/types'; import { isOs, OSType } from '../../common'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import * as util from '../../../client/testing/testController/common/utils'; use(chaiAsPromised); @@ -45,6 +46,7 @@ suite('Unit Tests - Debug Launcher', () => { let getWorkspaceFoldersStub: sinon.SinonStub; let pathExistsStub: sinon.SinonStub; let readFileStub: sinon.SinonStub; + let pythonTestAdapterRewriteEnabledStub: sinon.SinonStub; const envVars = { FOO: 'BAR' }; setup(async () => { @@ -65,6 +67,8 @@ suite('Unit Tests - Debug Launcher', () => { getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); pathExistsStub = sinon.stub(fs, 'pathExists'); readFileStub = sinon.stub(fs, 'readFile'); + pythonTestAdapterRewriteEnabledStub = sinon.stub(util, 'pythonTestAdapterRewriteEnabled'); + pythonTestAdapterRewriteEnabledStub.returns(false); const appShell = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); @@ -143,19 +147,22 @@ suite('Unit Tests - Debug Launcher', () => { uri: Uri.file(folderPath), }; } - function getTestLauncherScript(testProvider: TestProvider) { - switch (testProvider) { - case 'unittest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'visualstudio_py_testlauncher.py'); - } - case 'pytest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testlauncher.py'); - } - default: { - throw new Error(`Unknown test provider '${testProvider}'`); + function getTestLauncherScript(testProvider: TestProvider, pythonTestAdapterRewriteExperiment?: boolean) { + if (!pythonTestAdapterRewriteExperiment) { + switch (testProvider) { + case 'unittest': { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'visualstudio_py_testlauncher.py'); + } + case 'pytest': { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testlauncher.py'); + } + default: { + throw new Error(`Unknown test provider '${testProvider}'`); + } } } } + function getDefaultDebugConfig(): DebugConfiguration { return { name: 'Debug Unit Test', @@ -178,7 +185,7 @@ suite('Unit Tests - Debug Launcher', () => { expected?: DebugConfiguration, debugConfigs?: string | DebugConfiguration[], ) { - const testLaunchScript = getTestLauncherScript(testProvider); + const testLaunchScript = getTestLauncherScript(testProvider, false); const workspaceFolders = [createWorkspaceFolder(options.cwd), createWorkspaceFolder('five/six/seven')]; getWorkspaceFoldersStub.returns(workspaceFolders); From c2134917896b009ac678dac9c712aba813b55e55 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 26 May 2023 09:26:55 -0700 Subject: [PATCH 103/336] Apply environment variables after shell initialization scripts are run in `pythonTerminalEnvVarActivation` experiment (#21290) For #11039 https://github.com/microsoft/vscode-python/issues/20822 Closes https://github.com/microsoft/vscode-python/issues/21297 Update proposed APIs to be used in Terminal activation experiment. --- package-lock.json | 2 +- package.json | 5 +- .../terminalEnvVarCollectionService.ts | 38 +++++++---- ...rminalEnvVarCollectionService.unit.test.ts | 66 ++++++++++--------- .../vscode.proposed.envCollectionOptions.d.ts | 56 ++++++++++++++++ ...scode.proposed.envCollectionWorkspace.d.ts | 44 ++++++------- 6 files changed, 141 insertions(+), 70 deletions(-) create mode 100644 types/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts diff --git a/package-lock.json b/package-lock.json index bfbd316f269d..a6bdd1be4c1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.78.0-20230421" + "vscode": "^1.79.0-20230525" } }, "node_modules/@azure/abort-controller": { diff --git a/package.json b/package.json index a92875a230d4..7918881bd115 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "testObserver", "quickPickItemTooltip", "envCollectionWorkspace", - "saveEditor" + "saveEditor", + "envCollectionOptions" ], "author": { "name": "Microsoft Corporation" @@ -45,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.78.0-20230421" + "vscode": "^1.79.0-20230526" }, "keywords": [ "python", diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 26852303d099..dbf77e72379a 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -3,7 +3,14 @@ import * as path from 'path'; import { inject, injectable } from 'inversify'; -import { ProgressOptions, ProgressLocation, MarkdownString, WorkspaceFolder } from 'vscode'; +import { + ProgressOptions, + ProgressLocation, + MarkdownString, + WorkspaceFolder, + EnvironmentVariableCollection, + EnvironmentVariableScope, +} from 'vscode'; import { pathExists } from 'fs-extra'; import { IExtensionActivationService } from '../../activation/types'; import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; @@ -108,6 +115,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ undefined, shell, ); + const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); if (!env) { const shellType = identifyShellFromShellPath(shell); const defaultShell = defaultShells[this.platform.osType]; @@ -117,7 +125,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ await this._applyCollection(resource, defaultShell?.shell); return; } - this.context.environmentVariableCollection.clear({ workspaceFolder }); + envVarCollection.clear(); this.previousEnvVars = _normCaseKeys(process.env); return; } @@ -129,10 +137,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (prevValue !== value) { if (value !== undefined) { traceVerbose(`Setting environment variable ${key} in collection to ${value}`); - this.context.environmentVariableCollection.replace(key, value, { workspaceFolder }); + envVarCollection.replace(key, value, { applyAtShellIntegration: true }); } else { traceVerbose(`Clearing environment variable ${key} from collection`); - this.context.environmentVariableCollection.delete(key, { workspaceFolder }); + envVarCollection.delete(key); } } }); @@ -140,14 +148,21 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // If the previous env var is not in the current env, clear it from collection. if (!(key in env)) { traceVerbose(`Clearing environment variable ${key} from collection`); - this.context.environmentVariableCollection.delete(key, { workspaceFolder }); + envVarCollection.delete(key); } }); const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); - this.context.environmentVariableCollection.setDescription(description, { - workspaceFolder, - }); + envVarCollection.description = description; + } + + private getEnvironmentVariableCollection(workspaceFolder?: WorkspaceFolder) { + const envVarCollection = this.context.environmentVariableCollection as EnvironmentVariableCollection & { + getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; + }; + return workspaceFolder + ? envVarCollection.getScopedEnvironmentVariableCollection({ workspaceFolder }) + : envVarCollection; } private async handleMicroVenv(resource: Resource) { @@ -156,12 +171,11 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (interpreter?.envType === EnvironmentType.Venv) { const activatePath = path.join(path.dirname(interpreter.path), 'activate'); if (!(await pathExists(activatePath))) { - this.context.environmentVariableCollection.replace( + const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); + envVarCollection.replace( 'PATH', `${path.dirname(interpreter.path)}${path.delimiter}${process.env.Path}`, - { - workspaceFolder, - }, + { applyAtShellIntegration: true }, ); return; } diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index feecf63f5577..cb0b6b02f288 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -7,7 +7,13 @@ import * as sinon from 'sinon'; import { assert, expect } from 'chai'; import { cloneDeep } from 'lodash'; import { mock, instance, when, anything, verify, reset } from 'ts-mockito'; -import { EnvironmentVariableCollection, ProgressLocation, Uri, WorkspaceFolder } from 'vscode'; +import { + EnvironmentVariableCollection, + EnvironmentVariableScope, + ProgressLocation, + Uri, + WorkspaceFolder, +} from 'vscode'; import { IApplicationShell, IApplicationEnvironment, @@ -39,7 +45,10 @@ suite('Terminal Environment Variable Collection Service', () => { let context: IExtensionContext; let shell: IApplicationShell; let experimentService: IExperimentService; - let collection: EnvironmentVariableCollection; + let collection: EnvironmentVariableCollection & { + getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; + }; + let scopedCollection: EnvironmentVariableCollection; let applicationEnvironment: IApplicationEnvironment; let environmentActivationService: IEnvironmentActivationService; let workspaceService: IWorkspaceService; @@ -62,7 +71,13 @@ suite('Terminal Environment Variable Collection Service', () => { interpreterService = mock(); context = mock(); shell = mock(); - collection = mock(); + collection = mock< + EnvironmentVariableCollection & { + getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; + } + >(); + scopedCollection = mock(); + when(collection.getScopedEnvironmentVariableCollection(anything())).thenReturn(instance(scopedCollection)); when(context.environmentVariableCollection).thenReturn(instance(collection)); experimentService = mock(); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); @@ -166,12 +181,12 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete('PATH', anything())).once(); + verify(collection.delete('PATH')).once(); }); test('Verify envs are not applied if env activation is disabled', async () => { @@ -187,7 +202,7 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); reset(configService); when(configService.getSettings(anything())).thenReturn(({ terminal: { activateEnvironment: false }, @@ -197,10 +212,10 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).never(); - verify(collection.delete('PATH', anything())).never(); + verify(collection.delete('PATH')).never(); }); - test('Verify correct scope is used when applying envs and setting description', async () => { + test('Verify correct options are used when applying envs and setting description', async () => { const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; delete envVars.PATH; const resource = Uri.file('a'); @@ -214,25 +229,16 @@ suite('Terminal Environment Variable Collection Service', () => { environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, customShell), ).thenResolve(envVars); - when(collection.replace(anything(), anything(), anything())).thenCall((_e, _v, scope) => { - assert.deepEqual(scope, { workspaceFolder }); - return Promise.resolve(); - }); - when(collection.delete(anything(), anything())).thenCall((_e, scope) => { - assert.deepEqual(scope, { workspaceFolder }); + when(scopedCollection.replace(anything(), anything(), anything())).thenCall((_e, _v, options) => { + assert.deepEqual(options, { applyAtShellIntegration: true }); return Promise.resolve(); }); - let description = ''; - when(collection.setDescription(anything(), anything())).thenCall((d, scope) => { - assert.deepEqual(scope, { workspaceFolder }); - description = d.value; - }); + when(scopedCollection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(resource, customShell); - verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete('PATH', anything())).once(); - expect(description).to.equal(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); + verify(scopedCollection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + verify(scopedCollection.delete('PATH')).once(); }); test('Only relative changes to previously applied variables are applied to the collection', async () => { @@ -251,7 +257,7 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); @@ -270,8 +276,8 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); - verify(collection.delete('CONDA_PREFIX', anything())).once(); - verify(collection.delete('RANDOM_VAR', anything())).once(); + verify(collection.delete('CONDA_PREFIX')).once(); + verify(collection.delete('RANDOM_VAR')).once(); }); test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { @@ -294,12 +300,12 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete(anything(), anything())).never(); + verify(collection.delete(anything())).never(); }); test('If no activated variables are returned for default shell, clear collection', async () => { @@ -313,12 +319,10 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(undefined); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); - when(collection.setDescription(anything(), anything())).thenReturn(); + when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, defaultShell?.shell); - verify(collection.clear(anything())).once(); - verify(collection.setDescription(anything(), anything())).never(); + verify(collection.clear()).once(); }); }); diff --git a/types/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts b/types/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts new file mode 100644 index 000000000000..d25a92725a4d --- /dev/null +++ b/types/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/179476 + + /** + * Options applied to the mutator. + */ + export interface EnvironmentVariableMutatorOptions { + /** + * Apply to the environment just before the process is created. + * + * Defaults to true. + */ + applyAtProcessCreation?: boolean; + + /** + * Apply to the environment in the shell integration script. Note that this _will not_ apply + * the mutator if shell integration is disabled or not working for some reason. + * + * Defaults to false. + */ + applyAtShellIntegration?: boolean; + } + + /** + * A type of mutation and its value to be applied to an environment variable. + */ + export interface EnvironmentVariableMutator { + /** + * Options applied to the mutator. + */ + readonly options: EnvironmentVariableMutatorOptions; + } + + export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { + /** + * @param options Options applied to the mutator. + */ + replace(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + + /** + * @param options Options applied to the mutator. + */ + append(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + + /** + * @param options Options applied to the mutator. + */ + prepend(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + } +} diff --git a/types/vscode.proposed.envCollectionWorkspace.d.ts b/types/vscode.proposed.envCollectionWorkspace.d.ts index b1176a9d46c2..d778e53e5086 100644 --- a/types/vscode.proposed.envCollectionWorkspace.d.ts +++ b/types/vscode.proposed.envCollectionWorkspace.d.ts @@ -5,34 +5,30 @@ declare module 'vscode' { - // https://github.com/microsoft/vscode/issues/171173 + // https://github.com/microsoft/vscode/issues/182069 - export interface EnvironmentVariableMutator { - readonly type: EnvironmentVariableMutatorType; - readonly value: string; - readonly scope: EnvironmentVariableScope | undefined; - } - - export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { - /** - * Sets a description for the environment variable collection, this will be used to describe the changes in the UI. - * @param description A description for the environment variable collection. - * @param scope Specific scope to which this description applies to. - */ - setDescription(description: string | MarkdownString | undefined, scope?: EnvironmentVariableScope): void; - replace(variable: string, value: string, scope?: EnvironmentVariableScope): void; - append(variable: string, value: string, scope?: EnvironmentVariableScope): void; - prepend(variable: string, value: string, scope?: EnvironmentVariableScope): void; - get(variable: string, scope?: EnvironmentVariableScope): EnvironmentVariableMutator | undefined; - delete(variable: string, scope?: EnvironmentVariableScope): void; - clear(scope?: EnvironmentVariableScope): void; - - } + // export interface ExtensionContext { + // /** + // * Gets the extension's environment variable collection for this workspace, enabling changes + // * to be applied to terminal environment variables. + // * + // * @param scope The scope to which the environment variable collection applies to. + // */ + // readonly environmentVariableCollection: EnvironmentVariableCollection & { getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection }; + // } export type EnvironmentVariableScope = { /** - * The workspace folder to which this collection applies to. If unspecified, collection applies to all workspace folders. - */ + * Any specific workspace folder to get collection for. If unspecified, collection applicable to all workspace folders is returned. + */ workspaceFolder?: WorkspaceFolder; }; + + export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { + /** + * A description for the environment variable collection, this will be used to describe the + * changes in the UI. + */ + description: string | MarkdownString | undefined; + } } From f148139f65e455fcc911cd5a3fdaba15b4bdb776 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 26 May 2023 11:43:46 -0700 Subject: [PATCH 104/336] allow pytest tests to handle multiple payloads (#21301) As part of the switch to allow for dynamic run- the pytest discovery and execution tests are now switched to be take lists of dicts where the dicts are the payloads. --- pythonFiles/tests/pytestadapter/helpers.py | 29 +++++++---- .../tests/pytestadapter/test_discovery.py | 44 +++++++++------- .../tests/pytestadapter/test_execution.py | 52 +++++++++++-------- 3 files changed, 73 insertions(+), 52 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index e84c46037c2f..013e4bb31fca 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -1,18 +1,15 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - -import contextlib import io import json import os import pathlib -import random import socket import subprocess import sys import threading import uuid -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Tuple TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict @@ -70,31 +67,29 @@ def _new_sock() -> socket.socket: ) -def process_rpc_json(data: str) -> Dict[str, Any]: +def process_rpc_message(data: str) -> Tuple[Dict[str, Any], str]: """Process the JSON data which comes from the server which runs the pytest discovery.""" str_stream: io.StringIO = io.StringIO(data) length: int = 0 - while True: line: str = str_stream.readline() if CONTENT_LENGTH.lower() in line.lower(): length = int(line[len(CONTENT_LENGTH) :]) break - if not line or line.isspace(): raise ValueError("Header does not contain Content-Length") - while True: line: str = str_stream.readline() if not line or line.isspace(): break raw_json: str = str_stream.read(length) - return json.loads(raw_json) + dict_json: Dict[str, Any] = json.loads(raw_json) + return dict_json, str_stream.read() -def runner(args: List[str]) -> Optional[Dict[str, Any]]: +def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: """Run the pytest discovery and return the JSON data from the server.""" process_args: List[str] = [ sys.executable, @@ -133,7 +128,19 @@ def runner(args: List[str]) -> Optional[Dict[str, Any]]: t1.join() t2.join() - return process_rpc_json(result[0]) if result else None + a = process_rpc_json(result[0]) + return a if result else None + + +def process_rpc_json(data: str) -> List[Dict[str, Any]]: + """Process the JSON data which comes from the server which runs the pytest discovery.""" + json_messages = [] + remaining = data + while remaining: + json_data, remaining = process_rpc_message(remaining) + json_messages.append(json_data) + + return json_messages def _listen_on_socket(listener: socket.socket, result: List[str]): diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index bb6e7255704e..ab7abb508153 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import os import shutil +from typing import Any, Dict, List, Optional import pytest @@ -28,12 +29,15 @@ def test_syntax_error(tmp_path): temp_dir.mkdir() p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) - actual = runner(["--collect-only", os.fspath(p)]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + actual_list: Optional[List[Dict[str, Any]]] = runner( + ["--collect-only", os.fspath(p)] + ) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 2 def test_parameterized_error_collect(): @@ -42,12 +46,15 @@ def test_parameterized_error_collect(): The json should still be returned but the errors list should be present. """ file_path_str = "error_parametrize_discovery.py" - actual = runner(["--collect-only", file_path_str]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + actual_list: Optional[List[Dict[str, Any]]] = runner( + ["--collect-only", file_path_str] + ) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 2 @pytest.mark.parametrize( @@ -98,14 +105,15 @@ def test_pytest_collect(file, expected_const): file -- a string with the file or folder to run pytest discovery on. expected_const -- the expected output from running pytest discovery on the file. """ - actual = runner( + actual_list: Optional[List[Dict[str, Any]]] = runner( [ "--collect-only", os.fspath(TEST_DATA_PATH / file), ] ) - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert actual["tests"] == expected_const + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "tests")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert actual["tests"] == expected_const diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 8613deb96098..d54e4e758d35 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import os import shutil +from typing import Any, Dict, List, Optional import pytest from tests.pytestadapter import expected_execution_test_output @@ -13,7 +14,7 @@ def test_syntax_error_execution(tmp_path): """Test pytest execution on a file that has a syntax error. Copies the contents of a .txt file to a .py file in the temporary directory - to then run pytest exeuction on. + to then run pytest execution on. The json should still be returned but the errors list should be present. @@ -28,12 +29,15 @@ def test_syntax_error_execution(tmp_path): temp_dir.mkdir() p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) - actual = runner(["error_syntax_discover.py::test_function"]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + actual_list: Optional[List[Dict[str, Any]]] = runner( + ["error_syntax_discover.py::test_function"] + ) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 1 def test_bad_id_error_execution(): @@ -41,12 +45,13 @@ def test_bad_id_error_execution(): The json should still be returned but the errors list should be present. """ - actual = runner(["not/a/real::test_id"]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + actual_list: Optional[List[Dict[str, Any]]] = runner(["not/a/real::test_id"]) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 1 @pytest.mark.parametrize( @@ -153,13 +158,14 @@ def test_pytest_execution(test_ids, expected_const): expected_const -- a dictionary of the expected output from running pytest discovery on the files. """ args = test_ids - actual = runner(args) - assert actual - assert all(item in actual for item in ("status", "cwd", "result")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - result_data = actual["result"] - for key in result_data: - if result_data[key]["outcome"] == "failure": - result_data[key]["message"] = "ERROR MESSAGE" - assert result_data == expected_const + actual_list: Optional[List[Dict[str, Any]]] = runner(args) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "result")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + result_data = actual["result"] + for key in result_data: + if result_data[key]["outcome"] == "failure": + result_data[key]["message"] = "ERROR MESSAGE" + assert result_data == expected_const From e9a8dd52341a1a0f2beee7a43d0ccf44793e6a1f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 31 May 2023 09:08:07 -0700 Subject: [PATCH 105/336] remove duplicates from test_ids array (#21347) this will partially remediate https://github.com/microsoft/vscode-python/issues/21339 in regards to the duplicate IDs being run. --- src/client/testing/testController/workspaceTestAdapter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index ceb6a0b60afb..0edac06c2b5a 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -83,7 +83,7 @@ export class WorkspaceTestAdapter { let rawTestExecData; const testCaseNodes: TestItem[] = []; - const testCaseIds: string[] = []; + const testCaseIdsSet = new Set(); try { // first fetch all the individual test Items that we necessarily want includes.forEach((t) => { @@ -95,10 +95,10 @@ export class WorkspaceTestAdapter { runInstance.started(node); // do the vscode ui test item start here before runtest const runId = this.vsIdToRunId.get(node.id); if (runId) { - testCaseIds.push(runId); + testCaseIdsSet.add(runId); } }); - + const testCaseIds = Array.from(testCaseIdsSet); // ** execution factory only defined for new rewrite way if (executionFactory !== undefined) { rawTestExecData = await this.executionAdapter.runTests( From d968b8c472208f4635f75af649b58a9be027400d Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 31 May 2023 15:14:14 -0700 Subject: [PATCH 106/336] Dont show command for button trigger in command pallet (#21350) Fixes https://github.com/microsoft/vscode-python/issues/21322 --- package.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7918881bd115..b39392c8fa51 100644 --- a/package.json +++ b/package.json @@ -1600,6 +1600,12 @@ "title": "%python.command.python.createEnvironment.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, + { + "category": "Python", + "command": "python.createEnvironment-button", + "title": "%python.command.python.createEnvironment.title%", + "when": "false" + }, { "category": "Python", "command": "python.createTerminal", @@ -1629,14 +1635,14 @@ "command": "python.execInTerminal-icon", "icon": "$(play)", "title": "%python.command.python.execInTerminalIcon.title%", - "when": "false && editorLangId == python" + "when": "false" }, { "category": "Python", "command": "python.execInDedicatedTerminal", "icon": "$(play)", "title": "%python.command.python.execInDedicatedTerminal.title%", - "when": "false && editorLangId == python" + "when": "false" }, { "category": "Python", From dbd0b73b9605eb96d0da295a8ae95462f88e9915 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 31 May 2023 15:36:44 -0700 Subject: [PATCH 107/336] adding extra log messages for rewrite debugging (#21352) These logs print errors and other bits of information which will be helpful for debugging workflows of users where we need to get information such as args or which step in the process they got to. --- .../testing/testController/common/server.ts | 20 ++++++++++++----- .../testing/testController/controller.ts | 21 +++++++----------- .../pytest/pytestDiscoveryAdapter.ts | 13 ++++++----- .../pytest/pytestExecutionAdapter.ts | 22 ++++++++++--------- .../unittest/testDiscoveryAdapter.ts | 2 ++ .../testController/workspaceTestAdapter.ts | 3 ++- 6 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index a00623aa33c7..62c14d451fc2 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -9,7 +9,7 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; @@ -36,12 +36,12 @@ export class PythonTestServer implements ITestServer, Disposable { const uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); const totalContentLength = rpcHeaders.headers.get('Content-Length'); if (!uuid) { - traceLog('On data received: Error occurred because payload UUID is undefined'); + traceError('On data received: Error occurred because payload UUID is undefined'); this._onDataReceived.fire({ uuid: '', data: '' }); return; } if (!this.uuids.includes(uuid)) { - traceLog('On data received: Error occurred because the payload UUID is not recognized'); + traceError('On data received: Error occurred because the payload UUID is not recognized'); this._onDataReceived.fire({ uuid: '', data: '' }); return; } @@ -50,6 +50,7 @@ export class PythonTestServer implements ITestServer, Disposable { const extractedData = rpcContent.extractedJSON; if (extractedData.length === Number(totalContentLength)) { // do not send until we have the full content + traceVerbose(`Received data from test server: ${extractedData}`); this._onDataReceived.fire({ uuid, data: extractedData }); this.uuids = this.uuids.filter((u) => u !== uuid); buffer = Buffer.alloc(0); @@ -58,7 +59,7 @@ export class PythonTestServer implements ITestServer, Disposable { } } } catch (ex) { - traceLog(`Error processing test server request: ${ex} observe`); + traceError(`Error processing test server request: ${ex} observe`); this._onDataReceived.fire({ uuid: '', data: '' }); } }); @@ -114,6 +115,8 @@ export class PythonTestServer implements ITestServer, Disposable { outputChannel: options.outChannel, }; + const isRun = !options.testIds; + // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, @@ -154,9 +157,16 @@ export class PythonTestServer implements ITestServer, Disposable { token: options.token, testProvider: UNITTEST_PROVIDER, }; - + traceInfo(`Running DEBUG unittest with arguments: ${args}\r\n`); await this.debugLauncher!.launchDebugger(launchOptions); } else { + if (isRun) { + // This means it is running the test + traceInfo(`Running unittests with arguments: ${args}\r\n`); + } else { + // This means it is running discovery + traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); + } await execService.exec(args, spawnOptions); } } catch (ex) { diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 6c6b9f409e5f..0d3487855380 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -24,7 +24,7 @@ import { IConfigurationService, IDisposableRegistry, ITestOutputChannel, Resourc import { DelayedTrigger, IDelayedTrigger } from '../../common/utils/delayTrigger'; import { noop } from '../../common/utils/misc'; import { IInterpreterService } from '../../interpreter/contracts'; -import { traceError, traceVerbose } from '../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; import { IEventNamePropertyMapping, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; @@ -243,14 +243,14 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.refreshingStartedEvent.fire(); if (uri) { const settings = this.configSettings.getSettings(uri); - traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); + const workspace = this.workspaceService.getWorkspaceFolder(uri); + traceInfo(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + // Ensure we send test telemetry if it gets disabled again + this.sendTestDisabledTelemetry = true; + // ** experiment to roll out NEW test discovery mechanism if (settings.testing.pytestEnabled) { - // Ensure we send test telemetry if it gets disabled again - this.sendTestDisabledTelemetry = true; - // ** experiment to roll out NEW test discovery mechanism if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { - const workspace = this.workspaceService.getWorkspaceFolder(uri); - traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + traceInfo(`Running discovery for pytest using the new test adapter.`); const testAdapter = this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); testAdapter.discoverTests( @@ -263,12 +263,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); } } else if (settings.testing.unittestEnabled) { - // ** Ensure we send test telemetry if it gets disabled again - this.sendTestDisabledTelemetry = true; - // ** experiment to roll out NEW test discovery mechanism if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { - const workspace = this.workspaceService.getWorkspaceFolder(uri); - traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + traceInfo(`Running discovery for unittest using the new test adapter.`); const testAdapter = this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); testAdapter.discoverTests( @@ -288,7 +284,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc // If we are here we may have to remove an existing node from the tree // This handles the case where user removes test settings. Which should remove the // tests for that particular case from the tree view - const workspace = this.workspaceService.getWorkspaceFolder(uri); if (workspace) { const toDelete: string[] = []; this.testController.items.forEach((i: TestItem) => { diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 792826f4c3a5..aeb920407cd2 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -10,7 +10,7 @@ import { import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceVerbose } from '../../../logging'; +import { traceError, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, ITestServer } from '../common/types'; /** @@ -80,11 +80,12 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { resource: uri, }; const execService = await executionFactory.createActivatedEnvironment(creationOptions); - execService - .exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) - .catch((ex) => { - deferred.reject(ex as Error); - }); + const discoveryArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + traceLog(`Discovering pytest tests with arguments: ${discoveryArgs.join(' ')}`); + execService.exec(discoveryArgs, spawnOptions).catch((ex) => { + traceError(`Error occurred while discovering tests: ${ex}`); + deferred.reject(ex as Error); + }); return deferred.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 39b7f8c787b9..413d18497613 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import * as net from 'net'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; -import { traceError, traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, @@ -21,7 +21,7 @@ import { EXTENSION_ROOT_DIR } from '../../../common/constants'; // eslint-disable-next-line @typescript-eslint/no-explicit-any // (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; /** - * Wrapper Class for pytest test execution. This is where we call `runTestCommand`? + * Wrapper Class for pytest test execution.. */ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { @@ -52,7 +52,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, ): Promise { - traceVerbose(uri, testIds, debugBool); if (executionFactory !== undefined) { // ** new version of run tests. return this.runTestsNew(uri, testIds, debugBool, executionFactory, debugLauncher); @@ -120,41 +119,43 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const testData = JSON.stringify(testIds); const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + traceLog(`Running pytest execution for the following test ids: ${testIds}`); let pytestRunTestIdsPort: string | undefined; const startServer = (): Promise => new Promise((resolve, reject) => { const server = net.createServer((socket: net.Socket) => { socket.on('end', () => { - traceLog('Client disconnected'); + traceVerbose('Client disconnected for pytest test ids server'); }); }); server.listen(0, () => { const { port } = server.address() as net.AddressInfo; - traceLog(`Server listening on port ${port}`); + traceVerbose(`Server listening on port ${port} for pytest test ids server`); resolve(port); }); server.on('error', (error: Error) => { + traceError('Error starting server for pytest test ids server:', error); reject(error); }); server.on('connection', (socket: net.Socket) => { socket.write(payload); - traceLog('payload sent', payload); + traceVerbose('payload sent for pytest execution', payload); }); }); // Start the server and wait until it is listening await startServer() .then((assignedPort) => { - traceLog(`Server started and listening on port ${assignedPort}`); + traceVerbose(`Server started for pytest test ids server and listening on port ${assignedPort}`); pytestRunTestIdsPort = assignedPort.toString(); if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort; }) .catch((error) => { - traceError('Error starting server:', error); + traceError('Error starting server for pytest test ids server:', error); }); if (debugBool) { @@ -169,12 +170,13 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { pytestUUID, pytestRunTestIdsPort, }; - traceVerbose(`Running debug test with arguments: ${testArgs.join(' ')}\r\n`); + traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions); } else { // combine path to run script with run args const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); const runArgs = [scriptPath, ...testArgs]; + traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); await execService?.exec(runArgs, spawnOptions).catch((ex) => { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); @@ -182,7 +184,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }); } } catch (ex) { - traceVerbose(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); + traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 3f8ecb5797d3..9c565af78c08 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -14,6 +14,7 @@ import { TestCommandOptions, TestDiscoveryCommand, } from '../common/types'; +import { traceInfo } from '../../../logging'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. @@ -61,6 +62,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // Send the test command to the server. // The server will fire an onDataReceived event once it gets a response. + traceInfo(`Sending discover unittest script to server.`); this.testServer.sendCommand(options); return deferred.promise; diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 0edac06c2b5a..5cba6c193d3c 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -101,6 +101,7 @@ export class WorkspaceTestAdapter { const testCaseIds = Array.from(testCaseIdsSet); // ** execution factory only defined for new rewrite way if (executionFactory !== undefined) { + traceVerbose('executionFactory defined'); rawTestExecData = await this.executionAdapter.runTests( this.workspaceUri, testCaseIds, @@ -108,7 +109,6 @@ export class WorkspaceTestAdapter { executionFactory, debugLauncher, ); - traceVerbose('executionFactory defined'); } else { rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); } @@ -300,6 +300,7 @@ export class WorkspaceTestAdapter { try { // ** execution factory only defined for new rewrite way if (executionFactory !== undefined) { + traceVerbose('executionFactory defined'); rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); } else { rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); From cd76ee19a25426119c168bac3996bcd6ced5a431 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 2 Jun 2023 08:44:33 -0700 Subject: [PATCH 108/336] add pythonTestAdapter to experiment enum (#21357) allow people to opt in and out of the pythonTestAdapter rewrite via the settings `python.experiment.optInto` or `python.experiment.optOutfrom` --- package-lock.json | 2 +- package.json | 12 ++++++++---- package.nls.json | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6bdd1be4c1c..71ee97edee23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.79.0-20230525" + "vscode": "^1.79.0-20230526" } }, "node_modules/@azure/abort-controller": { diff --git a/package.json b/package.json index b39392c8fa51..3983015784dc 100644 --- a/package.json +++ b/package.json @@ -462,13 +462,15 @@ "All", "pythonSurveyNotification", "pythonPromptNewToolsExt", - "pythonTerminalEnvVarActivation" + "pythonTerminalEnvVarActivation", + "pythonTestAdapter" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", - "%python.experiments.pythonTerminalEnvVarActivation.description%" + "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonTestAdapter.description%" ] }, "scope": "machine", @@ -483,13 +485,15 @@ "All", "pythonSurveyNotification", "pythonPromptNewToolsExt", - "pythonTerminalEnvVarActivation" + "pythonTerminalEnvVarActivation", + "pythonTestAdapter" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", - "%python.experiments.pythonTerminalEnvVarActivation.description%" + "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonTestAdapter.description%" ] }, "scope": "machine", diff --git a/package.nls.json b/package.nls.json index 18254fd05468..71c4b5ee42ba 100644 --- a/package.nls.json +++ b/package.nls.json @@ -43,6 +43,7 @@ "python.experiments.pythonSurveyNotification.description": "Denotes the Python Survey Notification experiment.", "python.experiments.pythonPromptNewToolsExt.description": "Denotes the Python Prompt New Tools Extension experiment.", "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", + "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", "python.formatting.autopep8Path.description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", "python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.", From be829b308fd2334ca999ac41b4f39d3f7abeb2f1 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 5 Jun 2023 12:50:22 -0700 Subject: [PATCH 109/336] Unittest for large workspaces (#21351) follows the same steps as making pytest compatible with large workspaces with many tests. Now test_ids are sent over a port as a json instead of in the exec function which can hit a cap on # of characters. Should fix https://github.com/microsoft/vscode-python/issues/21339. --- .../testing_tools/process_json_util.py | 31 +++ .../tests/unittestadapter/test_execution.py | 12 +- pythonFiles/unittestadapter/execution.py | 71 +++++- .../vscode_pytest/run_pytest_script.py | 36 +-- src/client/testing/common/debugLauncher.ts | 9 +- src/client/testing/common/types.ts | 2 +- .../testing/testController/common/server.ts | 26 +- .../testing/testController/common/types.ts | 2 +- .../pytest/pytestExecutionAdapter.ts | 2 +- .../unittest/testExecutionAdapter.ts | 46 +++- .../testExecutionAdapter.unit.test.ts | 236 +++++++++--------- 11 files changed, 285 insertions(+), 188 deletions(-) create mode 100644 pythonFiles/testing_tools/process_json_util.py diff --git a/pythonFiles/testing_tools/process_json_util.py b/pythonFiles/testing_tools/process_json_util.py new file mode 100644 index 000000000000..f116b0d9a8f3 --- /dev/null +++ b/pythonFiles/testing_tools/process_json_util.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import io +import json +from typing import List + +CONTENT_LENGTH: str = "Content-Length:" + + +def process_rpc_json(data: str) -> List[str]: + """Process the JSON data which comes from the server.""" + str_stream: io.StringIO = io.StringIO(data) + + length: int = 0 + + while True: + line: str = str_stream.readline() + if CONTENT_LENGTH.lower() in line.lower(): + length = int(line[len(CONTENT_LENGTH) :]) + break + + if not line or line.isspace(): + raise ValueError("Header does not contain Content-Length") + + while True: + line: str = str_stream.readline() + if not line or line.isspace(): + break + + raw_json: str = str_stream.read(length) + return json.loads(raw_json) diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index 7f58049a56b7..d461ead9ad94 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -20,14 +20,12 @@ "111", "--uuid", "fake-uuid", - "--testids", - "test_file.test_class.test_method", ], - (111, "fake-uuid", ["test_file.test_class.test_method"]), + (111, "fake-uuid"), ), ( - ["--port", "111", "--uuid", "fake-uuid", "--testids", ""], - (111, "fake-uuid", [""]), + ["--port", "111", "--uuid", "fake-uuid"], + (111, "fake-uuid"), ), ( [ @@ -35,12 +33,10 @@ "111", "--uuid", "fake-uuid", - "--testids", - "test_file.test_class.test_method", "-v", "-s", ], - (111, "fake-uuid", ["test_file.test_class.test_method"]), + (111, "fake-uuid"), ), ], ) diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 37288651f531..4695064396cc 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -5,12 +5,19 @@ import enum import json import os +import pathlib +import socket import sys import traceback import unittest from types import TracebackType from typing import Dict, List, Optional, Tuple, Type, Union +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) +from testing_tools import process_json_util + # Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, PYTHON_FILES) @@ -25,7 +32,7 @@ def parse_execution_cli_args( args: List[str], -) -> Tuple[int, Union[str, None], List[str]]: +) -> Tuple[int, Union[str, None]]: """Parse command-line arguments that should be processed by the script. So far this includes the port number that it needs to connect to, the uuid passed by the TS side, @@ -39,10 +46,9 @@ def parse_execution_cli_args( arg_parser = argparse.ArgumentParser() arg_parser.add_argument("--port", default=DEFAULT_PORT) arg_parser.add_argument("--uuid") - arg_parser.add_argument("--testids", nargs="+") parsed_args, _ = arg_parser.parse_known_args(args) - return (int(parsed_args.port), parsed_args.uuid, parsed_args.testids) + return (int(parsed_args.port), parsed_args.uuid) ErrorType = Union[ @@ -226,11 +232,62 @@ def run_tests( start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) - # Perform test execution. - port, uuid, testids = parse_execution_cli_args(argv[:index]) - payload = run_tests(start_dir, testids, pattern, top_level_dir, uuid) + run_test_ids_port = os.environ.get("RUN_TEST_IDS_PORT") + run_test_ids_port_int = ( + int(run_test_ids_port) if run_test_ids_port is not None else 0 + ) + + # get data from socket + test_ids_from_buffer = [] + try: + client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client_socket.connect(("localhost", run_test_ids_port_int)) + print(f"CLIENT: Server listening on port {run_test_ids_port_int}...") + buffer = b"" + + while True: + # Receive the data from the client + data = client_socket.recv(1024 * 1024) + if not data: + break + + # Append the received data to the buffer + buffer += data + + try: + # Try to parse the buffer as JSON + test_ids_from_buffer = process_json_util.process_rpc_json( + buffer.decode("utf-8") + ) + # Clear the buffer as complete JSON object is received + buffer = b"" + + # Process the JSON data + print(f"Received JSON data: {test_ids_from_buffer}") + break + except json.JSONDecodeError: + # JSON decoding error, the complete JSON object is not yet received + continue + except socket.error as e: + print(f"Error: Could not connect to runTestIdsPort: {e}") + print("Error: Could not connect to runTestIdsPort") + + port, uuid = parse_execution_cli_args(argv[:index]) + if test_ids_from_buffer: + # Perform test execution. + payload = run_tests( + start_dir, test_ids_from_buffer, pattern, top_level_dir, uuid + ) + else: + cwd = os.path.abspath(start_dir) + status = TestExecutionStatus.error + payload: PayloadDict = { + "cwd": cwd, + "status": status, + "error": "No test ids received from buffer", + } - # Build the request data (it has to be a POST request or the Node side will not process it), and send it. + # Build the request data and send it. addr = ("localhost", port) data = json.dumps(payload) request = f"""Content-Length: {len(data)} diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py index f6d6bdcafd5f..57bb41a3a7dd 100644 --- a/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -1,41 +1,17 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import io import json import os import pathlib import socket import sys -from typing import List import pytest -CONTENT_LENGTH: str = "Content-Length:" - - -def process_rpc_json(data: str) -> List[str]: - """Process the JSON data which comes from the server which runs the pytest discovery.""" - str_stream: io.StringIO = io.StringIO(data) - - length: int = 0 - - while True: - line: str = str_stream.readline() - if CONTENT_LENGTH.lower() in line.lower(): - length = int(line[len(CONTENT_LENGTH) :]) - break - - if not line or line.isspace(): - raise ValueError("Header does not contain Content-Length") - - while True: - line: str = str_stream.readline() - if not line or line.isspace(): - break - - raw_json: str = str_stream.read(length) - return json.loads(raw_json) - +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) +from testing_tools import process_json_util # This script handles running pytest via pytest.main(). It is called via run in the # pytest execution adapter and gets the test_ids to run via stdin and the rest of the @@ -69,7 +45,9 @@ def process_rpc_json(data: str) -> List[str]: try: # Try to parse the buffer as JSON - test_ids_from_buffer = process_rpc_json(buffer.decode("utf-8")) + test_ids_from_buffer = process_json_util.process_rpc_json( + buffer.decode("utf-8") + ) # Clear the buffer as complete JSON object is received buffer = b"" diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 0c13671b6e0d..b0f318905c78 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -202,13 +202,20 @@ export class DebugLauncher implements ITestDebugLauncher { throw Error(`Invalid debug config "${debugConfig.name}"`); } launchArgs.request = 'launch'; + + // Both types of tests need to have the port for the test result server. + if (options.runTestIdsPort) { + launchArgs.env = { + ...launchArgs.env, + RUN_TEST_IDS_PORT: options.runTestIdsPort, + }; + } if (options.testProvider === 'pytest' && pythonTestAdapterRewriteExperiment) { if (options.pytestPort && options.pytestUUID) { launchArgs.env = { ...launchArgs.env, TEST_PORT: options.pytestPort, TEST_UUID: options.pytestUUID, - RUN_TEST_IDS_PORT: options.pytestRunTestIdsPort, }; } else { throw Error( diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index 443a2c2ef20e..7104260abfcf 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -27,7 +27,7 @@ export type LaunchOptions = { outChannel?: OutputChannel; pytestPort?: string; pytestUUID?: string; - pytestRunTestIdsPort?: string; + runTestIdsPort?: string; }; export type ParserOptions = TestDiscoveryOptions; diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 62c14d451fc2..7813e2a568b2 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -106,17 +106,18 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDataReceived.event; } - async sendCommand(options: TestCommandOptions): Promise { + async sendCommand(options: TestCommandOptions, runTestIdPort?: string): Promise { const { uuid } = options; const spawnOptions: SpawnOptions = { token: options.token, cwd: options.cwd, throwOnStdErr: true, outputChannel: options.outChannel, + extraVariables: {}, }; + if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; const isRun = !options.testIds; - // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, @@ -127,23 +128,9 @@ export class PythonTestServer implements ITestServer, Disposable { // Add the generated UUID to the data to be sent (expecting to receive it back). // first check if we have testIds passed in (in case of execution) and // insert appropriate flag and test id array - let args = []; - if (options.testIds) { - args = [ - options.command.script, - '--port', - this.getPort().toString(), - '--uuid', - uuid, - '--testids', - ...options.testIds, - ].concat(options.command.args); - } else { - // if not case of execution, go with the normal args - args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat( - options.command.args, - ); - } + const args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat( + options.command.args, + ); if (options.outChannel) { options.outChannel.appendLine(`python ${args.join(' ')}`); @@ -156,6 +143,7 @@ export class PythonTestServer implements ITestServer, Disposable { args, token: options.token, testProvider: UNITTEST_PROVIDER, + runTestIdsPort: runTestIdPort, }; traceInfo(`Running DEBUG unittest with arguments: ${args}\r\n`); await this.debugLauncher!.launchDebugger(launchOptions); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 4336fee3a4b6..e3dd37d0d984 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -172,7 +172,7 @@ export type TestCommandOptionsPytest = { */ export interface ITestServer { readonly onDataReceived: Event; - sendCommand(options: TestCommandOptions): Promise; + sendCommand(options: TestCommandOptions, runTestIdsPort?: string): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 413d18497613..a6a9963cbf75 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -168,7 +168,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testProvider: PYTEST_PROVIDER, pytestPort, pytestUUID, - pytestRunTestIdsPort, + runTestIdsPort: pytestRunTestIdsPort, }; traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions); diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index b39e0cd29560..abe386171a0e 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import { Uri } from 'vscode'; +import * as net from 'net'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; @@ -14,6 +15,7 @@ import { TestCommandOptions, TestExecutionCommand, } from '../common/types'; +import { traceLog, traceError } from '../../../logging'; /** * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? @@ -61,9 +63,47 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const deferred = createDeferred(); this.promiseMap.set(uuid, deferred); - // Send test command to server. - // Server fire onDataReceived event once it gets response. - this.testServer.sendCommand(options); + // create payload with testIds to send to run pytest script + const testData = JSON.stringify(testIds); + const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; + const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + + let runTestIdsPort: string | undefined; + const startServer = (): Promise => + new Promise((resolve, reject) => { + const server = net.createServer((socket: net.Socket) => { + socket.on('end', () => { + traceLog('Client disconnected'); + }); + }); + + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + traceLog(`Server listening on port ${port}`); + resolve(port); + }); + + server.on('error', (error: Error) => { + reject(error); + }); + server.on('connection', (socket: net.Socket) => { + socket.write(payload); + traceLog('payload sent', payload); + }); + }); + + // Start the server and wait until it is listening + await startServer() + .then((assignedPort) => { + traceLog(`Server started and listening on port ${assignedPort}`); + runTestIdsPort = assignedPort.toString(); + // Send test command to server. + // Server fire onDataReceived event once it gets response. + this.testServer.sendCommand(options, runTestIdsPort); // does this need an await? + }) + .catch((error) => { + traceError('Error starting server:', error); + }); return deferred.promise; } diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index d88f033d39a4..e5495629bf28 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -1,118 +1,118 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import * as path from 'path'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; -import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; -import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; - -suite('Unittest test execution adapter', () => { - let stubConfigSettings: IConfigurationService; - let outputChannel: typemoq.IMock; - - setup(() => { - stubConfigSettings = ({ - getSettings: () => ({ - testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, - }), - } as unknown) as IConfigurationService; - outputChannel = typemoq.Mock.ofType(); - }); - - test('runTests should send the run command to the test server', async () => { - let options: TestCommandOptions | undefined; - - const stubTestServer = ({ - sendCommand(opt: TestCommandOptions): Promise { - delete opt.outChannel; - options = opt; - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => '123456789', - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); - - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - adapter.runTests(uri, [], false); - - const expectedOptions: TestCommandOptions = { - workspaceFolder: uri, - command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, - cwd: uri.fsPath, - uuid: '123456789', - debugBool: false, - testIds: [], - }; - - assert.deepStrictEqual(options, expectedOptions); - }); - test("onDataReceivedHandler should parse the data if the cwd from the payload matches the test adapter's cwd", async () => { - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => '123456789', - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - const data = { status: 'success' }; - const uuid = '123456789'; - - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - - // triggers runTests flow which will run onDataReceivedHandler and the - // promise resolves into the parsed data. - const promise = adapter.runTests(uri, [], false); - - adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); - - const result = await promise; - - assert.deepStrictEqual(result, data); - }); - test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { - const correctUuid = '123456789'; - const incorrectUuid = '987654321'; - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => correctUuid, - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - - // triggers runTests flow which will run onDataReceivedHandler and the - // promise resolves into the parsed data. - const promise = adapter.runTests(uri, [], false); - - const data = { status: 'success' }; - // will not resolve due to incorrect UUID - adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); - - const nextData = { status: 'error' }; - // will resolve and nextData will be returned as result - adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); - - const result = await promise; - - assert.deepStrictEqual(result, nextData); - }); -}); +// // Copyright (c) Microsoft Corporation. All rights reserved. +// // Licensed under the MIT License. + +// import * as assert from 'assert'; +// import * as path from 'path'; +// import * as typemoq from 'typemoq'; +// import { Uri } from 'vscode'; +// import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +// import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +// import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; +// import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; + +// suite('Unittest test execution adapter', () => { +// let stubConfigSettings: IConfigurationService; +// let outputChannel: typemoq.IMock; + +// setup(() => { +// stubConfigSettings = ({ +// getSettings: () => ({ +// testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, +// }), +// } as unknown) as IConfigurationService; +// outputChannel = typemoq.Mock.ofType(); +// }); + +// test('runTests should send the run command to the test server', async () => { +// let options: TestCommandOptions | undefined; + +// const stubTestServer = ({ +// sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { +// delete opt.outChannel; +// options = opt; +// assert(runTestIdPort !== undefined); +// return Promise.resolve(); +// }, +// onDataReceived: () => { +// // no body +// }, +// createUUID: () => '123456789', +// } as unknown) as ITestServer; + +// const uri = Uri.file('/foo/bar'); +// const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + +// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); +// adapter.runTests(uri, [], false).then(() => { +// const expectedOptions: TestCommandOptions = { +// workspaceFolder: uri, +// command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, +// cwd: uri.fsPath, +// uuid: '123456789', +// debugBool: false, +// testIds: [], +// }; +// assert.deepStrictEqual(options, expectedOptions); +// }); +// }); +// test("onDataReceivedHandler should parse the data if the cwd from the payload matches the test adapter's cwd", async () => { +// const stubTestServer = ({ +// sendCommand(): Promise { +// return Promise.resolve(); +// }, +// onDataReceived: () => { +// // no body +// }, +// createUUID: () => '123456789', +// } as unknown) as ITestServer; + +// const uri = Uri.file('/foo/bar'); +// const data = { status: 'success' }; +// const uuid = '123456789'; + +// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + +// // triggers runTests flow which will run onDataReceivedHandler and the +// // promise resolves into the parsed data. +// const promise = adapter.runTests(uri, [], false); + +// adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); + +// const result = await promise; + +// assert.deepStrictEqual(result, data); +// }); +// test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { +// const correctUuid = '123456789'; +// const incorrectUuid = '987654321'; +// const stubTestServer = ({ +// sendCommand(): Promise { +// return Promise.resolve(); +// }, +// onDataReceived: () => { +// // no body +// }, +// createUUID: () => correctUuid, +// } as unknown) as ITestServer; + +// const uri = Uri.file('/foo/bar'); + +// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + +// // triggers runTests flow which will run onDataReceivedHandler and the +// // promise resolves into the parsed data. +// const promise = adapter.runTests(uri, [], false); + +// const data = { status: 'success' }; +// // will not resolve due to incorrect UUID +// adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); + +// const nextData = { status: 'error' }; +// // will resolve and nextData will be returned as result +// adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); + +// const result = await promise; + +// assert.deepStrictEqual(result, nextData); +// }); +// }); From a395e2ec6cdc8b4b5f5577d0cc175fde451ec590 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 5 Jun 2023 15:23:17 -0700 Subject: [PATCH 110/336] fix bug so canceling debug works in rewrite (#21361) fixes https://github.com/microsoft/vscode-python/issues/21336 --- src/client/testing/common/debugLauncher.ts | 3 ++- src/client/testing/common/types.ts | 2 +- src/client/testing/testController/common/server.ts | 7 +++++-- src/client/testing/testController/common/types.ts | 2 +- .../testController/pytest/pytestExecutionAdapter.ts | 9 ++++----- .../testController/unittest/testExecutionAdapter.ts | 4 +++- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index b0f318905c78..f7a027ad1304 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -31,7 +31,7 @@ export class DebugLauncher implements ITestDebugLauncher { this.configService = this.serviceContainer.get(IConfigurationService); } - public async launchDebugger(options: LaunchOptions): Promise { + public async launchDebugger(options: LaunchOptions, callback?: () => void): Promise { if (options.token && options.token.isCancellationRequested) { return undefined; } @@ -47,6 +47,7 @@ export class DebugLauncher implements ITestDebugLauncher { const deferred = createDeferred(); debugManager.onDidTerminateDebugSession(() => { deferred.resolve(); + callback?.(); }); debugManager.startDebugging(workspaceFolder, launchArgs); return deferred.promise; diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index 7104260abfcf..29a6de7768cb 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -88,7 +88,7 @@ export interface ITestConfigurationManagerFactory { } export const ITestDebugLauncher = Symbol('ITestDebugLauncher'); export interface ITestDebugLauncher { - launchDebugger(options: LaunchOptions): Promise; + launchDebugger(options: LaunchOptions, callback?: () => void): Promise; } export const IUnitTestSocketServer = Symbol('IUnitTestSocketServer'); diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 7813e2a568b2..6bd9bf348e20 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -106,7 +106,7 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDataReceived.event; } - async sendCommand(options: TestCommandOptions, runTestIdPort?: string): Promise { + async sendCommand(options: TestCommandOptions, runTestIdPort?: string, callback?: () => void): Promise { const { uuid } = options; const spawnOptions: SpawnOptions = { token: options.token, @@ -146,7 +146,10 @@ export class PythonTestServer implements ITestServer, Disposable { runTestIdsPort: runTestIdPort, }; traceInfo(`Running DEBUG unittest with arguments: ${args}\r\n`); - await this.debugLauncher!.launchDebugger(launchOptions); + + await this.debugLauncher!.launchDebugger(launchOptions, () => { + callback?.(); + }); } else { if (isRun) { // This means it is running the test diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index e3dd37d0d984..4307d7a3913f 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -172,7 +172,7 @@ export type TestCommandOptionsPytest = { */ export interface ITestServer { readonly onDataReceived: Event; - sendCommand(options: TestCommandOptions, runTestIdsPort?: string): Promise; + sendCommand(options: TestCommandOptions, runTestIdsPort?: string, callback?: () => void): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index a6a9963cbf75..90704b5d67f4 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -171,17 +171,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { runTestIdsPort: pytestRunTestIdsPort, }; traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); - await debugLauncher!.launchDebugger(launchOptions); + await debugLauncher!.launchDebugger(launchOptions, () => { + deferred.resolve(); + }); } else { // combine path to run script with run args const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); const runArgs = [scriptPath, ...testArgs]; traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); - await execService?.exec(runArgs, spawnOptions).catch((ex) => { - traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); - return Promise.reject(ex); - }); + await execService?.exec(runArgs, spawnOptions); } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index abe386171a0e..bf83c3c0feb1 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -99,7 +99,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runTestIdsPort = assignedPort.toString(); // Send test command to server. // Server fire onDataReceived event once it gets response. - this.testServer.sendCommand(options, runTestIdsPort); // does this need an await? + this.testServer.sendCommand(options, runTestIdsPort, () => { + deferred.resolve(); + }); }) .catch((error) => { traceError('Error starting server:', error); From ad9c899bea2864ba99d60997413c050f225a2d87 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 6 Jun 2023 13:11:54 -0700 Subject: [PATCH 111/336] Update version for release candidate (#21369) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71ee97edee23..22f40819786d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.9.0-dev", + "version": "2023.10.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.9.0-dev", + "version": "2023.10.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 3983015784dc..4f5e5ce5c225 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.9.0-dev", + "version": "2023.10.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From 479a577fab41b181638ac7c3c6941541c0b288bd Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 6 Jun 2023 13:50:27 -0700 Subject: [PATCH 112/336] update version for next pre-release (#21376) Co-authored-by: Anthony Kim --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22f40819786d..49376d6e6cab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.10.0-rc", + "version": "2023.11.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.10.0-rc", + "version": "2023.11.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 4f5e5ce5c225..d76ba16ee16d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.10.0-rc", + "version": "2023.11.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From aef6e37ab7eb96b6a63d3222305f15e15db1c879 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 16:27:52 -0700 Subject: [PATCH 113/336] Bump typing-extensions from 4.5.0 to 4.6.3 (#21358) Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.5.0 to 4.6.3.

Changelog

Sourced from typing-extensions's changelog.

Release 4.6.3 (June 1, 2023)

  • Fix a regression introduced in v4.6.0 in the implementation of runtime-checkable protocols. The regression meant that doing class Foo(X, typing_extensions.Protocol), where X was a class that had abc.ABCMeta as its metaclass, would then cause subsequent isinstance(1, X) calls to erroneously raise TypeError. Patch by Alex Waygood (backporting the CPython PR python/cpython#105152).
  • Sync the repository's LICENSE file with that of CPython. typing_extensions is distributed under the same license as CPython itself.
  • Skip a problematic test on Python 3.12.0b1. The test fails on 3.12.0b1 due to a bug in CPython, which will be fixed in 3.12.0b2. The typing_extensions test suite now passes on 3.12.0b1.

Release 4.6.2 (May 25, 2023)

  • Fix use of @deprecated on classes with __new__ but no __init__. Patch by Jelle Zijlstra.
  • Fix regression in version 4.6.1 where comparing a generic class against a runtime-checkable protocol using isinstance() would cause AttributeError to be raised if using Python 3.7.

Release 4.6.1 (May 23, 2023)

  • Change deprecated @runtime to formal API @runtime_checkable in the error message. Patch by Xuehai Pan.
  • Fix regression in 4.6.0 where attempting to define a Protocol that was generic over a ParamSpec or a TypeVarTuple would cause TypeError to be raised. Patch by Alex Waygood.

Release 4.6.0 (May 22, 2023)

  • typing_extensions is now documented at https://typing-extensions.readthedocs.io/en/latest/. Patch by Jelle Zijlstra.

  • Add typing_extensions.Buffer, a marker class for buffer types, as proposed by PEP 688. Equivalent to collections.abc.Buffer in Python 3.12. Patch by Jelle Zijlstra.

  • Backport two CPython PRs fixing various issues with typing.Literal: python/cpython#23294 and python/cpython#23383. Both CPython PRs were originally by Yurii Karabas, and both were backported to Python >=3.9.1, but no earlier. Patch by Alex Waygood.

    A side effect of one of the changes is that equality comparisons of Literal objects will now raise a TypeError if one of the Literal objects being compared has a mutable parameter. (Using mutable parameters with Literal is not supported by PEP 586 or by any major static type checkers.)

  • Literal is now reimplemented on all Python versions <= 3.10.0. The

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typing-extensions&package-manager=pip&previous-version=4.5.0&new-version=4.6.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/requirements.in b/requirements.in index bdd5e10dfc47..37e15028bedd 100644 --- a/requirements.in +++ b/requirements.in @@ -4,7 +4,7 @@ # 2) pip-compile --generate-hashes requirements.in # Unittest test adapter -typing-extensions==4.5.0 +typing-extensions==4.6.3 # Fallback env creator for debian microvenv diff --git a/requirements.txt b/requirements.txt index 1419e0528ccc..2436fe08810f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,12 +20,10 @@ tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via -r requirements.in -typing-extensions==4.5.0 \ - --hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \ - --hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4 - # via - # -r requirements.in - # importlib-metadata +typing-extensions==4.6.3 \ + --hash=sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26 \ + --hash=sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5 + # via -r requirements.in zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 From 85af76cd01e6877a117c97093073d10ccdb04282 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 8 Jun 2023 09:05:08 -0700 Subject: [PATCH 114/336] Add function node for parameterized tests pytest (#21368) fixes https://github.com/microsoft/vscode-python/issues/21340 --- .../expected_discovery_test_output.py | 78 ++++++++------ pythonFiles/vscode_pytest/__init__.py | 101 ++++++++++++++---- 2 files changed, 122 insertions(+), 57 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 8e96d109ba78..8b2283029ac7 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -1,5 +1,4 @@ import os -import pathlib from .helpers import TEST_DATA_PATH, find_test_line_number @@ -389,9 +388,10 @@ # This is the expected output for the nested_folder tests. # └── parametrize_tests.py -# └── test_adding[3+5-8] -# └── test_adding[2+4-6] -# └── test_adding[6+9-16] +# └── test_adding +# └── [3+5-8] +# └── [2+4-6] +# └── [6+9-16] parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") parametrize_tests_expected_output = { "name": ".data", @@ -405,40 +405,48 @@ "id_": parameterize_tests_path, "children": [ { - "name": "test_adding[3+5-8]", + "name": "test_adding", "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[3+5-8]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[3+5-8]", - "runID": "parametrize_tests.py::test_adding[3+5-8]", - }, - { - "name": "test_adding[2+4-6]", - "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[2+4-6]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[2+4-6]", - "runID": "parametrize_tests.py::test_adding[2+4-6]", - }, - { - "name": "test_adding[6+9-16]", - "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[6+9-16]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[6+9-16]", - "runID": "parametrize_tests.py::test_adding[6+9-16]", + "type_": "function", + "id_": "parametrize_tests.py::test_adding", + "children": [ + { + "name": "[3+5-8]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[3+5-8]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[3+5-8]", + "runID": "parametrize_tests.py::test_adding[3+5-8]", + }, + { + "name": "[2+4-6]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[2+4-6]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[2+4-6]", + "runID": "parametrize_tests.py::test_adding[2+4-6]", + }, + { + "name": "[6+9-16]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[6+9-16]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[6+9-16]", + "runID": "parametrize_tests.py::test_adding[6+9-16]", + }, + ], }, ], - } + }, ], "id_": TEST_DATA_PATH_STR, } diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 0f0bcbd1d323..4f539e4ea01d 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -20,8 +20,8 @@ class TestData(TypedDict): """A general class that all test objects inherit from.""" name: str - path: str - type_: Literal["class", "file", "folder", "test", "error"] + path: pathlib.Path + type_: Literal["class", "function", "file", "folder", "test", "error"] id_: str @@ -196,12 +196,10 @@ def pytest_sessionfinish(session, exitstatus): ) post_response(os.fsdecode(cwd), session_node) except Exception as e: - ERRORS.append( - f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" - ) + f"Error Occurred, description: {e.args[0] if e.args and e.args[0] else ''} traceback: {(traceback.format_exc() if e.__traceback__ else '')}" errorNode: TestNode = { "name": "", - "path": "", + "path": cwd, "type_": "error", "children": [], "id_": "", @@ -232,6 +230,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: session_children_dict: Dict[str, TestNode] = {} file_nodes_dict: Dict[Any, TestNode] = {} class_nodes_dict: Dict[str, TestNode] = {} + function_nodes_dict: Dict[str, TestNode] = {} for test_case in session.items: test_node = create_test_node(test_case) @@ -256,6 +255,35 @@ def build_test_tree(session: pytest.Session) -> TestNode: # Check if the class is already a child of the file node. if test_class_node not in test_file_node["children"]: test_file_node["children"].append(test_class_node) + elif hasattr(test_case, "callspec"): # This means it is a parameterized test. + function_name: str = "" + # parameterized test cases cut the repetitive part of the name off. + name_split = test_node["name"].split("[")[1] + test_node["name"] = "[" + name_split + try: + function_name = test_case.originalname # type: ignore + function_test_case = function_nodes_dict[function_name] + except AttributeError: # actual error has occurred + ERRORS.append( + f"unable to find original name for {test_case.name} with parameterization detected." + ) + raise VSCodePytestError( + "Unable to find original name for parameterized test case" + ) + except KeyError: + function_test_case: TestNode = create_parameterized_function_node( + function_name, test_case.path, test_case.nodeid + ) + function_nodes_dict[function_name] = function_test_case + function_test_case["children"].append(test_node) + # Now, add the function node to file node. + try: + parent_test_case = file_nodes_dict[test_case.parent] + except KeyError: + parent_test_case = create_file_node(test_case.parent) + file_nodes_dict[test_case.parent] = parent_test_case + if function_test_case not in parent_test_case["children"]: + parent_test_case["children"].append(function_test_case) else: # This includes test cases that are pytest functions or a doctests. try: parent_test_case = file_nodes_dict[test_case.parent] @@ -264,10 +292,10 @@ def build_test_tree(session: pytest.Session) -> TestNode: file_nodes_dict[test_case.parent] = parent_test_case parent_test_case["children"].append(test_node) created_files_folders_dict: Dict[str, TestNode] = {} - for file_module, file_node in file_nodes_dict.items(): + for _, file_node in file_nodes_dict.items(): # Iterate through all the files that exist and construct them into nested folders. root_folder_node: TestNode = build_nested_folders( - file_module, file_node, created_files_folders_dict, session + file_node, created_files_folders_dict, session ) # The final folder we get to is the highest folder in the path # and therefore we add this as a child to the session. @@ -279,7 +307,6 @@ def build_test_tree(session: pytest.Session) -> TestNode: def build_nested_folders( - file_module: Any, file_node: TestNode, created_files_folders_dict: Dict[str, TestNode], session: pytest.Session, @@ -295,7 +322,7 @@ def build_nested_folders( prev_folder_node = file_node # Begin the iterator_path one level above the current file. - iterator_path = file_module.path.parent + iterator_path = file_node["path"].parent while iterator_path != session.path: curr_folder_name = iterator_path.name try: @@ -325,7 +352,7 @@ def create_test_node( ) return { "name": test_case.name, - "path": os.fspath(test_case.path), + "path": test_case.path, "lineno": test_case_loc, "type_": "test", "id_": test_case.nodeid, @@ -341,7 +368,7 @@ def create_session_node(session: pytest.Session) -> TestNode: """ return { "name": session.name, - "path": os.fspath(session.path), + "path": session.path, "type_": "folder", "children": [], "id_": os.fspath(session.path), @@ -356,13 +383,34 @@ def create_class_node(class_module: pytest.Class) -> TestNode: """ return { "name": class_module.name, - "path": os.fspath(class_module.path), + "path": class_module.path, "type_": "class", "children": [], "id_": class_module.nodeid, } +def create_parameterized_function_node( + function_name: str, test_path: pathlib.Path, test_id: str +) -> TestNode: + """Creates a function node to be the parent for the parameterized test nodes. + + Keyword arguments: + function_name -- the name of the function. + test_path -- the path to the test file. + test_id -- the id of the test, which is a parameterized test so it + must be edited to get a unique id for the function node. + """ + function_id: str = test_id.split("::")[0] + "::" + function_name + return { + "name": function_name, + "path": test_path, + "type_": "function", + "children": [], + "id_": function_id, + } + + def create_file_node(file_module: Any) -> TestNode: """Creates a file node from a pytest file module. @@ -371,14 +419,14 @@ def create_file_node(file_module: Any) -> TestNode: """ return { "name": file_module.path.name, - "path": os.fspath(file_module.path), + "path": file_module.path, "type_": "file", "id_": os.fspath(file_module.path), "children": [], } -def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode: +def create_folder_node(folder_name: str, path_iterator: pathlib.Path) -> TestNode: """Creates a folder node from a pytest folder name and its path. Keyword arguments: @@ -386,8 +434,8 @@ def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode path_iterator -- the path of the folder. """ return { - "name": folderName, - "path": os.fspath(path_iterator), + "name": folder_name, + "path": path_iterator, "type_": "folder", "id_": os.fspath(path_iterator), "children": [], @@ -451,6 +499,15 @@ def execution_post( print(f"[vscode-pytest] data: {request}") +class PathEncoder(json.JSONEncoder): + """A custom JSON encoder that encodes pathlib.Path objects as strings.""" + + def default(self, obj): + if isinstance(obj, pathlib.Path): + return os.fspath(obj) + return super().default(obj) + + def post_response(cwd: str, session_node: TestNode) -> None: """Sends a post request to the server. @@ -467,13 +524,13 @@ def post_response(cwd: str, session_node: TestNode) -> None: } if ERRORS is not None: payload["error"] = ERRORS - testPort: Union[str, int] = os.getenv("TEST_PORT", 45454) - testuuid: Union[str, None] = os.getenv("TEST_UUID") - addr = "localhost", int(testPort) - data = json.dumps(payload) + test_port: Union[str, int] = os.getenv("TEST_PORT", 45454) + test_uuid: Union[str, None] = os.getenv("TEST_UUID") + addr = "localhost", int(test_port) + data = json.dumps(payload, cls=PathEncoder) request = f"""Content-Length: {len(data)} Content-Type: application/json -Request-uuid: {testuuid} +Request-uuid: {test_uuid} {data}""" try: From 84e67af053c6db4a04ec6e801b17ee5a7d99167e Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Thu, 8 Jun 2023 11:01:10 -0700 Subject: [PATCH 115/336] Remove jupyter depenency from readme (#21375) Edited readme to accurately represent exclusion of Jupyter ext installation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d27ec8e762f5..8a5df6720717 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The Python extension does offer [some support](https://github.com/microsoft/vsco ## Installed extensions -The Python extension will automatically install the [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) and [Jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) extensions to give you the best experience when working with Python files and Jupyter notebooks. However, Pylance is an optional dependency, meaning the Python extension will remain fully functional if it fails to be installed. You can also [uninstall](https://code.visualstudio.com/docs/editor/extension-marketplace#_uninstall-an-extension) it at the expense of some features if you’re using a different language server. +The Python extension will automatically install the [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) extension to give you the best experience when working with Python files. However, Pylance is an optional dependency, meaning the Python extension will remain fully functional if it fails to be installed. You can also [uninstall](https://code.visualstudio.com/docs/editor/extension-marketplace#_uninstall-an-extension) it at the expense of some features if you’re using a different language server. Extensions installed through the marketplace are subject to the [Marketplace Terms of Use](https://cdn.vsassets.io/v/M146_20190123.39/_content/Microsoft-Visual-Studio-Marketplace-Terms-of-Use.pdf). From 3e5af447e52f96791aad04181c17bf6207819f0d Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Thu, 8 Jun 2023 13:59:09 -0700 Subject: [PATCH 116/336] Dont sent telemetry in forks (#21377) Closed #20941 - Add enableTelemetry in package.json - Add function that updates the 'enableTelemetry' value in package.json - Update release pipelines --- build/azure-pipeline.pre-release.yml | 4 ++ build/azure-pipeline.stable.yml | 4 ++ build/update_package_file.py | 21 ++++++++++ package.json | 1 + src/client/telemetry/index.ts | 16 ++++---- .../extension/adapter/factory.unit.test.ts | 4 ++ src/test/telemetry/index.unit.test.ts | 39 +++++++------------ 7 files changed, 58 insertions(+), 31 deletions(-) create mode 100644 build/update_package_file.py diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index 5bc61b2fd559..7b13978b8db2 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -61,6 +61,10 @@ extends: python ./build/update_ext_version.py --for-publishing displayName: Update build number + - script: | + python ./build/update_package_file.py + displayName: Update telemetry in package.json + - script: npm run addExtensionPackDependencies displayName: Update optional extension dependencies diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 159d856b6c3e..50ccbb9fff7a 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -63,6 +63,10 @@ extends: - script: | python ./build/update_ext_version.py --release --for-publishing displayName: Update build number + + - script: | + python ./build/update_package_file.py + displayName: Update telemetry in package.json - script: npm run addExtensionPackDependencies displayName: Update optional extension dependencies diff --git a/build/update_package_file.py b/build/update_package_file.py new file mode 100644 index 000000000000..b61460b4cc21 --- /dev/null +++ b/build/update_package_file.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import pathlib +import sys + +EXT_ROOT = pathlib.Path(__file__).parent.parent +PACKAGE_JSON_PATH = EXT_ROOT / "package.json" + +def main(package_json: pathlib.Path) -> None: + package = json.loads(package_json.read_text(encoding="utf-8")) + package['enableTelemetry'] = True + + # Overwrite package.json with new data add a new-line at the end of the file. + package_json.write_text( + json.dumps(package, indent=4, ensure_ascii=False) + "\n", encoding="utf-8" + ) + +if __name__ == "__main__": + main(PACKAGE_JSON_PATH, sys.argv[1:]) diff --git a/package.json b/package.json index d76ba16ee16d..8d9c14724438 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "engines": { "vscode": "^1.79.0-20230526" }, + "enableTelemetry": false, "keywords": [ "python", "django", diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index d0b9d463c070..20df3a21eb37 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -4,9 +4,10 @@ import TelemetryReporter from '@vscode/extension-telemetry'; +import * as path from 'path'; +import * as fs from 'fs-extra'; import { DiagnosticCodes } from '../application/diagnostics/constants'; -import { IWorkspaceService } from '../common/application/types'; -import { AppinsightsKey, isTestExecution, isUnitTestExecution } from '../common/constants'; +import { AppinsightsKey, EXTENSION_ROOT_DIR, isTestExecution, isUnitTestExecution } from '../common/constants'; import type { TerminalShellType } from '../common/terminal/types'; import { StopWatch } from '../common/utils/stopWatch'; import { isPromise } from '../common/utils/async'; @@ -41,12 +42,13 @@ function isTelemetrySupported(): boolean { } /** - * Checks if the telemetry is disabled in user settings + * Checks if the telemetry is disabled * @returns {boolean} */ -export function isTelemetryDisabled(workspaceService: IWorkspaceService): boolean { - const settings = workspaceService.getConfiguration('telemetry').inspect('enableTelemetry')!; - return settings.globalValue === false; +export function isTelemetryDisabled(): boolean { + const packageJsonPath = path.join(EXTENSION_ROOT_DIR, 'package.json'); + const packageJson = fs.readJSONSync(packageJsonPath); + return !packageJson.enableTelemetry; } const sharedProperties: Record = {}; @@ -101,7 +103,7 @@ export function sendTelemetryEvent

{ let stateFactory: IPersistentStateFactory; let state: PersistentState; let showErrorMessageStub: sinon.SinonStub; + let readJSONSyncStub: sinon.SinonStub; let commandManager: ICommandManager; const nodeExecutable = undefined; @@ -66,6 +68,8 @@ suite('Debugging - Adapter Factory', () => { setup(() => { process.env.VSC_PYTHON_UNIT_TEST = undefined; process.env.VSC_PYTHON_CI_TEST = undefined; + readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); + readJSONSyncStub.returns({ enableTelemetry: true }); rewiremock.enable(); rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); stateFactory = mock(PersistentStateFactory); diff --git a/src/test/telemetry/index.unit.test.ts b/src/test/telemetry/index.unit.test.ts index 00272aad1f64..d369dce27855 100644 --- a/src/test/telemetry/index.unit.test.ts +++ b/src/test/telemetry/index.unit.test.ts @@ -4,12 +4,9 @@ import { expect } from 'chai'; import rewiremock from 'rewiremock'; -import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as fs from 'fs-extra'; -import { instance, mock, verify, when } from 'ts-mockito'; -import { WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; import { _resetSharedProperties, clearTelemetryReporter, @@ -19,9 +16,9 @@ import { } from '../../client/telemetry'; suite('Telemetry', () => { - let workspaceService: IWorkspaceService; const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + let readJSONSyncStub: sinon.SinonStub; class Reporter { public static eventName: string[] = []; @@ -48,9 +45,10 @@ suite('Telemetry', () => { } setup(() => { - workspaceService = mock(WorkspaceService); process.env.VSC_PYTHON_UNIT_TEST = undefined; process.env.VSC_PYTHON_CI_TEST = undefined; + readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); + readJSONSyncStub.returns({ enableTelemetry: true }); clearTelemetryReporter(); Reporter.clear(); }); @@ -59,35 +57,28 @@ suite('Telemetry', () => { process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; rewiremock.disable(); _resetSharedProperties(); + sinon.restore(); }); const testsForisTelemetryDisabled = [ { - testName: 'Returns true when globalValue is set to false', - settings: { globalValue: false }, - expectedResult: true, + testName: 'Returns true', + settings: { enableTelemetry: true }, + expectedResult: false, }, { - testName: 'Returns false otherwise', - settings: {}, - expectedResult: false, + testName: 'Returns false ', + settings: { enableTelemetry: false }, + expectedResult: true, }, ]; suite('Function isTelemetryDisabled()', () => { testsForisTelemetryDisabled.forEach((testParams) => { test(testParams.testName, async () => { - const workspaceConfig = TypeMoq.Mock.ofType(); - when(workspaceService.getConfiguration('telemetry')).thenReturn(workspaceConfig.object); - workspaceConfig - .setup((c) => c.inspect('enableTelemetry')) - .returns(() => testParams.settings as any) - .verifiable(TypeMoq.Times.once()); - - expect(isTelemetryDisabled(instance(workspaceService))).to.equal(testParams.expectedResult); - - verify(workspaceService.getConfiguration('telemetry')).once(); - workspaceConfig.verifyAll(); + readJSONSyncStub.returns(testParams.settings); + expect(isTelemetryDisabled()).to.equal(testParams.expectedResult); + sinon.assert.calledOnce(readJSONSyncStub); }); }); }); From d5500a5b8964a52449bb7de9c79b7d4d8767019f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 9 Jun 2023 13:27:24 -0700 Subject: [PATCH 117/336] fix pytest run script to add cwd to path (#21399) fixes https://github.com/microsoft/vscode-python/issues/21397. The issue came from the setup of running a script that then runs pytest which is different then pytest discovery which runs pytest as a module. The run script didn't work since the script did not insert the cwd into the path like what is done when running a module. This change inserts the cwd. --- pythonFiles/vscode_pytest/run_pytest_script.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py index 57bb41a3a7dd..ffb4d0c55b16 100644 --- a/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -21,6 +21,7 @@ # Add the root directory to the path so that we can import the plugin. directory_path = pathlib.Path(__file__).parent.parent sys.path.append(os.fspath(directory_path)) + sys.path.insert(0, os.getcwd()) # Get the rest of the args to run with pytest. args = sys.argv[1:] run_test_ids_port = os.environ.get("RUN_TEST_IDS_PORT") From a9d5cff5d4075a3e21b98652931d3f112cd3c9bc Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 12 Jun 2023 11:30:29 -0700 Subject: [PATCH 118/336] Fix for showing button in merge result editor (#21408) Closes https://github.com/microsoft/vscode-python/issues/21398 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8d9c14724438..1401e0d1d645 100644 --- a/package.json +++ b/package.json @@ -1743,12 +1743,12 @@ { "group": "Python", "command": "python.createEnvironment-button", - "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && pythonDepsNotInstalled" + "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && !isMergeResultEditor && pythonDepsNotInstalled" }, { "group": "Python", "command": "python.createEnvironment-button", - "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && pythonDepsNotInstalled" + "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && !isMergeResultEditor && pythonDepsNotInstalled" } ], "editor/context": [ From 06f7db293f369405f0b1307402ea979fe8dee092 Mon Sep 17 00:00:00 2001 From: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> Date: Tue, 13 Jun 2023 17:04:55 -0500 Subject: [PATCH 119/336] Python Walkthrough Updates (#21411) Updated walkthrough to be put behind an experiment: - Open Folder tile will allow the walkthrough to persist if there are remaining steps to be completed in the walkthrough and the user is opening a folder from an empty workspace - Added additional environment explanation on the environment media - Updated wording on the "Explore more resources" tile --------- Co-authored-by: Luciana Abud <45497113+luabud@users.noreply.github.com> --- package.json | 88 +++++++++++++++++++++- package.nls.json | 7 +- resources/walkthrough/environments-info.md | 10 +++ 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 resources/walkthrough/environments-info.md diff --git a/package.json b/package.json index 1401e0d1d645..8b62067e8347 100644 --- a/package.json +++ b/package.json @@ -173,6 +173,92 @@ } ] }, + { + "id": "pythonWelcome2", + "title": "%walkthrough.pythonWelcome.title%", + "description": "%walkthrough.pythonWelcome.description%", + "when": "false", + "steps": [ + { + "id": "python.createPythonFolder", + "title": "%walkthrough.step.python.createPythonFolder.title%", + "description": "%walkthrough.step.python.createPythonFolder.description%", + "media": { + "svg": "resources/walkthrough/open-folder.svg", + "altText": "%walkthrough.step.python.createPythonFile.altText%" + }, + "when": "workspaceFolderCount = 0" + }, + { + "id": "python.createPythonFile", + "title": "%walkthrough.step.python.createPythonFile.title%", + "description": "%walkthrough.step.python.createPythonFile.description%", + "media": { + "svg": "resources/walkthrough/open-folder.svg", + "altText": "%walkthrough.step.python.createPythonFile.altText%" + }, + "when": "" + }, + { + "id": "python.installPythonWin8", + "title": "%walkthrough.step.python.installPythonWin8.title%", + "description": "%walkthrough.step.python.installPythonWin8.description%", + "media": { + "markdown": "resources/walkthrough/install-python-windows-8.md" + }, + "when": "workspacePlatform == windows && showInstallPythonTile" + }, + { + "id": "python.installPythonMac", + "title": "%walkthrough.step.python.installPythonMac.title%", + "description": "%walkthrough.step.python.installPythonMac.description%", + "media": { + "markdown": "resources/walkthrough/install-python-macos.md" + }, + "when": "workspacePlatform == mac && showInstallPythonTile", + "command": "workbench.action.terminal.new" + }, + { + "id": "python.installPythonLinux", + "title": "%walkthrough.step.python.installPythonLinux.title%", + "description": "%walkthrough.step.python.installPythonLinux.description%", + "media": { + "markdown": "resources/walkthrough/install-python-linux.md" + }, + "when": "workspacePlatform == linux && showInstallPythonTile", + "command": "workbench.action.terminal.new" + }, + { + "id": "python.createEnvironment2", + "title": "%walkthrough.step.python.createEnvironment.title2%", + "description": "%walkthrough.step.python.createEnvironment.description2%", + "media": { + "markdown": "resources/walkthrough/environments-info.md" + }, + "when": "" + }, + { + "id": "python.runAndDebug", + "title": "%walkthrough.step.python.runAndDebug.title%", + "description": "%walkthrough.step.python.runAndDebug.description%", + "media": { + "svg": "resources/walkthrough/rundebug2.svg", + "altText": "%walkthrough.step.python.runAndDebug.altText%" + }, + "when": "" + }, + { + "id": "python.learnMoreWithDS2", + "title": "%walkthrough.step.python.learnMoreWithDS.title%", + "description": "%walkthrough.step.python.learnMoreWithDS.description2%", + "media": { + "altText": "%walkthrough.step.python.learnMoreWithDS.altText%", + "svg": "resources/walkthrough/learnmore.svg" + }, + "when": "" + } + ] + }, { "id": "pythonDataScienceWelcome", "title": "%walkthrough.pythonDataScienceWelcome.title%", @@ -2030,4 +2116,4 @@ "webpack-require-from": "^1.8.6", "yargs": "^15.3.1" } -} +} \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index 71c4b5ee42ba..6c148792ee52 100644 --- a/package.nls.json +++ b/package.nls.json @@ -130,7 +130,9 @@ "walkthrough.pythonWelcome.title": "Get Started with Python Development", "walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", "walkthrough.step.python.createPythonFile.title": "Create a Python file", + "walkthrough.step.python.createPythonFolder.title": "Open a Python project folder", "walkthrough.step.python.createPythonFile.description": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", + "walkthrough.step.python.createPythonFolder.description": "[Open](command:workbench.action.files.openFolder) or create a project folder.\n[Open Project Folder](command:workbench.action.files.openFolder)", "walkthrough.step.python.installPythonWin8.title": "Install Python", "walkthrough.step.python.installPythonWin8.description": "The Python Extension requires Python to be installed. Install Python [from python.org](https://www.python.org/downloads).\n\n[Install Python](https://www.python.org/downloads)\n", "walkthrough.step.python.installPythonMac.title": "Install Python", @@ -140,11 +142,14 @@ "walkthrough.step.python.selectInterpreter.title": "Select a Python Interpreter", "walkthrough.step.python.selectInterpreter.description": "Choose which Python interpreter/environment you want to use for your Python project.\n[Select Python Interpreter](command:python.setInterpreter)\n**Tip**: Run the ``Python: Select Interpreter`` command in the [Command Palette](command:workbench.action.showCommands).", "walkthrough.step.python.createEnvironment.title": "Create a Python Environment ", + "walkthrough.step.python.createEnvironment.title2": "Create or select a Python Environment ", "walkthrough.step.python.createEnvironment.description": "Create an environment for your Python project.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).\n 🔍 Check out our [docs](https://aka.ms/pythonenvs) to learn more.", + "walkthrough.step.python.createEnvironment.description2": "Create an environment for your Python project or use [Select Python Interpreter](command:python.setInterpreter) to select an existing one.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).", "walkthrough.step.python.runAndDebug.title": "Run and debug your Python file", "walkthrough.step.python.runAndDebug.description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", "walkthrough.step.python.learnMoreWithDS.title": "Explore more resources", "walkthrough.step.python.learnMoreWithDS.description": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Learn More](https://aka.ms/AA8dqti)", + "walkthrough.step.python.learnMoreWithDS.description2": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Follow along with the Python Tutorial](https://aka.ms/AA8dqti)", "walkthrough.pythonDataScienceWelcome.title": "Get Started with Python for Data Science", "walkthrough.pythonDataScienceWelcome.description": "Your first steps to getting started with a Data Science project with Python!", "walkthrough.step.python.installJupyterExt.title": "Install Jupyter extension", @@ -164,4 +169,4 @@ "walkthrough.step.python.createNewNotebook.altText": "Creating a new Jupyter notebook", "walkthrough.step.python.openInteractiveWindow.altText": "Opening Python interactive window", "walkthrough.step.python.dataScienceLearnMore.altText": "Image representing our documentation page and mailing list resources." -} +} \ No newline at end of file diff --git a/resources/walkthrough/environments-info.md b/resources/walkthrough/environments-info.md new file mode 100644 index 000000000000..7bdc61a96e2e --- /dev/null +++ b/resources/walkthrough/environments-info.md @@ -0,0 +1,10 @@ +## Python Environments + +Create Environment Dropdown + +Python virtual environments are considered a best practice in Python development. A virtual environment includes a Python interpreter and any packages you have installed into it, such as numpy or Flask. + +After you create a virtual environment using the **Python: Create Environment** command, you can install packages into the environment. +For example, type `python -m pip install numpy` in an activated terminal to install `numpy` into the environment. + +🔍 Check out our [docs](https://aka.ms/pythonenvs) to learn more. From cb859ca0900a03a08a5d73ad9d8e01c0f5e9c52f Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 13 Jun 2023 15:09:55 -0700 Subject: [PATCH 120/336] Revert: Run in dedicated terminal feature due to regressions (#21418) For https://github.com/microsoft/vscode-python/issues/21393 --- package.json | 19 ------- package.nls.json | 1 - src/client/common/constants.ts | 1 - src/client/common/terminal/factory.ts | 21 ++----- src/client/common/terminal/types.ts | 2 +- src/client/telemetry/index.ts | 6 -- .../codeExecution/codeExecutionManager.ts | 56 +++++++------------ .../codeExecution/terminalCodeExecution.ts | 34 +++++------ src/client/terminals/types.ts | 2 +- .../common/terminals/factory.unit.test.ts | 47 +--------------- .../codeExecutionManager.unit.test.ts | 25 +++------ 11 files changed, 55 insertions(+), 159 deletions(-) diff --git a/package.json b/package.json index 8b62067e8347..ce407ed9ee4d 100644 --- a/package.json +++ b/package.json @@ -392,12 +392,6 @@ "icon": "$(play)", "title": "%python.command.python.execInTerminalIcon.title%" }, - { - "category": "Python", - "command": "python.execInDedicatedTerminal", - "icon": "$(play)", - "title": "%python.command.python.execInDedicatedTerminal.title%" - }, { "category": "Python", "command": "python.debugInTerminal", @@ -1728,13 +1722,6 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "false" }, - { - "category": "Python", - "command": "python.execInDedicatedTerminal", - "icon": "$(play)", - "title": "%python.command.python.execInDedicatedTerminal.title%", - "when": "false" - }, { "category": "Python", "command": "python.debugInTerminal", @@ -1893,12 +1880,6 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, - { - "command": "python.execInDedicatedTerminal", - "group": "navigation@0", - "title": "%python.command.python.execInDedicatedTerminal.title%", - "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" - }, { "command": "python.debugInTerminal", "group": "navigation@1", diff --git a/package.nls.json b/package.nls.json index 6c148792ee52..e228e6a19552 100644 --- a/package.nls.json +++ b/package.nls.json @@ -7,7 +7,6 @@ "python.command.python.execInTerminal.title": "Run Python File in Terminal", "python.command.python.debugInTerminal.title": "Debug Python File", "python.command.python.execInTerminalIcon.title": "Run Python File", - "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index bea0ef9e235c..b285667aaa6a 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -44,7 +44,6 @@ export namespace Commands { export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; - export const Exec_In_Separate_Terminal = 'python.execInDedicatedTerminal'; export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index 39cc88c4b024..3855cb6cee3c 100644 --- a/src/client/common/terminal/factory.ts +++ b/src/client/common/terminal/factory.ts @@ -3,7 +3,6 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; -import * as path from 'path'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; @@ -24,17 +23,13 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { ) { this.terminalServices = new Map(); } - public getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService { + public getTerminalService(options: TerminalCreationOptions): ITerminalService { const resource = options?.resource; const title = options?.title; - let terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; + const terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; const interpreter = options?.interpreter; - const id = this.getTerminalId(terminalTitle, resource, interpreter, options.newTerminalPerFile); + const id = this.getTerminalId(terminalTitle, resource, interpreter); if (!this.terminalServices.has(id)) { - if (resource && options.newTerminalPerFile) { - terminalTitle = `${terminalTitle}: ${path.basename(resource.fsPath).replace('.py', '')}`; - } - options.title = terminalTitle; const terminalService = new TerminalService(this.serviceContainer, options); this.terminalServices.set(id, terminalService); } @@ -51,19 +46,13 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { title = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; return new TerminalService(this.serviceContainer, { resource, title }); } - private getTerminalId( - title: string, - resource?: Uri, - interpreter?: PythonEnvironment, - newTerminalPerFile?: boolean, - ): string { + private getTerminalId(title: string, resource?: Uri, interpreter?: PythonEnvironment): string { if (!resource && !interpreter) { return title; } const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource || undefined); - const fileId = resource && newTerminalPerFile ? resource.fsPath : ''; - return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${fileId}`; + return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}`; } } diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index 303188682378..880bf0dd72fb 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -97,7 +97,7 @@ export interface ITerminalServiceFactory { * @returns {ITerminalService} * @memberof ITerminalServiceFactory */ - getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService; + getTerminalService(options: TerminalCreationOptions): ITerminalService; createTerminalService(resource?: Uri, title?: string): ITerminalService; } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 20df3a21eb37..7881ada5653c 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -827,12 +827,6 @@ export interface IEventNamePropertyMapping { * @type {('command' | 'icon')} */ trigger?: 'command' | 'icon'; - /** - * Whether user chose to execute this Python file in a separate terminal or not. - * - * @type {boolean} - */ - newTerminalPerFile?: boolean; }; /** * Telemetry Event sent when user executes code against Django Shell. diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 9f1ba6e90d90..ed671f2846a2 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -36,31 +36,25 @@ export class CodeExecutionManager implements ICodeExecutionManager { } public registerCommands() { - [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon, Commands.Exec_In_Separate_Terminal].forEach( - (cmd) => { - this.disposableRegistry.push( - this.commandManager.registerCommand(cmd as any, async (file: Resource) => { - const interpreterService = this.serviceContainer.get(IInterpreterService); - const interpreter = await interpreterService.getActiveInterpreter(file); - if (!interpreter) { - this.commandManager - .executeCommand(Commands.TriggerEnvironmentSelection, file) - .then(noop, noop); - return; - } - const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; - await this.executeFileInTerminal(file, trigger, { - newTerminalPerFile: cmd === Commands.Exec_In_Separate_Terminal, + [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon].forEach((cmd) => { + this.disposableRegistry.push( + this.commandManager.registerCommand(cmd as any, async (file: Resource) => { + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); + return; + } + const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; + await this.executeFileInTerminal(file, trigger) + .then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); }) - .then(() => { - if (this.shouldTerminalFocusOnStart(file)) - this.commandManager.executeCommand('workbench.action.terminal.focus'); - }) - .catch((ex) => traceError('Failed to execute file in terminal', ex)); - }), - ); - }, - ); + .catch((ex) => traceError('Failed to execute file in terminal', ex)); + }), + ); + }); this.disposableRegistry.push( this.commandManager.registerCommand(Commands.Exec_Selection_In_Terminal as any, async (file: Resource) => { const interpreterService = this.serviceContainer.get(IInterpreterService); @@ -93,16 +87,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { ), ); } - private async executeFileInTerminal( - file: Resource, - trigger: 'command' | 'icon', - options?: { newTerminalPerFile: boolean }, - ): Promise { - sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { - scope: 'file', - trigger, - newTerminalPerFile: options?.newTerminalPerFile, - }); + private async executeFileInTerminal(file: Resource, trigger: 'command' | 'icon') { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'file', trigger }); const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); file = file instanceof Uri ? file : undefined; let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); @@ -124,7 +110,7 @@ export class CodeExecutionManager implements ICodeExecutionManager { } const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); - await executionService.executeFile(fileToExecute, options); + await executionService.executeFile(fileToExecute); } @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index ca7488530f75..9261483b45e1 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -19,6 +19,7 @@ import { ICodeExecutionService } from '../../terminals/types'; export class TerminalCodeExecutionProvider implements ICodeExecutionService { private hasRanOutsideCurrentDrive = false; protected terminalTitle!: string; + private _terminalService!: ITerminalService; private replActive?: Promise; constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @@ -29,13 +30,13 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { @inject(IInterpreterService) protected readonly interpreterService: IInterpreterService, ) {} - public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) { + public async executeFile(file: Uri) { await this.setCwdForFileExecution(file); const { command, args } = await this.getExecuteFileArgs(file, [ file.fsPath.fileToCommandArgumentForPythonExt(), ]); - await this.getTerminalService(file, options).sendCommand(command, args); + await this.getTerminalService(file).sendCommand(command, args); } public async execute(code: string, resource?: Uri): Promise { @@ -47,23 +48,17 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { await this.getTerminalService(resource).sendText(code); } public async initializeRepl(resource?: Uri) { - const terminalService = this.getTerminalService(resource); if (this.replActive && (await this.replActive)) { - await terminalService.show(); + await this._terminalService.show(); return; } this.replActive = new Promise(async (resolve) => { const replCommandArgs = await this.getExecutableInfo(resource); - terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); + await this.getTerminalService(resource).sendCommand(replCommandArgs.command, replCommandArgs.args); // Give python repl time to start before we start sending text. setTimeout(() => resolve(true), 1000); }); - this.disposables.push( - terminalService.onDidCloseTerminal(() => { - this.replActive = undefined; - }), - ); await this.replActive; } @@ -81,12 +76,19 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { return this.getExecutableInfo(resource, executeArgs); } - private getTerminalService(resource?: Uri, options?: { newTerminalPerFile: boolean }): ITerminalService { - return this.terminalServiceFactory.getTerminalService({ - resource, - title: this.terminalTitle, - newTerminalPerFile: options?.newTerminalPerFile, - }); + private getTerminalService(resource?: Uri): ITerminalService { + if (!this._terminalService) { + this._terminalService = this.terminalServiceFactory.getTerminalService({ + resource, + title: this.terminalTitle, + }); + this.disposables.push( + this._terminalService.onDidCloseTerminal(() => { + this.replActive = undefined; + }), + ); + } + return this._terminalService; } private async setCwdForFileExecution(file: Uri) { const pythonSettings = this.configurationService.getSettings(file); diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 47ac16d9e08b..cf31f4ef1dd0 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -8,7 +8,7 @@ export const ICodeExecutionService = Symbol('ICodeExecutionService'); export interface ICodeExecutionService { execute(code: string, resource?: Uri): Promise; - executeFile(file: Uri, options?: { newTerminalPerFile: boolean }): Promise; + executeFile(file: Uri): Promise; initializeRepl(resource?: Uri): Promise; } diff --git a/src/test/common/terminals/factory.unit.test.ts b/src/test/common/terminals/factory.unit.test.ts index 5ad2da8e793a..ef6b7d8f5b0f 100644 --- a/src/test/common/terminals/factory.unit.test.ts +++ b/src/test/common/terminals/factory.unit.test.ts @@ -105,7 +105,7 @@ suite('Terminal Service Factory', () => { expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same'); }); - test('Ensure same terminal is returned when using different resources from the same workspace', () => { + test('Ensure same terminal is returned when using resources from the same workspace', () => { const file1A = Uri.file('1a'); const file2A = Uri.file('2a'); const fileB = Uri.file('b'); @@ -140,49 +140,4 @@ suite('Terminal Service Factory', () => { 'Instances should be different for different workspaces', ); }); - - test('When `newTerminalPerFile` is true, ensure different terminal is returned when using different resources from the same workspace', () => { - const file1A = Uri.file('1a'); - const file2A = Uri.file('2a'); - const fileB = Uri.file('b'); - const workspaceUriA = Uri.file('A'); - const workspaceUriB = Uri.file('B'); - const workspaceFolderA = TypeMoq.Mock.ofType(); - workspaceFolderA.setup((w) => w.uri).returns(() => workspaceUriA); - const workspaceFolderB = TypeMoq.Mock.ofType(); - workspaceFolderB.setup((w) => w.uri).returns(() => workspaceUriB); - - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file1A))) - .returns(() => workspaceFolderA.object); - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file2A))) - .returns(() => workspaceFolderA.object); - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(fileB))) - .returns(() => workspaceFolderB.object); - - const terminalForFile1A = factory.getTerminalService({ - resource: file1A, - newTerminalPerFile: true, - }) as SynchronousTerminalService; - const terminalForFile2A = factory.getTerminalService({ - resource: file2A, - newTerminalPerFile: true, - }) as SynchronousTerminalService; - const terminalForFileB = factory.getTerminalService({ - resource: fileB, - newTerminalPerFile: true, - }) as SynchronousTerminalService; - - const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; - expect(terminalsAreSameForWorkspaceA).to.equal(false, 'Instances are the same for Workspace A'); - - const terminalsForWorkspaceABAreDifferent = - terminalForFile1A.terminalService === terminalForFileB.terminalService; - expect(terminalsForWorkspaceABAreDifferent).to.equal( - false, - 'Instances should be different for different workspaces', - ); - }); }); diff --git a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 30f95c94d217..3676834873a0 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -77,15 +77,12 @@ suite('Terminal - Code Execution Manager', () => { executionManager.registerCommands(); const sorted = registered.sort(); - expect(sorted).to.deep.equal( - [ - Commands.Exec_In_Separate_Terminal, - Commands.Exec_In_Terminal, - Commands.Exec_In_Terminal_Icon, - Commands.Exec_Selection_In_Django_Shell, - Commands.Exec_Selection_In_Terminal, - ].sort(), - ); + expect(sorted).to.deep.equal([ + Commands.Exec_In_Terminal, + Commands.Exec_In_Terminal_Icon, + Commands.Exec_Selection_In_Django_Shell, + Commands.Exec_Selection_In_Terminal, + ]); }); test('Ensure executeFileInterTerminal will do nothing if no file is avialble', async () => { @@ -138,10 +135,7 @@ suite('Terminal - Code Execution Manager', () => { const fileToExecute = Uri.file('x'); await commandHandler!(fileToExecute); helper.verify(async (h) => h.getFileToExecute(), TypeMoq.Times.never()); - executionService.verify( - async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), - TypeMoq.Times.once(), - ); + executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); }); test('Ensure executeFileInterTerminal will use active file', async () => { @@ -170,10 +164,7 @@ suite('Terminal - Code Execution Manager', () => { .returns(() => executionService.object); await commandHandler!(fileToExecute); - executionService.verify( - async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), - TypeMoq.Times.once(), - ); + executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); }); async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { From 625b8609bf29d8c20e6e9ebaa63e9163208aa587 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 13 Jun 2023 15:28:30 -0700 Subject: [PATCH 121/336] Fix build failure (#21425) Closes https://github.com/microsoft/vscode-python/issues/21424 --- build/update_package_file.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build/update_package_file.py b/build/update_package_file.py index b61460b4cc21..f82587ced846 100644 --- a/build/update_package_file.py +++ b/build/update_package_file.py @@ -3,19 +3,20 @@ import json import pathlib -import sys EXT_ROOT = pathlib.Path(__file__).parent.parent PACKAGE_JSON_PATH = EXT_ROOT / "package.json" + def main(package_json: pathlib.Path) -> None: package = json.loads(package_json.read_text(encoding="utf-8")) - package['enableTelemetry'] = True + package["enableTelemetry"] = True # Overwrite package.json with new data add a new-line at the end of the file. package_json.write_text( json.dumps(package, indent=4, ensure_ascii=False) + "\n", encoding="utf-8" ) + if __name__ == "__main__": - main(PACKAGE_JSON_PATH, sys.argv[1:]) + main(PACKAGE_JSON_PATH) From f5acb54d8d4b6f612cf44754073d831d960c1a93 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 14 Jun 2023 01:54:46 -0700 Subject: [PATCH 122/336] Add flag to publish releases on manual pre-release builds (#21429) --- build/azure-pipeline.pre-release.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index 7b13978b8db2..f2e4c2461a6d 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -18,9 +18,16 @@ resources: ref: main endpoint: Monaco +parameters: + - name: publishExtension + displayName: 🚀 Publish Extension + type: boolean + default: false + extends: template: azure-pipelines/extension/pre-release.yml@templates parameters: + publishExtension: ${{ parameters.publishExtension }} l10nSourcePaths: ./src/client buildSteps: - task: NodeTool@0 From e1c50ae8ff0bf35b8492122e312268513e9189ce Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Wed, 14 Jun 2023 11:16:24 -0700 Subject: [PATCH 123/336] 21249 read launch config in workspace file (#21426) Closed: https://github.com/microsoft/vscode-python/issues/21249 --- .../launch.json/launchJsonReader.ts | 13 ++- .../launch.json/launchJsonReader.unit.test.ts | 86 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts diff --git a/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts index 789dda510e37..1d76e3b8cd26 100644 --- a/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts +++ b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts @@ -5,13 +5,19 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import { parse } from 'jsonc-parser'; import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { getConfiguration, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { traceLog } from '../../../../logging'; export async function getConfigurationsForWorkspace(workspace: WorkspaceFolder): Promise { const filename = path.join(workspace.uri.fsPath, '.vscode', 'launch.json'); - if (!(await fs.pathExists(filename))) { - return []; + // Check launch config in the workspace file + const codeWorkspaceConfig = getConfiguration('launch'); + if (!codeWorkspaceConfig.configurations || !Array.isArray(codeWorkspaceConfig.configurations)) { + return []; + } + traceLog(`Using launch configuration in workspace folder.`); + return codeWorkspaceConfig.configurations; } const text = await fs.readFile(filename, 'utf-8'); @@ -23,6 +29,7 @@ export async function getConfigurationsForWorkspace(workspace: WorkspaceFolder): throw Error('Missing field in launch.json: version'); } // We do not bother ensuring each item is a DebugConfiguration... + traceLog(`Using launch configuration in launch.json file.`); return parsed.configurations; } diff --git a/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts new file mode 100644 index 000000000000..8ed19dc254aa --- /dev/null +++ b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { assert } from 'chai'; +import { getConfigurationsForWorkspace } from '../../../../../client/debugger/extension/configuration/launch.json/launchJsonReader'; +import * as vscodeApis from '../../../../../client/common/vscodeApis/workspaceApis'; + +suite('Launch Json Reader', () => { + let pathExistsStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + const workspacePath = 'path/to/workspace'; + const workspaceFolder = { + name: 'workspace', + uri: Uri.file(workspacePath), + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + readFileStub = sinon.stub(fs, 'readFile'); + getConfigurationStub = sinon.stub(vscodeApis, 'getConfiguration'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Return the config in the launch.json file', async () => { + const launchPath = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); + pathExistsStub.withArgs(launchPath).resolves(true); + const launchJson = `{ + "version": "0.1.0", + "configurations": [ + { + "name": "Python: Launch.json", + "type": "python", + "request": "launch", + "purpose": ["debug-test"], + }, + ] + }`; + readFileStub.withArgs(launchPath, 'utf-8').returns(launchJson); + + const config = await getConfigurationsForWorkspace(workspaceFolder); + + assert.deepStrictEqual(config, [ + { + name: 'Python: Launch.json', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ]); + }); + + test('If there is no launch.json return the config in the workspace file', async () => { + getConfigurationStub.withArgs('launch').returns({ + configurations: [ + { + name: 'Python: Workspace File', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ], + }); + + const config = await getConfigurationsForWorkspace(workspaceFolder); + + assert.deepStrictEqual(config, [ + { + name: 'Python: Workspace File', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ]); + }); +}); From 88e2ef57fe684473184642f19ce6b673b471979c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 15 Jun 2023 09:50:27 -0700 Subject: [PATCH 124/336] Fix bug that errors on repeat folder name (#21433) fixes https://github.com/microsoft/vscode-python/issues/21423 fixes so the dictionary that stores the folders created so far in the plugin architecture is using the folder path as the key instead of folder name so the key is always unique. --- .../folder_b/folder_a}/test_nest.py | 0 .../expected_discovery_test_output.py | 55 ++++++++----------- .../expected_execution_test_output.py | 17 +++--- .../tests/pytestadapter/test_discovery.py | 2 +- .../tests/pytestadapter/test_execution.py | 4 +- pythonFiles/vscode_pytest/__init__.py | 8 ++- 6 files changed, 40 insertions(+), 46 deletions(-) rename pythonFiles/tests/pytestadapter/.data/{double_nested_folder/nested_folder_one/nested_folder_two => folder_a/folder_b/folder_a}/test_nest.py (100%) diff --git a/pythonFiles/tests/pytestadapter/.data/double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py b/pythonFiles/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py similarity index 100% rename from pythonFiles/tests/pytestadapter/.data/double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py rename to pythonFiles/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 8b2283029ac7..494d8794ca73 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -315,24 +315,17 @@ } # This is the expected output for the double_nested_folder tests. -# └── double_nested_folder -# └── nested_folder_one -# └── nested_folder_two +# └── folder_a +# └── folder_b +# └── folder_a # └── test_nest.py # └── test_function -double_nested_folder_path = os.fspath(TEST_DATA_PATH / "double_nested_folder") -double_nested_folder_one_path = os.fspath( - TEST_DATA_PATH / "double_nested_folder" / "nested_folder_one" -) -double_nested_folder_two_path = os.fspath( - TEST_DATA_PATH / "double_nested_folder" / "nested_folder_one" / "nested_folder_two" -) -double_nested_test_nest_path = os.fspath( - TEST_DATA_PATH - / "double_nested_folder" - / "nested_folder_one" - / "nested_folder_two" - / "test_nest.py" + +folder_a_path = os.fspath(TEST_DATA_PATH / "folder_a") +folder_b_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b") +folder_a_nested_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a") +test_nest_path = os.fspath( + TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" ) double_nested_folder_expected_output = { "name": ".data", @@ -340,39 +333,39 @@ "type_": "folder", "children": [ { - "name": "double_nested_folder", - "path": double_nested_folder_path, + "name": "folder_a", + "path": folder_a_path, "type_": "folder", - "id_": double_nested_folder_path, + "id_": folder_a_path, "children": [ { - "name": "nested_folder_one", - "path": double_nested_folder_one_path, + "name": "folder_b", + "path": folder_b_path, "type_": "folder", - "id_": double_nested_folder_one_path, + "id_": folder_b_path, "children": [ { - "name": "nested_folder_two", - "path": double_nested_folder_two_path, + "name": "folder_a", + "path": folder_a_nested_path, "type_": "folder", - "id_": double_nested_folder_two_path, + "id_": folder_a_nested_path, "children": [ { "name": "test_nest.py", - "path": double_nested_test_nest_path, + "path": test_nest_path, "type_": "file", - "id_": double_nested_test_nest_path, + "id_": test_nest_path, "children": [ { "name": "test_function", - "path": double_nested_test_nest_path, + "path": test_nest_path, "lineno": find_test_line_number( "test_function", - double_nested_test_nest_path, + test_nest_path, ), "type_": "test", - "id_": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", - "runID": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + "id_": "folder_a/folder_b/folder_a/test_nest.py::test_function", + "runID": "folder_a/folder_b/folder_a/test_nest.py::test_function", } ], } diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index a894403c7d71..abe27ffc79ce 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -191,13 +191,14 @@ } # This is the expected output for the nested_folder tests. -# └── nested_folder_one -# └── nested_folder_two -# └── test_nest.py -# └── test_function: success +# └── folder_a +# └── folder_b +# └── folder_a +# └── test_nest.py +# └── test_function: success double_nested_folder_expected_execution_output = { - "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function": { - "test": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + "folder_a/folder_b/folder_a/test_nest.py::test_function": { + "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", "outcome": "success", "message": None, "traceback": None, @@ -262,8 +263,8 @@ # Will run all tests in the cwd that fit the test file naming pattern. no_test_ids_pytest_execution_expected_output = { - "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function": { - "test": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + "folder_a/folder_b/folder_a/test_nest.py::test_function": { + "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", "outcome": "success", "message": None, "traceback": None, diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index ab7abb508153..8c0f8dbae68c 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -85,7 +85,7 @@ def test_parameterized_error_collect(): expected_discovery_test_output.dual_level_nested_folder_expected_output, ), ( - "double_nested_folder", + "folder_a", expected_discovery_test_output.double_nested_folder_expected_output, ), ( diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index d54e4e758d35..5b2cbddb1644 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -103,9 +103,7 @@ def test_bad_id_error_execution(): expected_execution_test_output.dual_level_nested_folder_execution_expected_output, ), ( - [ - "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function" - ], + ["folder_a/folder_b/folder_a/test_nest.py::test_function"], expected_execution_test_output.double_nested_folder_expected_execution_output, ), ( diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 4f539e4ea01d..f9f147c381a9 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -316,7 +316,7 @@ def build_nested_folders( Keyword arguments: file_module -- the created module for the file we are nesting. file_node -- the file node that we are building the nested folders for. - created_files_folders_dict -- Dictionary of all the folders and files that have been created. + created_files_folders_dict -- Dictionary of all the folders and files that have been created where the key is the path. session -- the pytest session object. """ prev_folder_node = file_node @@ -326,12 +326,14 @@ def build_nested_folders( while iterator_path != session.path: curr_folder_name = iterator_path.name try: - curr_folder_node: TestNode = created_files_folders_dict[curr_folder_name] + curr_folder_node: TestNode = created_files_folders_dict[ + os.fspath(iterator_path) + ] except KeyError: curr_folder_node: TestNode = create_folder_node( curr_folder_name, iterator_path ) - created_files_folders_dict[curr_folder_name] = curr_folder_node + created_files_folders_dict[os.fspath(iterator_path)] = curr_folder_node if prev_folder_node not in curr_folder_node["children"]: curr_folder_node["children"].append(prev_folder_node) iterator_path = iterator_path.parent From 3dc11d2ec54054343d7daf9373852a467a84f354 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Thu, 15 Jun 2023 10:28:08 -0700 Subject: [PATCH 125/336] 20711 use cwd in debug testing (#21437) Closed: #20711 --- src/client/testing/common/debugLauncher.ts | 2 +- .../unittest/testExecutionAdapter.ts | 4 ++-- .../testing/common/debugLauncher.unit.test.ts | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index f7a027ad1304..dcf23c0478db 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -146,7 +146,7 @@ export class DebugLauncher implements ITestDebugLauncher { cfg.console = 'internalConsole'; } if (!cfg.cwd) { - cfg.cwd = workspaceFolder.uri.fsPath; + cfg.cwd = configSettings.testing.cwd || workspaceFolder.uri.fsPath; } if (!cfg.env) { cfg.env = {}; diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index bf83c3c0feb1..b671a64138cb 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -44,10 +44,10 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { public async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { const settings = this.configSettings.getSettings(uri); - const { unittestArgs } = settings.testing; + const { cwd, unittestArgs } = settings.testing; const command = buildExecutionCommand(unittestArgs); - this.cwd = uri.fsPath; + this.cwd = cwd || uri.fsPath; const uuid = this.testServer.createUUID(uri.fsPath); const options: TestCommandOptions = { diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index 41b95c66040e..4712c9b6136a 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -77,7 +77,7 @@ suite('Unit Tests - Debug Launcher', () => { settings = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - unitTestSettings = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + unitTestSettings = TypeMoq.Mock.ofType(); settings.setup((p) => p.testing).returns(() => unitTestSettings.object); debugEnvHelper = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); @@ -333,6 +333,21 @@ suite('Unit Tests - Debug Launcher', () => { debugService.verifyAll(); }); + test('Use cwd value in settings if exist', async () => { + unitTestSettings.setup((p) => p.cwd).returns(() => 'path/to/settings/cwd'); + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + }; + const expected = getDefaultDebugConfig(); + expected.cwd = 'path/to/settings/cwd'; + setupSuccess(options, 'unittest', expected); + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + test('Full debug config', async () => { const options: LaunchOptions = { cwd: 'one/two/three', From 1323a6a02dffb53661f795c721e3009448a1b9e1 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 21 Jun 2023 02:01:18 +0200 Subject: [PATCH 126/336] Engineering - add TSAOptions (#21460) --- build/azure-pipeline.stable.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 50ccbb9fff7a..5c8444de9b2e 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -63,7 +63,7 @@ extends: - script: | python ./build/update_ext_version.py --release --for-publishing displayName: Update build number - + - script: | python ./build/update_package_file.py displayName: Update telemetry in package.json @@ -73,3 +73,16 @@ extends: - script: gulp prePublishBundle displayName: Build + tsa: + enabled: true + options: + codebaseName: 'devdiv_$(Build.Repository.Name)' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + instanceUrl: 'https://devdiv.visualstudio.com/defaultcollection' + projectName: 'DevDiv' + areaPath: "DevDiv\\VS Code (compliance tracking only)\\Visual Studio Code Python Extensions" + notificationAliases: + - 'stbatt@microsoft.com' + - 'lszomoru@microsoft.com' + - 'brcan@microsoft.com' + - 'kanadig@microsoft.com' From 0f232386375b8012cd2dd838183e0e82e4803076 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 21 Jun 2023 10:28:34 -0700 Subject: [PATCH 127/336] Result resolver feature branch (#21457) fixes https://github.com/microsoft/vscode-python/issues/21394 --- pythonFiles/unittestadapter/execution.py | 2 - src/client/testing/common/socketServer.ts | 2 +- .../testController/common/resultResolver.ts | 233 ++++++++ .../testing/testController/common/server.ts | 43 +- .../testing/testController/common/types.ts | 12 +- .../testing/testController/common/utils.ts | 141 ++++- .../testing/testController/controller.ts | 16 +- .../pytest/pytestDiscoveryAdapter.ts | 73 +-- .../pytest/pytestExecutionAdapter.ts | 99 ++-- .../unittest/testDiscoveryAdapter.ts | 36 +- .../unittest/testExecutionAdapter.ts | 94 ++-- .../testController/workspaceTestAdapter.ts | 306 +---------- src/test/mocks/vsc/index.ts | 112 ++++ .../pytestDiscoveryAdapter.unit.test.ts | 123 +++-- .../pytestExecutionAdapter.unit.test.ts | 257 ++++++--- .../resultResolver.unit.test.ts | 368 +++++++++++++ .../testController/server.unit.test.ts | 507 ++++++++++++------ .../testDiscoveryAdapter.unit.test.ts | 62 +-- .../testExecutionAdapter.unit.test.ts | 183 +++---- .../workspaceTestAdapter.unit.test.ts | 404 +++++++++++++- src/test/vscode-mock.ts | 2 + 21 files changed, 2091 insertions(+), 984 deletions(-) create mode 100644 src/client/testing/testController/common/resultResolver.ts create mode 100644 src/test/testing/testController/resultResolver.unit.test.ts diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 4695064396cc..17c125e5843a 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -242,7 +242,6 @@ def run_tests( try: client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client_socket.connect(("localhost", run_test_ids_port_int)) - print(f"CLIENT: Server listening on port {run_test_ids_port_int}...") buffer = b"" while True: @@ -263,7 +262,6 @@ def run_tests( buffer = b"" # Process the JSON data - print(f"Received JSON data: {test_ids_from_buffer}") break except json.JSONDecodeError: # JSON decoding error, the complete JSON object is not yet received diff --git a/src/client/testing/common/socketServer.ts b/src/client/testing/common/socketServer.ts index 554d8c8a0c76..c27bf5a1606c 100644 --- a/src/client/testing/common/socketServer.ts +++ b/src/client/testing/common/socketServer.ts @@ -123,7 +123,7 @@ export class UnitTestSocketServer extends EventEmitter implements IUnitTestSocke if ((socket as any).id) { destroyedSocketId = (socket as any).id; } - this.log('socket disconnected', destroyedSocketId.toString()); + this.log('socket disconnected', destroyedSocketId?.toString()); if (socket && socket.destroy) { socket.destroy(); } diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts new file mode 100644 index 000000000000..49243390ad0f --- /dev/null +++ b/src/client/testing/testController/common/resultResolver.ts @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode'; +import * as util from 'util'; +import { DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; +import { TestProvider } from '../../types'; +import { traceError, traceLog } from '../../../logging'; +import { Testing } from '../../../common/utils/localize'; +import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testItemUtilities'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { splitLines } from '../../../common/stringUtils'; +import { buildErrorNodeOptions, fixLogLines, populateTestTree } from './utils'; + +export class PythonResultResolver implements ITestResultResolver { + testController: TestController; + + testProvider: TestProvider; + + public runIdToTestItem: Map; + + public runIdToVSid: Map; + + public vsIdToRunId: Map; + + constructor(testController: TestController, testProvider: TestProvider, private workspaceUri: Uri) { + this.testController = testController; + this.testProvider = testProvider; + + this.runIdToTestItem = new Map(); + this.runIdToVSid = new Map(); + this.vsIdToRunId = new Map(); + } + + public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise { + const workspacePath = this.workspaceUri.fsPath; + traceLog('Using result resolver for discovery'); + + const rawTestData = payload; + if (!rawTestData) { + // No test data is available + return Promise.resolve(); + } + + // Check if there were any errors in the discovery process. + if (rawTestData.status === 'error') { + const testingErrorConst = + this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; + const { errors } = rawTestData; + traceError(testingErrorConst, '\r\n', errors!.join('\r\n\r\n')); + + let errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); + const message = util.format( + `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, + errors!.join('\r\n\r\n'), + ); + + if (errorNode === undefined) { + const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); + errorNode = createErrorTestItem(this.testController, options); + this.testController.items.add(errorNode); + } + errorNode.error = message; + } else { + // Remove the error node if necessary, + // then parse and insert test data. + this.testController.items.delete(`DiscoveryError:${workspacePath}`); + + if (rawTestData.tests) { + // If the test root for this folder exists: Workspace refresh, update its children. + // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. + populateTestTree(this.testController, rawTestData.tests, undefined, this, token); + } else { + // Delete everything from the test controller. + this.testController.items.replace([]); + } + } + + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { + tool: this.testProvider, + failed: false, + }); + return Promise.resolve(); + } + + public resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise { + const rawTestExecData = payload; + if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { + // Map which holds the subtest information for each test item. + const subTestStats: Map = new Map(); + + // iterate through payload and update the UI accordingly. + for (const keyTemp of Object.keys(rawTestExecData.result)) { + const testCases: TestItem[] = []; + + // grab leaf level test items + this.testController.items.forEach((i) => { + const tempArr: TestItem[] = getTestCaseNodes(i); + testCases.push(...tempArr); + }); + + if ( + rawTestExecData.result[keyTemp].outcome === 'failure' || + rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' + ) { + const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + + const text = `${rawTestExecData.result[keyTemp].test} failed: ${ + rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome + }\r\n${traceback}\r\n`; + const message = new TestMessage(text); + + // note that keyTemp is a runId for unittest library... + const grabVSid = this.runIdToVSid.get(keyTemp); + // search through freshly built array of testItem to find the failed test and update UI. + testCases.forEach((indiItem) => { + if (indiItem.id === grabVSid) { + if (indiItem.uri && indiItem.range) { + message.location = new Location(indiItem.uri, indiItem.range); + runInstance.failed(indiItem, message); + runInstance.appendOutput(fixLogLines(text)); + } + } + }); + } else if ( + rawTestExecData.result[keyTemp].outcome === 'success' || + rawTestExecData.result[keyTemp].outcome === 'expected-failure' + ) { + const grabTestItem = this.runIdToTestItem.get(keyTemp); + const grabVSid = this.runIdToVSid.get(keyTemp); + if (grabTestItem !== undefined) { + testCases.forEach((indiItem) => { + if (indiItem.id === grabVSid) { + if (indiItem.uri && indiItem.range) { + runInstance.passed(grabTestItem); + runInstance.appendOutput('Passed here'); + } + } + }); + } + } else if (rawTestExecData.result[keyTemp].outcome === 'skipped') { + const grabTestItem = this.runIdToTestItem.get(keyTemp); + const grabVSid = this.runIdToVSid.get(keyTemp); + if (grabTestItem !== undefined) { + testCases.forEach((indiItem) => { + if (indiItem.id === grabVSid) { + if (indiItem.uri && indiItem.range) { + runInstance.skipped(grabTestItem); + runInstance.appendOutput('Skipped here'); + } + } + }); + } + } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { + // split on " " since the subtest ID has the parent test ID in the first part of the ID. + const parentTestCaseId = keyTemp.split(' ')[0]; + const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); + const data = rawTestExecData.result[keyTemp]; + // find the subtest's parent test item + if (parentTestItem) { + const subtestStats = subTestStats.get(parentTestCaseId); + if (subtestStats) { + subtestStats.failed += 1; + } else { + subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); + runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); + // clear since subtest items don't persist between runs + clearAllChildren(parentTestItem); + } + const subtestId = keyTemp; + const subTestItem = this.testController?.createTestItem(subtestId, subtestId); + runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); + // create a new test item for the subtest + if (subTestItem) { + const traceback = data.traceback ?? ''; + const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; + runInstance.appendOutput(fixLogLines(text)); + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + const message = new TestMessage(rawTestExecData?.result[keyTemp].message ?? ''); + if (parentTestItem.uri && parentTestItem.range) { + message.location = new Location(parentTestItem.uri, parentTestItem.range); + } + runInstance.failed(subTestItem, message); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { + // split on " " since the subtest ID has the parent test ID in the first part of the ID. + const parentTestCaseId = keyTemp.split(' ')[0]; + const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); + + // find the subtest's parent test item + if (parentTestItem) { + const subtestStats = subTestStats.get(parentTestCaseId); + if (subtestStats) { + subtestStats.passed += 1; + } else { + subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); + runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); + // clear since subtest items don't persist between runs + clearAllChildren(parentTestItem); + } + const subtestId = keyTemp; + const subTestItem = this.testController?.createTestItem(subtestId, subtestId); + // create a new test item for the subtest + if (subTestItem) { + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + runInstance.passed(subTestItem); + runInstance.appendOutput(fixLogLines(`${subtestId} Passed\r\n`)); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } + } + } + return Promise.resolve(); + } +} + +// had to switch the order of the original parameter since required param cannot follow optional. diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 6bd9bf348e20..32829e355ccb 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -9,7 +9,7 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceLog } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; @@ -24,6 +24,10 @@ export class PythonTestServer implements ITestServer, Disposable { private ready: Promise; + private _onRunDataReceived: EventEmitter = new EventEmitter(); + + private _onDiscoveryDataReceived: EventEmitter = new EventEmitter(); + constructor(private executionFactory: IPythonExecutionFactory, private debugLauncher: ITestDebugLauncher) { this.server = net.createServer((socket: net.Socket) => { let buffer: Buffer = Buffer.alloc(0); // Buffer to accumulate received data @@ -48,11 +52,28 @@ export class PythonTestServer implements ITestServer, Disposable { rawData = rpcHeaders.remainingRawData; const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); const extractedData = rpcContent.extractedJSON; + // do not send until we have the full content if (extractedData.length === Number(totalContentLength)) { - // do not send until we have the full content - traceVerbose(`Received data from test server: ${extractedData}`); - this._onDataReceived.fire({ uuid, data: extractedData }); - this.uuids = this.uuids.filter((u) => u !== uuid); + // if the rawData includes tests then this is a discovery request + if (rawData.includes(`"tests":`)) { + this._onDiscoveryDataReceived.fire({ + uuid, + data: rpcContent.extractedJSON, + }); + // if the rawData includes result then this is a run request + } else if (rawData.includes(`"result":`)) { + this._onRunDataReceived.fire({ + uuid, + data: rpcContent.extractedJSON, + }); + } else { + traceLog( + `Error processing test server request: request is not recognized as discovery or run.`, + ); + this._onDataReceived.fire({ uuid: '', data: '' }); + return; + } + // this.uuids = this.uuids.filter((u) => u !== uuid); WHERE DOES THIS GO?? buffer = Buffer.alloc(0); } else { break; @@ -97,6 +118,18 @@ export class PythonTestServer implements ITestServer, Disposable { return uuid; } + public deleteUUID(uuid: string): void { + this.uuids = this.uuids.filter((u) => u !== uuid); + } + + public get onRunDataReceived(): Event { + return this._onRunDataReceived.event; + } + + public get onDiscoveryDataReceived(): Event { + return this._onDiscoveryDataReceived.event; + } + public dispose(): void { this.server.close(); this._onDataReceived.dispose(); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 4307d7a3913f..cb7fda797c4a 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -172,12 +172,21 @@ export type TestCommandOptionsPytest = { */ export interface ITestServer { readonly onDataReceived: Event; + readonly onRunDataReceived: Event; + readonly onDiscoveryDataReceived: Event; sendCommand(options: TestCommandOptions, runTestIdsPort?: string, callback?: () => void): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; + deleteUUID(uuid: string): void; +} +export interface ITestResultResolver { + runIdToVSid: Map; + runIdToTestItem: Map; + vsIdToRunId: Map; + resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise; + resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise; } - export interface ITestDiscoveryAdapter { // ** first line old method signature, second line new method signature discoverTests(uri: Uri): Promise; @@ -192,6 +201,7 @@ export interface ITestExecutionAdapter { uri: Uri, testIds: string[], debugBool?: boolean, + runInstance?: TestRun, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, ): Promise; diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 1bf31e80e11a..f98550d3e72b 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -1,11 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as net from 'net'; -import { traceLog } from '../../../logging'; +import * as path from 'path'; +import { CancellationToken, Position, TestController, TestItem, Uri, Range } from 'vscode'; +import { traceError, traceLog, traceVerbose } from '../../../logging'; import { EnableTestAdapterRewrite } from '../../../common/experiments/groups'; import { IExperimentService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; +import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities'; +import { DiscoveredTestItem, DiscoveredTestNode, ITestResultResolver } from './types'; export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); @@ -62,38 +66,125 @@ export function pythonTestAdapterRewriteEnabled(serviceContainer: IServiceContai return experiment.inExperimentSync(EnableTestAdapterRewrite.experiment); } -export const startServer = (testIds: string): Promise => - new Promise((resolve, reject) => { - const server = net.createServer((socket: net.Socket) => { - // Convert the test_ids array to JSON - const testData = JSON.stringify(testIds); +export async function startTestIdServer(testIds: string[]): Promise { + const startServer = (): Promise => + new Promise((resolve, reject) => { + const server = net.createServer((socket: net.Socket) => { + // Convert the test_ids array to JSON + const testData = JSON.stringify(testIds); - // Create the headers - const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; + // Create the headers + const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; - // Create the payload by concatenating the headers and the test data - const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + // Create the payload by concatenating the headers and the test data + const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; - // Send the payload to the socket - socket.write(payload); + // Send the payload to the socket + socket.write(payload); - // Handle socket events - socket.on('data', (data) => { - traceLog('Received data:', data.toString()); + // Handle socket events + socket.on('data', (data) => { + traceLog('Received data:', data.toString()); + }); + + socket.on('end', () => { + traceLog('Client disconnected'); + }); }); - socket.on('end', () => { - traceLog('Client disconnected'); + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + traceLog(`Server listening on port ${port}`); + resolve(port); }); - }); - server.listen(0, () => { - const { port } = server.address() as net.AddressInfo; - traceLog(`Server listening on port ${port}`); - resolve(port); + server.on('error', (error: Error) => { + reject(error); + }); }); - server.on('error', (error: Error) => { - reject(error); - }); + // Start the server and wait until it is listening + let returnPort = 0; + try { + await startServer() + .then((assignedPort) => { + traceVerbose(`Server started for pytest test ids server and listening on port ${assignedPort}`); + returnPort = assignedPort; + }) + .catch((error) => { + traceError('Error starting server for pytest test ids server:', error); + return 0; + }) + .finally(() => returnPort); + return returnPort; + } catch { + traceError('Error starting server for pytest test ids server, cannot get port.'); + return returnPort; + } +} + +export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { + const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error'; + return { + id: `DiscoveryError:${uri.fsPath}`, + label: `${labelText} [${path.basename(uri.fsPath)}]`, + error: message, + }; +} + +export function populateTestTree( + testController: TestController, + testTreeData: DiscoveredTestNode, + testRoot: TestItem | undefined, + resultResolver: ITestResultResolver, + token?: CancellationToken, +): void { + // If testRoot is undefined, use the info of the root item of testTreeData to create a test item, and append it to the test controller. + if (!testRoot) { + testRoot = testController.createTestItem(testTreeData.path, testTreeData.name, Uri.file(testTreeData.path)); + + testRoot.canResolveChildren = true; + testRoot.tags = [RunTestTag, DebugTestTag]; + + testController.items.add(testRoot); + } + + // Recursively populate the tree with test data. + testTreeData.children.forEach((child) => { + if (!token?.isCancellationRequested) { + if (isTestItem(child)) { + const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); + testItem.tags = [RunTestTag, DebugTestTag]; + + const range = new Range( + new Position(Number(child.lineno) - 1, 0), + new Position(Number(child.lineno), 0), + ); + testItem.canResolveChildren = false; + testItem.range = range; + testItem.tags = [RunTestTag, DebugTestTag]; + + testRoot!.children.add(testItem); + // add to our map + resultResolver.runIdToTestItem.set(child.runID, testItem); + resultResolver.runIdToVSid.set(child.runID, child.id_); + resultResolver.vsIdToRunId.set(child.id_, child.runID); + } else { + let node = testController.items.get(child.path); + + if (!node) { + node = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); + + node.canResolveChildren = true; + node.tags = [RunTestTag, DebugTestTag]; + testRoot!.children.add(node); + } + populateTestTree(testController, child, node, resultResolver, token); + } + } }); +} + +function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem { + return test.type_ === 'test'; +} diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 0d3487855380..eff333a4cdd9 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -31,12 +31,14 @@ import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; import { TestProvider } from '../types'; import { PythonTestServer } from './common/server'; import { DebugTestTag, getNodeByUri, RunTestTag } from './common/testItemUtilities'; +import { pythonTestAdapterRewriteEnabled } from './common/utils'; import { ITestController, ITestDiscoveryAdapter, ITestFrameworkController, TestRefreshOptions, ITestExecutionAdapter, + ITestResultResolver, } from './common/types'; import { UnittestTestDiscoveryAdapter } from './unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from './unittest/testExecutionAdapter'; @@ -44,8 +46,8 @@ import { PytestTestDiscoveryAdapter } from './pytest/pytestDiscoveryAdapter'; import { PytestTestExecutionAdapter } from './pytest/pytestExecutionAdapter'; import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { ITestDebugLauncher } from '../common/types'; -import { pythonTestAdapterRewriteEnabled } from './common/utils'; import { IServiceContainer } from '../../ioc/types'; +import { PythonResultResolver } from './common/resultResolver'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; @@ -161,30 +163,37 @@ export class PythonTestController implements ITestController, IExtensionSingleAc let discoveryAdapter: ITestDiscoveryAdapter; let executionAdapter: ITestExecutionAdapter; let testProvider: TestProvider; + let resultResolver: ITestResultResolver; if (settings.testing.unittestEnabled) { + testProvider = UNITTEST_PROVIDER; + resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); discoveryAdapter = new UnittestTestDiscoveryAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, + resultResolver, ); executionAdapter = new UnittestTestExecutionAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, + resultResolver, ); - testProvider = UNITTEST_PROVIDER; } else { + testProvider = PYTEST_PROVIDER; + resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); discoveryAdapter = new PytestTestDiscoveryAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, + resultResolver, ); executionAdapter = new PytestTestExecutionAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, + resultResolver, ); - testProvider = PYTEST_PROVIDER; } const workspaceTestAdapter = new WorkspaceTestAdapter( @@ -192,6 +201,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc discoveryAdapter, executionAdapter, workspace.uri, + resultResolver, ); this.testAdapters.set(workspace.uri, workspaceTestAdapter); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index aeb920407cd2..4378c68b534c 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -10,8 +10,14 @@ import { import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceError, traceLog, traceVerbose } from '../../../logging'; -import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, ITestServer } from '../common/types'; +import { traceError, traceVerbose } from '../../../logging'; +import { + DataReceivedEvent, + DiscoveredTestPayload, + ITestDiscoveryAdapter, + ITestResultResolver, + ITestServer, +} from '../common/types'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied @@ -19,39 +25,32 @@ import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, ITestS export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { private promiseMap: Map> = new Map(); - private deferred: Deferred | undefined; - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } - - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); - } - } + private readonly resultResolver?: ITestResultResolver, + ) {} - discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { - if (executionFactory !== undefined) { - // ** new version of discover tests. - const settings = this.configSettings.getSettings(uri); - const { pytestArgs } = settings.testing; - traceVerbose(pytestArgs); - return this.runPytestDiscovery(uri, executionFactory); + async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; + traceVerbose(pytestArgs); + const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { + // cancelation token ? + this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + }); + try { + await this.runPytestDiscovery(uri, executionFactory); + } finally { + disposable.dispose(); } - // if executionFactory is undefined, we are using the old method signature of discover tests. - traceVerbose(uri); - this.deferred = createDeferred(); - return this.deferred.promise; + // this is only a placeholder to handle function overloading until rewrite is finished + const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; + return discoveryPayload; } - async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { + async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); @@ -79,13 +78,19 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { allowEnvironmentFetchExceptions: false, resource: uri, }; - const execService = await executionFactory.createActivatedEnvironment(creationOptions); - const discoveryArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); - traceLog(`Discovering pytest tests with arguments: ${discoveryArgs.join(' ')}`); - execService.exec(discoveryArgs, spawnOptions).catch((ex) => { - traceError(`Error occurred while discovering tests: ${ex}`); - deferred.reject(ex as Error); - }); + const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + // delete UUID following entire discovery finishing. + execService + ?.exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) + .then(() => { + this.testServer.deleteUUID(uuid); + return deferred.resolve(); + }) + .catch((err) => { + traceError(`Error while trying to run pytest discovery, \n${err}\r\n\r\n`); + this.testServer.deleteUUID(uuid); + return deferred.reject(err); + }); return deferred.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 90704b5d67f4..1bf032b9b594 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -1,13 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Uri } from 'vscode'; +import { TestRun, Uri } from 'vscode'; import * as path from 'path'; -import * as net from 'net'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; -import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; +import { + DataReceivedEvent, + ExecutionTestPayload, + ITestExecutionAdapter, + ITestResultResolver, + ITestServer, +} from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, IPythonExecutionFactory, @@ -17,6 +22,7 @@ import { removePositionalFoldersAndFiles } from './arguments'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; +import { startTestIdServer } from '../common/utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any // (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; @@ -27,39 +33,35 @@ import { EXTENSION_ROOT_DIR } from '../../../common/constants'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { private promiseMap: Map> = new Map(); - private deferred: Deferred | undefined; - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } - - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); - } - } + private readonly resultResolver?: ITestResultResolver, + ) {} async runTests( uri: Uri, testIds: string[], debugBool?: boolean, + runInstance?: TestRun, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, ): Promise { - if (executionFactory !== undefined) { - // ** new version of run tests. - return this.runTestsNew(uri, testIds, debugBool, executionFactory, debugLauncher); + traceVerbose(uri, testIds, debugBool); + const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + if (runInstance) { + this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + } + }); + try { + await this.runTestsNew(uri, testIds, debugBool, executionFactory, debugLauncher); + } finally { + disposable.dispose(); + // confirm with testing that this gets called (it must clean this up) } - // if executionFactory is undefined, we are using the old method signature of run tests. - this.outputChannel.appendLine('Running tests.'); - this.deferred = createDeferred(); - return this.deferred.promise; + const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + return executionPayload; } private async runTestsNew( @@ -114,49 +116,11 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } + traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); - // create payload with testIds to send to run pytest script - const testData = JSON.stringify(testIds); - const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; - const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; - traceLog(`Running pytest execution for the following test ids: ${testIds}`); - - let pytestRunTestIdsPort: string | undefined; - const startServer = (): Promise => - new Promise((resolve, reject) => { - const server = net.createServer((socket: net.Socket) => { - socket.on('end', () => { - traceVerbose('Client disconnected for pytest test ids server'); - }); - }); - - server.listen(0, () => { - const { port } = server.address() as net.AddressInfo; - traceVerbose(`Server listening on port ${port} for pytest test ids server`); - resolve(port); - }); - - server.on('error', (error: Error) => { - traceError('Error starting server for pytest test ids server:', error); - reject(error); - }); - server.on('connection', (socket: net.Socket) => { - socket.write(payload); - traceVerbose('payload sent for pytest execution', payload); - }); - }); - - // Start the server and wait until it is listening - await startServer() - .then((assignedPort) => { - traceVerbose(`Server started for pytest test ids server and listening on port ${assignedPort}`); - pytestRunTestIdsPort = assignedPort.toString(); - if (spawnOptions.extraVariables) - spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort; - }) - .catch((error) => { - traceError('Error starting server for pytest test ids server:', error); - }); + const pytestRunTestIdsPort = await startTestIdServer(testIds); + if (spawnOptions.extraVariables) + spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); if (debugBool) { const pytestPort = this.testServer.getPort().toString(); @@ -168,7 +132,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testProvider: PYTEST_PROVIDER, pytestPort, pytestUUID, - runTestIdsPort: pytestRunTestIdsPort, + runTestIdsPort: pytestRunTestIdsPort.toString(), }; traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { @@ -187,6 +151,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { return Promise.reject(ex); } - return deferred.promise; + const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + return executionPayload; } } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 9c565af78c08..8d393a8da18d 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -10,11 +10,11 @@ import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, + ITestResultResolver, ITestServer, TestCommandOptions, TestDiscoveryCommand, } from '../common/types'; -import { traceInfo } from '../../../logging'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. @@ -28,17 +28,8 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { public testServer: ITestServer, public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } - - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); - } - } + private readonly resultResolver?: ITestResultResolver, + ) {} public async discoverTests(uri: Uri): Promise { const deferred = createDeferred(); @@ -60,12 +51,23 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { this.promiseMap.set(uuid, deferred); - // Send the test command to the server. - // The server will fire an onDataReceived event once it gets a response. - traceInfo(`Sending discover unittest script to server.`); - this.testServer.sendCommand(options); + const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { + this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + }); + try { + await this.callSendCommand(options); + } finally { + disposable.dispose(); + // confirm with testing that this gets called (it must clean this up) + } + const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; + return discoveryPayload; + } - return deferred.promise; + private async callSendCommand(options: TestCommandOptions): Promise { + await this.testServer.sendCommand(options); + const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; + return discoveryPayload; } } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index b671a64138cb..ca88d3871706 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -2,20 +2,21 @@ // Licensed under the MIT License. import * as path from 'path'; -import { Uri } from 'vscode'; -import * as net from 'net'; +import { TestRun, Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; +import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, + ITestResultResolver, ITestServer, TestCommandOptions, TestExecutionCommand, } from '../common/types'; -import { traceLog, traceError } from '../../../logging'; +import { traceLog } from '../../../logging'; +import { startTestIdServer } from '../common/utils'; /** * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? @@ -30,19 +31,31 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { public testServer: ITestServer, public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } - - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); + private readonly resultResolver?: ITestResultResolver, + ) {} + + public async runTests( + uri: Uri, + testIds: string[], + debugBool?: boolean, + runInstance?: TestRun, + ): Promise { + const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + if (runInstance) { + this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + } + }); + try { + await this.runTestsNew(uri, testIds, debugBool); + } finally { + disposable.dispose(); + // confirm with testing that this gets called (it must clean this up) } + const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + return executionPayload; } - public async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { + private async runTestsNew(uri: Uri, testIds: string[], debugBool?: boolean): Promise { const settings = this.configSettings.getSettings(uri); const { cwd, unittestArgs } = settings.testing; @@ -62,52 +75,17 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const deferred = createDeferred(); this.promiseMap.set(uuid, deferred); + traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); - // create payload with testIds to send to run pytest script - const testData = JSON.stringify(testIds); - const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; - const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; - - let runTestIdsPort: string | undefined; - const startServer = (): Promise => - new Promise((resolve, reject) => { - const server = net.createServer((socket: net.Socket) => { - socket.on('end', () => { - traceLog('Client disconnected'); - }); - }); - - server.listen(0, () => { - const { port } = server.address() as net.AddressInfo; - traceLog(`Server listening on port ${port}`); - resolve(port); - }); - - server.on('error', (error: Error) => { - reject(error); - }); - server.on('connection', (socket: net.Socket) => { - socket.write(payload); - traceLog('payload sent', payload); - }); - }); - - // Start the server and wait until it is listening - await startServer() - .then((assignedPort) => { - traceLog(`Server started and listening on port ${assignedPort}`); - runTestIdsPort = assignedPort.toString(); - // Send test command to server. - // Server fire onDataReceived event once it gets response. - this.testServer.sendCommand(options, runTestIdsPort, () => { - deferred.resolve(); - }); - }) - .catch((error) => { - traceError('Error starting server:', error); - }); + const runTestIdsPort = await startTestIdServer(testIds); - return deferred.promise; + await this.testServer.sendCommand(options, runTestIdsPort.toString(), () => { + // disposable.dispose(); + deferred.resolve(); + }); + // return deferred.promise; + const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + return executionPayload; } } diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 5cba6c193d3c..f3ea0b9f6193 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -1,38 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as path from 'path'; import * as util from 'util'; -import { - CancellationToken, - Position, - Range, - TestController, - TestItem, - TestMessage, - TestRun, - Uri, - Location, -} from 'vscode'; -import { splitLines } from '../../common/stringUtils'; +import { CancellationToken, TestController, TestItem, TestRun, Uri } from 'vscode'; import { createDeferred, Deferred } from '../../common/utils/async'; import { Testing } from '../../common/utils/localize'; -import { traceError, traceVerbose } from '../../logging'; +import { traceError } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { TestProvider } from '../types'; -import { - clearAllChildren, - createErrorTestItem, - DebugTestTag, - ErrorTestItemOptions, - getTestCaseNodes, - RunTestTag, -} from './common/testItemUtilities'; -import { DiscoveredTestItem, DiscoveredTestNode, ITestDiscoveryAdapter, ITestExecutionAdapter } from './common/types'; -import { fixLogLines } from './common/utils'; +import { createErrorTestItem, getTestCaseNodes } from './common/testItemUtilities'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './common/types'; import { IPythonExecutionFactory } from '../../common/process/types'; import { ITestDebugLauncher } from '../common/types'; +import { buildErrorNodeOptions } from './common/utils'; /** * This class exposes a test-provider-agnostic way of discovering tests. @@ -48,22 +29,13 @@ export class WorkspaceTestAdapter { private executing: Deferred | undefined; - runIdToTestItem: Map; - - runIdToVSid: Map; - - vsIdToRunId: Map; - constructor( private testProvider: TestProvider, private discoveryAdapter: ITestDiscoveryAdapter, private executionAdapter: ITestExecutionAdapter, private workspaceUri: Uri, - ) { - this.runIdToTestItem = new Map(); - this.runIdToVSid = new Map(); - this.vsIdToRunId = new Map(); - } + private resultResolver: ITestResultResolver, + ) {} public async executeTests( testController: TestController, @@ -81,7 +53,6 @@ export class WorkspaceTestAdapter { const deferred = createDeferred(); this.executing = deferred; - let rawTestExecData; const testCaseNodes: TestItem[] = []; const testCaseIdsSet = new Set(); try { @@ -93,7 +64,7 @@ export class WorkspaceTestAdapter { // iterate through testItems nodes and fetch their unittest runID to pass in as argument testCaseNodes.forEach((node) => { runInstance.started(node); // do the vscode ui test item start here before runtest - const runId = this.vsIdToRunId.get(node.id); + const runId = this.resultResolver.vsIdToRunId.get(node.id); if (runId) { testCaseIdsSet.add(runId); } @@ -101,16 +72,16 @@ export class WorkspaceTestAdapter { const testCaseIds = Array.from(testCaseIdsSet); // ** execution factory only defined for new rewrite way if (executionFactory !== undefined) { - traceVerbose('executionFactory defined'); - rawTestExecData = await this.executionAdapter.runTests( + await this.executionAdapter.runTests( this.workspaceUri, testCaseIds, debugBool, + runInstance, executionFactory, debugLauncher, ); } else { - rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); + await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); } deferred.resolve(); } catch (ex) { @@ -136,146 +107,6 @@ export class WorkspaceTestAdapter { this.executing = undefined; } - if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { - // Map which holds the subtest information for each test item. - const subTestStats: Map = new Map(); - - // iterate through payload and update the UI accordingly. - for (const keyTemp of Object.keys(rawTestExecData.result)) { - const testCases: TestItem[] = []; - - // grab leaf level test items - testController.items.forEach((i) => { - const tempArr: TestItem[] = getTestCaseNodes(i); - testCases.push(...tempArr); - }); - - if ( - rawTestExecData.result[keyTemp].outcome === 'failure' || - rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' - ) { - const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; - const traceback = splitLines(rawTraceback, { - trim: false, - removeEmptyEntries: true, - }).join('\r\n'); - - const text = `${rawTestExecData.result[keyTemp].test} failed: ${ - rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome - }\r\n${traceback}\r\n`; - const message = new TestMessage(text); - - // note that keyTemp is a runId for unittest library... - const grabVSid = this.runIdToVSid.get(keyTemp); - // search through freshly built array of testItem to find the failed test and update UI. - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - message.location = new Location(indiItem.uri, indiItem.range); - runInstance.failed(indiItem, message); - runInstance.appendOutput(fixLogLines(text)); - } - } - }); - } else if ( - rawTestExecData.result[keyTemp].outcome === 'success' || - rawTestExecData.result[keyTemp].outcome === 'expected-failure' - ) { - const grabTestItem = this.runIdToTestItem.get(keyTemp); - const grabVSid = this.runIdToVSid.get(keyTemp); - if (grabTestItem !== undefined) { - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - runInstance.passed(grabTestItem); - runInstance.appendOutput('Passed here'); - } - } - }); - } - } else if (rawTestExecData.result[keyTemp].outcome === 'skipped') { - const grabTestItem = this.runIdToTestItem.get(keyTemp); - const grabVSid = this.runIdToVSid.get(keyTemp); - if (grabTestItem !== undefined) { - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - runInstance.skipped(grabTestItem); - runInstance.appendOutput('Skipped here'); - } - } - }); - } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - const data = rawTestExecData.result[keyTemp]; - // find the subtest's parent test item - if (parentTestItem) { - const subtestStats = subTestStats.get(parentTestCaseId); - if (subtestStats) { - subtestStats.failed += 1; - } else { - subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); - // clear since subtest items don't persist between runs - clearAllChildren(parentTestItem); - } - const subtestId = keyTemp; - const subTestItem = testController?.createTestItem(subtestId, subtestId); - runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); - // create a new test item for the subtest - if (subTestItem) { - const traceback = data.traceback ?? ''; - const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; - runInstance.appendOutput(fixLogLines(text)); - parentTestItem.children.add(subTestItem); - runInstance.started(subTestItem); - const message = new TestMessage(rawTestExecData?.result[keyTemp].message ?? ''); - if (parentTestItem.uri && parentTestItem.range) { - message.location = new Location(parentTestItem.uri, parentTestItem.range); - } - runInstance.failed(subTestItem, message); - } else { - throw new Error('Unable to create new child node for subtest'); - } - } else { - throw new Error('Parent test item not found'); - } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - - // find the subtest's parent test item - if (parentTestItem) { - const subtestStats = subTestStats.get(parentTestCaseId); - if (subtestStats) { - subtestStats.passed += 1; - } else { - subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); - // clear since subtest items don't persist between runs - clearAllChildren(parentTestItem); - } - const subtestId = keyTemp; - const subTestItem = testController?.createTestItem(subtestId, subtestId); - // create a new test item for the subtest - if (subTestItem) { - parentTestItem.children.add(subTestItem); - runInstance.started(subTestItem); - runInstance.passed(subTestItem); - runInstance.appendOutput(fixLogLines(`${subtestId} Passed\r\n`)); - } else { - throw new Error('Unable to create new child node for subtest'); - } - } else { - throw new Error('Parent test item not found'); - } - } - } - } return Promise.resolve(); } @@ -286,8 +117,6 @@ export class WorkspaceTestAdapter { ): Promise { sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); - const workspacePath = this.workspaceUri.fsPath; - // Discovery is expensive. If it is already running, use the existing promise. if (this.discovering) { return this.discovering.promise; @@ -296,14 +125,12 @@ export class WorkspaceTestAdapter { const deferred = createDeferred(); this.discovering = deferred; - let rawTestData; try { // ** execution factory only defined for new rewrite way if (executionFactory !== undefined) { - traceVerbose('executionFactory defined'); - rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); + await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); } else { - rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); + await this.discoveryAdapter.discoverTests(this.workspaceUri); } deferred.resolve(); } catch (ex) { @@ -324,119 +151,14 @@ export class WorkspaceTestAdapter { const errorNode = createErrorTestItem(testController, options); testController.items.add(errorNode); - deferred.reject(ex as Error); + return deferred.reject(ex as Error); } finally { // Discovery has finished running, we have the data, // we don't need the deferred promise anymore. this.discovering = undefined; } - if (!rawTestData) { - // No test data is available - return Promise.resolve(); - } - - // Check if there were any errors in the discovery process. - if (rawTestData.status === 'error') { - const testingErrorConst = - this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; - const { errors } = rawTestData; - traceError(testingErrorConst, '\r\n', errors?.join('\r\n\r\n')); - let errorNode = testController.items.get(`DiscoveryError:${workspacePath}`); - const message = util.format( - `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, - errors?.join('\r\n\r\n'), - ); - - if (errorNode === undefined) { - const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); - errorNode = createErrorTestItem(testController, options); - testController.items.add(errorNode); - } - errorNode.error = message; - } else { - // Remove the error node if necessary, - // then parse and insert test data. - testController.items.delete(`DiscoveryError:${workspacePath}`); - - if (rawTestData.tests) { - // If the test root for this folder exists: Workspace refresh, update its children. - // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. - populateTestTree(testController, rawTestData.tests, undefined, this, token); - } else { - // Delete everything from the test controller. - testController.items.replace([]); - } - } - sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: false }); return Promise.resolve(); } } - -function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem { - return test.type_ === 'test'; -} - -// had to switch the order of the original parameter since required param cannot follow optional. -function populateTestTree( - testController: TestController, - testTreeData: DiscoveredTestNode, - testRoot: TestItem | undefined, - wstAdapter: WorkspaceTestAdapter, - token?: CancellationToken, -): void { - // If testRoot is undefined, use the info of the root item of testTreeData to create a test item, and append it to the test controller. - if (!testRoot) { - testRoot = testController.createTestItem(testTreeData.path, testTreeData.name, Uri.file(testTreeData.path)); - - testRoot.canResolveChildren = true; - testRoot.tags = [RunTestTag, DebugTestTag]; - - testController.items.add(testRoot); - } - - // Recursively populate the tree with test data. - testTreeData.children.forEach((child) => { - if (!token?.isCancellationRequested) { - if (isTestItem(child)) { - const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); - testItem.tags = [RunTestTag, DebugTestTag]; - - const range = new Range( - new Position(Number(child.lineno) - 1, 0), - new Position(Number(child.lineno), 0), - ); - testItem.canResolveChildren = false; - testItem.range = range; - testItem.tags = [RunTestTag, DebugTestTag]; - - testRoot!.children.add(testItem); - // add to our map - wstAdapter.runIdToTestItem.set(child.runID, testItem); - wstAdapter.runIdToVSid.set(child.runID, child.id_); - wstAdapter.vsIdToRunId.set(child.id_, child.runID); - } else { - let node = testController.items.get(child.path); - - if (!node) { - node = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); - - node.canResolveChildren = true; - node.tags = [RunTestTag, DebugTestTag]; - testRoot!.children.add(node); - } - populateTestTree(testController, child, node, wstAdapter, token); - } - } - }); -} - -function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { - const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error'; - return { - id: `DiscoveryError:${uri.fsPath}`, - label: `${labelText} [${path.basename(uri.fsPath)}]`, - error: message, - }; -} diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index 89f4ab1a2d07..092fc67da6c6 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -6,6 +6,7 @@ import { EventEmitter as NodeEventEmitter } from 'events'; import * as vscode from 'vscode'; + // export * from './range'; // export * from './position'; // export * from './selection'; @@ -443,3 +444,114 @@ export enum LogLevel { */ Error = 5, } + +export class TestMessage { + /** + * Human-readable message text to display. + */ + message: string | MarkdownString; + + /** + * Expected test output. If given with {@link TestMessage.actualOutput actualOutput }, a diff view will be shown. + */ + expectedOutput?: string; + + /** + * Actual test output. If given with {@link TestMessage.expectedOutput expectedOutput }, a diff view will be shown. + */ + actualOutput?: string; + + /** + * Associated file location. + */ + location?: vscode.Location; + + /** + * Creates a new TestMessage that will present as a diff in the editor. + * @param message Message to display to the user. + * @param expected Expected output. + * @param actual Actual output. + */ + static diff(message: string | MarkdownString, expected: string, actual: string): TestMessage { + const testMessage = new TestMessage(message); + testMessage.expectedOutput = expected; + testMessage.actualOutput = actual; + return testMessage; + } + + /** + * Creates a new TestMessage instance. + * @param message The message to show to the user. + */ + constructor(message: string | MarkdownString) { + this.message = message; + } +} + +export interface TestItemCollection extends Iterable<[string, vscode.TestItem]> { + /** + * Gets the number of items in the collection. + */ + readonly size: number; + + /** + * Replaces the items stored by the collection. + * @param items Items to store. + */ + replace(items: readonly vscode.TestItem[]): void; + + /** + * Iterate over each entry in this collection. + * + * @param callback Function to execute for each entry. + * @param thisArg The `this` context used when invoking the handler function. + */ + forEach(callback: (item: vscode.TestItem, collection: TestItemCollection) => unknown, thisArg?: unknown): void; + + /** + * Adds the test item to the children. If an item with the same ID already + * exists, it'll be replaced. + * @param item Item to add. + */ + add(item: vscode.TestItem): void; + + /** + * Removes a single test item from the collection. + * @param itemId Item ID to delete. + */ + delete(itemId: string): void; + + /** + * Efficiently gets a test item by ID, if it exists, in the children. + * @param itemId Item ID to get. + * @returns The found item or undefined if it does not exist. + */ + get(itemId: string): vscode.TestItem | undefined; +} + +/** + * Represents a location inside a resource, such as a line + * inside a text file. + */ +export class Location { + /** + * The resource identifier of this location. + */ + uri: vscode.Uri; + + /** + * The document range of this location. + */ + range: vscode.Range; + + /** + * Creates a new location object. + * + * @param uri The resource identifier. + * @param rangeOrPosition The range or position. Positions will be converted to an empty range. + */ + constructor(uri: vscode.Uri, rangeOrPosition: vscode.Range) { + this.uri = uri; + this.range = rangeOrPosition; + } +} diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 12c79a23c7fd..0286235be1bf 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -4,11 +4,17 @@ import * as assert from 'assert'; import { Uri } from 'vscode'; import * as typeMoq from 'typemoq'; +import * as path from 'path'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; -import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; -import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; +import { ITestServer } from '../../../../client/testing/testController/common/types'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + SpawnOptions, +} from '../../../../client/common/process/types'; import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; suite('pytest test discovery adapter', () => { let testServer: typeMoq.IMock; @@ -18,27 +24,56 @@ suite('pytest test discovery adapter', () => { let execService: typeMoq.IMock; let deferred: Deferred; let outputChannel: typeMoq.IMock; + let portNum: number; + let uuid: string; + let expectedPath: string; + let uri: Uri; + let expectedExtraVariables: Record; setup(() => { + const mockExtensionRootDir = typeMoq.Mock.ofType(); + mockExtensionRootDir.setup((m) => m.toString()).returns(() => '/mocked/extension/root/dir'); + + // constants + portNum = 12345; + uuid = 'uuid123'; + expectedPath = path.join('/', 'my', 'test', 'path'); + uri = Uri.file(expectedPath); + const relativePathToPytest = 'pythonFiles'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + expectedExtraVariables = { + PYTHONPATH: fullPluginPath, + TEST_UUID: uuid, + TEST_PORT: portNum.toString(), + }; + + // set up test server testServer = typeMoq.Mock.ofType(); - testServer.setup((t) => t.getPort()).returns(() => 12345); + testServer.setup((t) => t.getPort()).returns(() => portNum); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); testServer - .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ }, })); + + // set up config service configService = ({ getSettings: () => ({ testing: { pytestArgs: ['.'] }, }), } as unknown) as IConfigurationService; + + // set up exec factory execFactory = typeMoq.Mock.ofType(); - execService = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => Promise.resolve(execService.object)); + + // set up exec service + execService = typeMoq.Mock.ofType(); deferred = createDeferred(); execService .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -46,49 +81,51 @@ suite('pytest test discovery adapter', () => { deferred.resolve(); return Promise.resolve({ stdout: '{}' }); }); - execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); outputChannel = typeMoq.Mock.ofType(); }); - test('onDataReceivedHandler should parse only if known UUID', async () => { - const uri = Uri.file('/my/test/path/'); - const uuid = 'uuid123'; - const data = { status: 'success' }; - testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); - const eventData: DataReceivedEvent = { - uuid, - data: JSON.stringify(data), - }; - + test('Discovery should call exec with correct basic args', async () => { adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - const promise = adapter.discoverTests(uri, execFactory.object); - // const promise = adapter.discoverTests(uri); - await deferred.promise; - adapter.onDataReceivedHandler(eventData); - const result = await promise; - assert.deepStrictEqual(result, data); + await adapter.discoverTests(uri, execFactory.object); + const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; + + execService.verify( + (x) => + x.exec( + expectedArgs, + typeMoq.It.is((options) => { + assert.deepEqual(options.extraVariables, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); }); - test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { - const uri = Uri.file('/my/test/path/'); - const uuid = 'uuid456'; - let data = { status: 'error' }; - testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); - const wrongUriEventData: DataReceivedEvent = { - uuid: 'incorrect-uuid456', - data: JSON.stringify(data), - }; - adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - const promise = adapter.discoverTests(uri, execFactory.object); - // const promise = adapter.discoverTests(uri); - adapter.onDataReceivedHandler(wrongUriEventData); + test('Test discovery correctly pulls pytest args from config service settings', async () => { + // set up a config service with different pytest args + const configServiceNew: IConfigurationService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.', 'abc', 'xyz'] }, + }), + } as unknown) as IConfigurationService; - data = { status: 'success' }; - const correctUriEventData: DataReceivedEvent = { - uuid, - data: JSON.stringify(data), - }; - adapter.onDataReceivedHandler(correctUriEventData); - const result = await promise; - assert.deepStrictEqual(result, data); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configServiceNew, outputChannel.object); + await adapter.discoverTests(uri, execFactory.object); + const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.', 'abc', 'xyz']; + execService.verify( + (x) => + x.exec( + expectedArgs, + typeMoq.It.is((options) => { + assert.deepEqual(options.extraVariables, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); }); }); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index ac6c6bd274a4..9f4c41ba8309 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -1,90 +1,173 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. -// import * as assert from 'assert'; -// import { Uri } from 'vscode'; -// import * as typeMoq from 'typemoq'; -// import { IConfigurationService } from '../../../../client/common/types'; -// import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; -// import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; -// import { createDeferred, Deferred } from '../../../../client/common/utils/async'; -// import { PytestTestExecutionAdapter } from '../../../../client/testing/testController/pytest/pytestExecutionAdapter'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as assert from 'assert'; +import { TestRun, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import { ITestServer } from '../../../../client/testing/testController/common/types'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + SpawnOptions, +} from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +import { PytestTestExecutionAdapter } from '../../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; +import * as util from '../../../../client/testing/testController/common/utils'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -// suite('pytest test execution adapter', () => { -// let testServer: typeMoq.IMock; -// let configService: IConfigurationService; -// let execFactory = typeMoq.Mock.ofType(); -// let adapter: PytestTestExecutionAdapter; -// let execService: typeMoq.IMock; -// let deferred: Deferred; -// setup(() => { -// testServer = typeMoq.Mock.ofType(); -// testServer.setup((t) => t.getPort()).returns(() => 12345); -// testServer -// .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) -// .returns(() => ({ -// dispose: () => { -// /* no-body */ -// }, -// })); -// configService = ({ -// getSettings: () => ({ -// testing: { pytestArgs: ['.'] }, -// }), -// isTestExecution: () => false, -// } as unknown) as IConfigurationService; -// execFactory = typeMoq.Mock.ofType(); -// execService = typeMoq.Mock.ofType(); -// execFactory -// .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) -// .returns(() => Promise.resolve(execService.object)); -// deferred = createDeferred(); -// execService -// .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) -// .returns(() => { -// deferred.resolve(); -// return Promise.resolve({ stdout: '{}' }); -// }); -// execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); -// execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); -// }); -// test('onDataReceivedHandler should parse only if known UUID', async () => { -// const uri = Uri.file('/my/test/path/'); -// const uuid = 'uuid123'; -// const data = { status: 'success' }; -// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); -// const eventData: DataReceivedEvent = { -// uuid, -// data: JSON.stringify(data), -// }; +suite('pytest test execution adapter', () => { + let testServer: typeMoq.IMock; + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType(); + let adapter: PytestTestExecutionAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let debugLauncher: typeMoq.IMock; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let startTestIdServerStub: sinon.SinonStub; -// adapter = new PytestTestExecutionAdapter(testServer.object, configService); -// const promise = adapter.runTests(uri, [], false); -// await deferred.promise; -// adapter.onDataReceivedHandler(eventData); -// const result = await promise; -// assert.deepStrictEqual(result, data); -// }); -// test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { -// const uri = Uri.file('/my/test/path/'); -// const uuid = 'uuid456'; -// let data = { status: 'error' }; -// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); -// const wrongUriEventData: DataReceivedEvent = { -// uuid: 'incorrect-uuid456', -// data: JSON.stringify(data), -// }; -// adapter = new PytestTestExecutionAdapter(testServer.object, configService); -// const promise = adapter.runTests(uri, [], false); -// adapter.onDataReceivedHandler(wrongUriEventData); + setup(() => { + testServer = typeMoq.Mock.ofType(); + testServer.setup((t) => t.getPort()).returns(() => 12345); + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + execFactory = typeMoq.Mock.ofType(); + execService = typeMoq.Mock.ofType(); + debugLauncher = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + debugLauncher + .setup((d) => d.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(); + }); + startTestIdServerStub = sinon.stub(util, 'startTestIdServer').returns(Promise.resolve(54321)); -// data = { status: 'success' }; -// const correctUriEventData: DataReceivedEvent = { -// uuid, -// data: JSON.stringify(data), -// }; -// adapter.onDataReceivedHandler(correctUriEventData); -// const result = await promise; -// assert.deepStrictEqual(result, data); -// }); -// }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + }); + teardown(() => { + sinon.restore(); + }); + test('startTestIdServer called with correct testIds', async () => { + const uri = Uri.file(myTestPath); + const uuid = 'uuid123'; + testServer + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); + adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); + + const testIds = ['test1id', 'test2id']; + await adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); + + sinon.assert.calledWithExactly(startTestIdServerStub, testIds); + }); + test('pytest execution called with correct args', async () => { + const uri = Uri.file(myTestPath); + const uuid = 'uuid123'; + testServer + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); + adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); + await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const expectedArgs = [pathToPythonScript, '--rootdir', myTestPath]; + const expectedExtraVariables = { + PYTHONPATH: pathToPythonFiles, + TEST_UUID: 'uuid123', + TEST_PORT: '12345', + }; + // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); + execService.verify( + (x) => + x.exec( + expectedArgs, + typeMoq.It.is((options) => { + assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.extraVariables?.TEST_UUID, expectedExtraVariables.TEST_UUID); + assert.equal(options.extraVariables?.TEST_PORT, expectedExtraVariables.TEST_PORT); + assert.equal(options.extraVariables?.RUN_TEST_IDS_PORT, '54321'); + assert.equal(options.cwd, uri.fsPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Debug launched correctly for pytest', async () => { + const uri = Uri.file(myTestPath); + const uuid = 'uuid123'; + testServer + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); + adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); + await adapter.runTests(uri, [], true, testRun.object, execFactory.object, debugLauncher.object); + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is((launchOptions) => { + assert.equal(launchOptions.cwd, uri.fsPath); + assert.deepEqual(launchOptions.args, ['--rootdir', myTestPath, '--capture', 'no']); + assert.equal(launchOptions.testProvider, 'pytest'); + assert.equal(launchOptions.pytestPort, '12345'); + assert.equal(launchOptions.pytestUUID, 'uuid123'); + assert.strictEqual(launchOptions.runTestIdsPort, '54321'); + return true; + }), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); +}); diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts new file mode 100644 index 000000000000..57b321c2e36c --- /dev/null +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -0,0 +1,368 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, Uri, TestItem, CancellationToken, TestRun, TestItemCollection, Range } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as sinon from 'sinon'; +import { TestProvider } from '../../../client/testing/types'; +import { + DiscoveredTestNode, + DiscoveredTestPayload, + ExecutionTestPayload, +} from '../../../client/testing/testController/common/types'; +import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; +import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; +import * as util from '../../../client/testing/testController/common/utils'; + +suite('Result Resolver tests', () => { + suite('Test discovery', () => { + let resultResolver: ResultResolver.PythonResultResolver; + let testController: TestController; + const log: string[] = []; + let workspaceUri: Uri; + let testProvider: TestProvider; + let defaultErrorMessage: string; + let blankTestItem: TestItem; + let cancelationToken: CancellationToken; + + setup(() => { + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + + dispose: () => { + // empty + }, + } as unknown) as TestController; + defaultErrorMessage = 'pytest test discovery error (see Output > Python)'; + blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + }); + teardown(() => { + sinon.restore(); + }); + + test('resolveDiscovery calls populate test tree correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); + + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // header of populateTestTree is (testController: TestController, testTreeData: DiscoveredTestNode, testRoot: TestItem | undefined, resultResolver: ITestResultResolver, token?: CancellationToken) + sinon.assert.calledWithMatch( + populateTestTreeStub, + testController, // testController + tests, // testTreeData + undefined, // testRoot + resultResolver, // resultResolver + cancelationToken, // token + ); + }); + // what about if the error node already exists: this.testController.items.get(`DiscoveryError:${workspacePath}`); + test('resolveDiscovery should create error node on error with correct params', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // stub out return values of functions called in resolveDiscovery + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + errors: [errorMessage], + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // call resolve discovery + resultResolver.resolveDiscovery(payload); + + // assert the stub functions were called with the correct parameters + + // header of buildErrorNodeOptions is (uri: Uri, message: string, testType: string) + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // header of createErrorTestItem is (options: ErrorTestItemOptions, testController: TestController, uri: Uri) + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + }); + }); + suite('Test execution result resolver', () => { + let resultResolver: ResultResolver.PythonResultResolver; + const log: string[] = []; + let workspaceUri: Uri; + let testProvider: TestProvider; + let cancelationToken: CancellationToken; + let runInstance: typemoq.IMock; + let testControllerMock: typemoq.IMock; + let mockTestItem1: TestItem; + let mockTestItem2: TestItem; + + setup(() => { + // create mock test items + mockTestItem1 = createMockTestItem('mockTestItem1'); + mockTestItem2 = createMockTestItem('mockTestItem2'); + + // create mock testItems to pass into a iterable + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + // create mock testItemCollection + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + + // create mock testController + testControllerMock = typemoq.Mock.ofType(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + + // define functions within runInstance + runInstance = typemoq.Mock.ofType(); + runInstance.setup((r) => r.name).returns(() => 'name'); + runInstance.setup((r) => r.token).returns(() => cancelationToken); + runInstance.setup((r) => r.isPersisted).returns(() => true); + runInstance + .setup((r) => r.enqueued(typemoq.It.isAny())) + .returns(() => { + // empty + log.push('enqueue'); + return undefined; + }); + runInstance + .setup((r) => r.started(typemoq.It.isAny())) + .returns(() => { + // empty + log.push('start'); + }); + + // mock getTestCaseNodes to just return the given testNode added + sinon.stub(testItemUtilities, 'getTestCaseNodes').callsFake((testNode: TestItem) => [testNode]); + }); + teardown(() => { + sinon.restore(); + }); + test('resolveExecution handles failed tests correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'failure', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles skipped correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'skipped', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles success correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles error correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + + const errorPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: 'error', + }; + + resultResolver.resolveExecution(errorPayload, runInstance.object); + + // verify that none of these functions are called + + runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.never()); + runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.never()); + }); + }); +}); + +function createMockTestItem(id: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockTestItem = ({ + id, + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + range, + uri: Uri.file('/foo/bar'), + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index d7b3a242ee9a..38b71992aefb 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -1,158 +1,349 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import * as net from 'net'; -import * as sinon from 'sinon'; -import * as crypto from 'crypto'; -import { OutputChannel, Uri } from 'vscode'; -import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; -import { PythonTestServer } from '../../../client/testing/testController/common/server'; -import { ITestDebugLauncher } from '../../../client/testing/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; - -suite('Python Test Server', () => { - const fakeUuid = 'fake-uuid'; - - let stubExecutionFactory: IPythonExecutionFactory; - let stubExecutionService: IPythonExecutionService; - let server: PythonTestServer; - let sandbox: sinon.SinonSandbox; - let execArgs: string[]; - let v4Stub: sinon.SinonStub; - let debugLauncher: ITestDebugLauncher; - - setup(() => { - sandbox = sinon.createSandbox(); - v4Stub = sandbox.stub(crypto, 'randomUUID'); - - v4Stub.returns(fakeUuid); - stubExecutionService = ({ - exec: (args: string[]) => { - execArgs = args; - return Promise.resolve({ stdout: '', stderr: '' }); - }, - } as unknown) as IPythonExecutionService; - - stubExecutionFactory = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService), - } as unknown) as IPythonExecutionFactory; - }); - - teardown(() => { - sandbox.restore(); - execArgs = []; - server.dispose(); - }); - - test('sendCommand should add the port to the command being sent', async () => { - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - await server.sendCommand(options); - const port = server.getPort(); - - assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); - }); - - test('sendCommand should write to an output channel if it is provided as an option', async () => { - const output: string[] = []; - const outChannel = { - appendLine: (str: string) => { - output.push(str); - }, - } as OutputChannel; - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - outChannel, - }; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - await server.sendCommand(options); - - const port = server.getPort(); - const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); - - assert.deepStrictEqual(output, [expected]); - }); - - test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { - let eventData: { status: string; errors: string[] }; - stubExecutionService = ({ - exec: () => { - throw new Error('Failed to execute'); - }, - } as unknown) as IPythonExecutionService; - - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - server.onDataReceived(({ data }) => { - eventData = JSON.parse(data); - }); - - await server.sendCommand(options); - - assert.deepStrictEqual(eventData!.status, 'error'); - assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); - }); - - test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const deferred = createDeferred(); - - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - stubExecutionService = ({ - exec: async () => { - client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); - }, - } as unknown) as IPythonExecutionService; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('malformed data'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - await server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, ''); - }); -}); +// // Copyright (c) Microsoft Corporation. All rights reserved. +// // Licensed under the MIT License. + +// import * as assert from 'assert'; +// import * as net from 'net'; +// import * as sinon from 'sinon'; +// import * as crypto from 'crypto'; +// import { Uri } from 'vscode'; +// import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; +// import { PythonTestServer } from '../../../client/testing/testController/common/server'; +// import { ITestDebugLauncher } from '../../../client/testing/common/types'; +// import { createDeferred } from '../../../client/common/utils/async'; + +// suite('Python Test Server', () => { +// const fakeUuid = 'fake-uuid'; + +// let stubExecutionFactory: IPythonExecutionFactory; +// let stubExecutionService: IPythonExecutionService; +// let server: PythonTestServer; +// let sandbox: sinon.SinonSandbox; +// let execArgs: string[]; +// let v4Stub: sinon.SinonStub; +// let debugLauncher: ITestDebugLauncher; + +// setup(() => { +// sandbox = sinon.createSandbox(); +// v4Stub = sandbox.stub(crypto, 'randomUUID'); + +// v4Stub.returns(fakeUuid); +// stubExecutionService = ({ +// exec: (args: string[]) => { +// execArgs = args; +// return Promise.resolve({ stdout: '', stderr: '' }); +// }, +// } as unknown) as IPythonExecutionService; + +// stubExecutionFactory = ({ +// createActivatedEnvironment: () => Promise.resolve(stubExecutionService), +// } as unknown) as IPythonExecutionFactory; +// }); + +// teardown(() => { +// sandbox.restore(); +// execArgs = []; +// server.dispose(); +// }); + +// // test('sendCommand should add the port to the command being sent', async () => { +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // }; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); + +// // await server.sendCommand(options); +// // const port = server.getPort(); + +// // assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); +// // }); + +// // test('sendCommand should write to an output channel if it is provided as an option', async () => { +// // const output: string[] = []; +// // const outChannel = { +// // appendLine: (str: string) => { +// // output.push(str); +// // }, +// // } as OutputChannel; +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // outChannel, +// // }; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); + +// // await server.sendCommand(options); + +// // const port = server.getPort(); +// // const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); + +// // assert.deepStrictEqual(output, [expected]); +// // }); + +// // test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { +// // let eventData: { status: string; errors: string[] }; +// // stubExecutionService = ({ +// // exec: () => { +// // throw new Error('Failed to execute'); +// // }, +// // } as unknown) as IPythonExecutionService; + +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // }; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); + +// // server.onDataReceived(({ data }) => { +// // eventData = JSON.parse(data); +// // }); + +// // await server.sendCommand(options); + +// // assert.deepStrictEqual(eventData!.status, 'error'); +// // assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); +// // }); + +// // test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { +// // let eventData: string | undefined; +// // const client = new net.Socket(); +// // const deferred = createDeferred(); + +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // }; + +// // stubExecutionService = ({ +// // exec: async () => { +// // client.connect(server.getPort()); +// // return Promise.resolve({ stdout: '', stderr: '' }); +// // }, +// // } as unknown) as IPythonExecutionService; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); +// // server.onDataReceived(({ data }) => { +// // eventData = data; +// // deferred.resolve(); +// // }); + +// // client.on('connect', () => { +// // console.log('Socket connected, local port:', client.localPort); +// // client.write('malformed data'); +// // client.end(); +// // }); +// // client.on('error', (error) => { +// // console.log('Socket connection error:', error); +// // }); + +// // await server.sendCommand(options); +// // await deferred.promise; +// // assert.deepStrictEqual(eventData, ''); +// // }); + +// // test('If the server doesnt recognize the UUID it should ignore it', async () => { +// // let eventData: string | undefined; +// // const client = new net.Socket(); +// // const deferred = createDeferred(); + +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // }; + +// // stubExecutionService = ({ +// // exec: async () => { +// // client.connect(server.getPort()); +// // return Promise.resolve({ stdout: '', stderr: '' }); +// // }, +// // } as unknown) as IPythonExecutionService; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); +// // server.onDataReceived(({ data }) => { +// // eventData = data; +// // deferred.resolve(); +// // }); + +// // client.on('connect', () => { +// // console.log('Socket connected, local port:', client.localPort); +// // client.write('{"Request-uuid": "unknown-uuid"}'); +// // client.end(); +// // }); +// // client.on('error', (error) => { +// // console.log('Socket connection error:', error); +// // }); + +// // await server.sendCommand(options); +// // await deferred.promise; +// // assert.deepStrictEqual(eventData, ''); +// // }); + +// // required to have "tests" or "results" +// // the heading length not being equal and yes being equal +// // multiple payloads +// // test('Error if payload does not have a content length header', async () => { +// // let eventData: string | undefined; +// // const client = new net.Socket(); +// // const deferred = createDeferred(); + +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // }; + +// // stubExecutionService = ({ +// // exec: async () => { +// // client.connect(server.getPort()); +// // return Promise.resolve({ stdout: '', stderr: '' }); +// // }, +// // } as unknown) as IPythonExecutionService; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); +// // server.onDataReceived(({ data }) => { +// // eventData = data; +// // deferred.resolve(); +// // }); + +// // client.on('connect', () => { +// // console.log('Socket connected, local port:', client.localPort); +// // client.write('{"not content length": "5"}'); +// // client.end(); +// // }); +// // client.on('error', (error) => { +// // console.log('Socket connection error:', error); +// // }); + +// // await server.sendCommand(options); +// // await deferred.promise; +// // assert.deepStrictEqual(eventData, ''); +// // }); + +// const testData = [ +// { +// testName: 'fires discovery correctly on test payload', +// payload: `Content-Length: 52 +// Content-Type: application/json +// Request-uuid: UUID_HERE + +// {"cwd": "path", "status": "success", "tests": "xyz"}`, +// expectedResult: '{"cwd": "path", "status": "success", "tests": "xyz"}', +// }, +// // Add more test data as needed +// ]; + +// testData.forEach(({ testName, payload, expectedResult }) => { +// test(`test: ${testName}`, async () => { +// // Your test logic here +// let eventData: string | undefined; +// const client = new net.Socket(); +// const deferred = createDeferred(); + +// const options = { +// command: { script: 'myscript', args: ['-foo', 'foo'] }, +// workspaceFolder: Uri.file('/foo/bar'), +// cwd: '/foo/bar', +// uuid: fakeUuid, +// }; + +// stubExecutionService = ({ +// exec: async () => { +// client.connect(server.getPort()); +// return Promise.resolve({ stdout: '', stderr: '' }); +// }, +// } as unknown) as IPythonExecutionService; + +// server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// await server.serverReady(); +// const uuid = server.createUUID(); +// payload = payload.replace('UUID_HERE', uuid); +// server.onDiscoveryDataReceived(({ data }) => { +// eventData = data; +// deferred.resolve(); +// }); + +// client.on('connect', () => { +// console.log('Socket connected, local port:', client.localPort); +// client.write(payload); +// client.end(); +// }); +// client.on('error', (error) => { +// console.log('Socket connection error:', error); +// }); + +// await server.sendCommand(options); +// await deferred.promise; +// assert.deepStrictEqual(eventData, expectedResult); +// }); +// }); + +// test('Calls run resolver if the result header is in the payload', async () => { +// let eventData: string | undefined; +// const client = new net.Socket(); +// const deferred = createDeferred(); + +// const options = { +// command: { script: 'myscript', args: ['-foo', 'foo'] }, +// workspaceFolder: Uri.file('/foo/bar'), +// cwd: '/foo/bar', +// uuid: fakeUuid, +// }; + +// stubExecutionService = ({ +// exec: async () => { +// client.connect(server.getPort()); +// return Promise.resolve({ stdout: '', stderr: '' }); +// }, +// } as unknown) as IPythonExecutionService; + +// server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// await server.serverReady(); +// const uuid = server.createUUID(); +// server.onRunDataReceived(({ data }) => { +// eventData = data; +// deferred.resolve(); +// }); + +// const payload = `Content-Length: 87 +// Content-Type: application/json +// Request-uuid: ${uuid} + +// {"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}`; + +// client.on('connect', () => { +// console.log('Socket connected, local port:', client.localPort); +// client.write(payload); +// client.end(); +// }); +// client.on('error', (error) => { +// console.log('Socket connection error:', error); +// }); + +// await server.sendCommand(options); +// await deferred.promise; +// console.log('event data', eventData); +// const expectedResult = +// '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; +// assert.deepStrictEqual(eventData, expectedResult); +// }); +// }); diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index 3d3521291f74..ef21655e93e4 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -23,7 +23,7 @@ suite('Unittest test discovery adapter', () => { outputChannel = typemoq.Mock.ofType(); }); - test('discoverTests should send the discovery command to the test server', async () => { + test('DiscoverTests should send the discovery command to the test server with the correct args', async () => { let options: TestCommandOptions | undefined; const stubTestServer = ({ @@ -32,7 +32,7 @@ suite('Unittest test discovery adapter', () => { options = opt; return Promise.resolve(); }, - onDataReceived: () => { + onDiscoveryDataReceived: () => { // no body }, createUUID: () => '123456789', @@ -47,61 +47,11 @@ suite('Unittest test discovery adapter', () => { assert.deepStrictEqual(options, { workspaceFolder: uri, cwd: uri.fsPath, - command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, + command: { + script, + args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'], + }, uuid: '123456789', }); }); - - test("onDataReceivedHandler should parse the data if the cwd from the payload matches the test adapter's cwd", async () => { - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => '123456789', - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - const data = { status: 'success' }; - const uuid = '123456789'; - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - const promise = adapter.discoverTests(uri); - - adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); - - const result = await promise; - - assert.deepStrictEqual(result, data); - }); - - test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { - const correctUuid = '123456789'; - const incorrectUuid = '987654321'; - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => correctUuid, - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - const promise = adapter.discoverTests(uri); - - const data = { status: 'success' }; - adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); - - const nextData = { status: 'error' }; - adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); - - const result = await promise; - - assert.deepStrictEqual(result, nextData); - }); }); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index e5495629bf28..88126225a177 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -1,118 +1,65 @@ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. - -// import * as assert from 'assert'; -// import * as path from 'path'; -// import * as typemoq from 'typemoq'; -// import { Uri } from 'vscode'; -// import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; -// import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -// import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; -// import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; - -// suite('Unittest test execution adapter', () => { -// let stubConfigSettings: IConfigurationService; -// let outputChannel: typemoq.IMock; - -// setup(() => { -// stubConfigSettings = ({ -// getSettings: () => ({ -// testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, -// }), -// } as unknown) as IConfigurationService; -// outputChannel = typemoq.Mock.ofType(); -// }); - -// test('runTests should send the run command to the test server', async () => { -// let options: TestCommandOptions | undefined; - -// const stubTestServer = ({ -// sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { -// delete opt.outChannel; -// options = opt; -// assert(runTestIdPort !== undefined); -// return Promise.resolve(); -// }, -// onDataReceived: () => { -// // no body -// }, -// createUUID: () => '123456789', -// } as unknown) as ITestServer; - -// const uri = Uri.file('/foo/bar'); -// const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); - -// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); -// adapter.runTests(uri, [], false).then(() => { -// const expectedOptions: TestCommandOptions = { -// workspaceFolder: uri, -// command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, -// cwd: uri.fsPath, -// uuid: '123456789', -// debugBool: false, -// testIds: [], -// }; -// assert.deepStrictEqual(options, expectedOptions); -// }); -// }); -// test("onDataReceivedHandler should parse the data if the cwd from the payload matches the test adapter's cwd", async () => { -// const stubTestServer = ({ -// sendCommand(): Promise { -// return Promise.resolve(); -// }, -// onDataReceived: () => { -// // no body -// }, -// createUUID: () => '123456789', -// } as unknown) as ITestServer; - -// const uri = Uri.file('/foo/bar'); -// const data = { status: 'success' }; -// const uuid = '123456789'; - -// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - -// // triggers runTests flow which will run onDataReceivedHandler and the -// // promise resolves into the parsed data. -// const promise = adapter.runTests(uri, [], false); - -// adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); - -// const result = await promise; - -// assert.deepStrictEqual(result, data); -// }); -// test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { -// const correctUuid = '123456789'; -// const incorrectUuid = '987654321'; -// const stubTestServer = ({ -// sendCommand(): Promise { -// return Promise.resolve(); -// }, -// onDataReceived: () => { -// // no body -// }, -// createUUID: () => correctUuid, -// } as unknown) as ITestServer; - -// const uri = Uri.file('/foo/bar'); - -// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - -// // triggers runTests flow which will run onDataReceivedHandler and the -// // promise resolves into the parsed data. -// const promise = adapter.runTests(uri, [], false); - -// const data = { status: 'success' }; -// // will not resolve due to incorrect UUID -// adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); - -// const nextData = { status: 'error' }; -// // will resolve and nextData will be returned as result -// adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); - -// const result = await promise; - -// assert.deepStrictEqual(result, nextData); -// }); -// }); +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import * as sinon from 'sinon'; +import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; +import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; +import * as util from '../../../../client/testing/testController/common/utils'; + +suite('Unittest test execution adapter', () => { + let stubConfigSettings: IConfigurationService; + let outputChannel: typemoq.IMock; + + setup(() => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + } as unknown) as IConfigurationService; + outputChannel = typemoq.Mock.ofType(); + sinon.stub(util, 'startTestIdServer').returns(Promise.resolve(54321)); + }); + teardown(() => { + sinon.restore(); + }); + + test('runTests should send the run command to the test server', async () => { + let options: TestCommandOptions | undefined; + + const stubTestServer = ({ + sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { + delete opt.outChannel; + options = opt; + assert(runTestIdPort !== undefined); + return Promise.resolve(); + }, + onRunDataReceived: () => { + // no body + }, + createUUID: () => '123456789', + } as unknown) as ITestServer; + + const uri = Uri.file('/foo/bar'); + const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + const testIds = ['test1id', 'test2id']; + adapter.runTests(uri, testIds, false).then(() => { + const expectedOptions: TestCommandOptions = { + workspaceFolder: uri, + command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, + cwd: uri.fsPath, + uuid: '123456789', + debugBool: false, + testIds, + }; + assert.deepStrictEqual(options, expectedOptions); + }); + }); +}); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index 539647aece9f..5a2e48130746 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -5,19 +5,23 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; -import { TestController, TestItem, Uri } from 'vscode'; +import { TestController, TestItem, TestItemCollection, TestRun, Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; // 7/7 import { WorkspaceTestAdapter } from '../../../client/testing/testController/workspaceTestAdapter'; import * as Telemetry from '../../../client/telemetry'; import { EventName } from '../../../client/telemetry/constants'; -import { ITestServer } from '../../../client/testing/testController/common/types'; +import { ITestResultResolver, ITestServer } from '../../../client/testing/testController/common/types'; +import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; +import * as util from '../../../client/testing/testController/common/utils'; +import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; suite('Workspace test adapter', () => { suite('Test discovery', () => { let stubTestServer: ITestServer; let stubConfigSettings: IConfigurationService; + let stubResultResolver: ITestResultResolver; let discoverTestsStub: sinon.SinonStub; let sendTelemetryStub: sinon.SinonStub; @@ -29,8 +33,6 @@ suite('Workspace test adapter', () => { let testController: TestController; let log: string[] = []; - const sandbox = sinon.createSandbox(); - setup(() => { stubConfigSettings = ({ getSettings: () => ({ @@ -47,6 +49,19 @@ suite('Workspace test adapter', () => { }, } as unknown) as ITestServer; + stubResultResolver = ({ + resolveDiscovery: () => { + // no body + }, + resolveExecution: () => { + // no body + }, + } as unknown) as ITestResultResolver; + + // const vsIdToRunIdGetStub = sinon.stub(stubResultResolver.vsIdToRunId, 'get'); + // const expectedRunId = 'expectedRunId'; + // vsIdToRunIdGetStub.withArgs(sinon.match.any).returns(expectedRunId); + // For some reason the 'tests' namespace in vscode returns undefined. // While I figure out how to expose to the tests, they will run // against a stub test controller and stub test items. @@ -97,8 +112,8 @@ suite('Workspace test adapter', () => { }); }; - discoverTestsStub = sandbox.stub(UnittestTestDiscoveryAdapter.prototype, 'discoverTests'); - sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + discoverTestsStub = sinon.stub(UnittestTestDiscoveryAdapter.prototype, 'discoverTests'); + sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); outputChannel = typemoq.Mock.ofType(); }); @@ -106,7 +121,54 @@ suite('Workspace test adapter', () => { telemetryEvent = []; log = []; testController.dispose(); - sandbox.restore(); + sinon.restore(); + }); + + test('If discovery failed correctly create error node', async () => { + discoverTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + const blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const testProvider = 'unittest'; + + const abc = await workspaceTestAdapter.discoverTests(testController); + console.log(abc); + + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); }); test("When discovering tests, the workspace test adapter should call the test discovery adapter's discoverTest method", async () => { @@ -127,6 +189,7 @@ suite('Workspace test adapter', () => { testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); await workspaceTestAdapter.discoverTests(testController); @@ -160,6 +223,7 @@ suite('Workspace test adapter', () => { testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); // Try running discovery twice @@ -190,6 +254,7 @@ suite('Workspace test adapter', () => { testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); await workspaceTestAdapter.discoverTests(testController); @@ -220,6 +285,7 @@ suite('Workspace test adapter', () => { testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); await workspaceTestAdapter.discoverTests(testController); @@ -229,18 +295,322 @@ suite('Workspace test adapter', () => { const lastEvent = telemetryEvent[1]; assert.ok(lastEvent.properties.failed); + }); + }); + suite('Test execution workspace test adapter', () => { + let stubTestServer: ITestServer; + let stubConfigSettings: IConfigurationService; + let stubResultResolver: ITestResultResolver; + let executionTestsStub: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; + let outputChannel: typemoq.IMock; + let runInstance: typemoq.IMock; + let testControllerMock: typemoq.IMock; + let telemetryEvent: { eventName: EventName; properties: Record }[] = []; + let resultResolver: ResultResolver.PythonResultResolver; + + // Stubbed test controller (see comment around L.40) + let testController: TestController; + let log: string[] = []; + + const sandbox = sinon.createSandbox(); + + setup(() => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['--foo'] }, + }), + } as unknown) as IConfigurationService; + + stubTestServer = ({ + sendCommand(): Promise { + return Promise.resolve(); + }, + onDataReceived: () => { + // no body + }, + } as unknown) as ITestServer; - assert.deepStrictEqual(log, ['createTestItem', 'add']); + stubResultResolver = ({ + resolveDiscovery: () => { + // no body + }, + resolveExecution: () => { + // no body + }, + vsIdToRunId: { + get: sinon.stub().returns('expectedRunId'), + }, + } as unknown) as ITestResultResolver; + const testItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + createTestItem: () => { + log.push('createTestItem'); + return testItem; + }, + dispose: () => { + // empty + }, + } as unknown) as TestController; + + const mockSendTelemetryEvent = ( + eventName: EventName, + _: number | Record | undefined, + properties: unknown, + ) => { + telemetryEvent.push({ + eventName, + properties: properties as Record, + }); + }; + + executionTestsStub = sandbox.stub(UnittestTestExecutionAdapter.prototype, 'runTests'); + sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + outputChannel = typemoq.Mock.ofType(); + runInstance = typemoq.Mock.ofType(); + + const testProvider = 'pytest'; + const workspaceUri = Uri.file('foo'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); }); - /** - * TODO To test: - * - successful discovery but no data: delete everything from the test controller - * - successful discovery with error status: add error node to tree - * - single root: populate tree if there's no root node - * - single root: update tree if there's a root node - * - single root: delete tree if there are no tests in the test data - * - multiroot: update the correct folders - */ + teardown(() => { + telemetryEvent = []; + log = []; + testController.dispose(); + sandbox.restore(); + }); + test('When executing tests, the right tests should be sent to be executed', async () => { + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + resultResolver, + ); + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + + sinon.stub(testItemUtilities, 'getTestCaseNodes').callsFake((testNode: TestItem) => + // Custom implementation logic here based on the provided testNode and collection + + // Example implementation: returning a predefined array of TestItem objects + [testNode], + ); + + const mockTestItem1 = createMockTestItem('mockTestItem1'); + const mockTestItem2 = createMockTestItem('mockTestItem2'); + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + // Add as many mock TestItems as needed + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + const testItemCollectionMock = typemoq.Mock.ofType(); + + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + testControllerMock = typemoq.Mock.ofType(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, [mockTestItem1, mockTestItem2]); + + runInstance.verify((r) => r.started(typemoq.It.isAny()), typemoq.Times.exactly(2)); + }); + + test("When executing tests, the workspace test adapter should call the test execute adapter's executionTest method", async () => { + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, []); + + sinon.assert.calledOnce(executionTestsStub); + }); + + test('If execution is already running, do not call executionAdapter.runTests again', async () => { + executionTestsStub.callsFake( + async () => + new Promise((resolve) => { + setTimeout(() => { + // Simulate time taken by discovery. + resolve(); + }, 2000); + }), + ); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + // Try running discovery twice + const one = workspaceTestAdapter.executeTests(testController, runInstance.object, []); + const two = workspaceTestAdapter.executeTests(testController, runInstance.object, []); + + Promise.all([one, two]); + + sinon.assert.calledOnce(executionTestsStub); + }); + + test('If execution failed correctly create error node', async () => { + executionTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + const blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const testProvider = 'unittest'; + + await workspaceTestAdapter.executeTests(testController, runInstance.object, []); + + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); + }); + + test('If execution failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { + executionTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, []); + + sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_RUN_ALL_FAILED); + assert.strictEqual(telemetryEvent.length, 1); + }); }); }); + +function createMockTestItem(id: string): TestItem { + const range = typemoq.Mock.ofType(); + const mockTestItem = ({ + id, + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + range, + uri: Uri.file('/foo/bar'), + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index ebbe7ca59e72..44518e7575a7 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -70,6 +70,8 @@ mockedVSCode.Hover = vscodeMocks.Hover; mockedVSCode.Disposable = vscodeMocks.Disposable as any; mockedVSCode.ExtensionKind = vscodeMocks.ExtensionKind; mockedVSCode.CodeAction = vscodeMocks.CodeAction; +mockedVSCode.TestMessage = vscodeMocks.TestMessage; +mockedVSCode.Location = vscodeMocks.Location; mockedVSCode.EventEmitter = vscodeMocks.EventEmitter; mockedVSCode.CancellationTokenSource = vscodeMocks.CancellationTokenSource; mockedVSCode.CompletionItemKind = vscodeMocks.CompletionItemKind; From acd12995eacfd52577a5aedf494e1cd163433476 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Wed, 21 Jun 2023 14:07:29 -0700 Subject: [PATCH 128/336] Fix error checking python version (#21464) Closed: https://github.com/microsoft/vscode-python/issues/19482 --- src/client/debugger/extension/adapter/factory.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts index fc9232729eae..067c2e405ea0 100644 --- a/src/client/debugger/extension/adapter/factory.ts +++ b/src/client/debugger/extension/adapter/factory.ts @@ -183,7 +183,10 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise { if (interpreter) { - if ((interpreter.version?.major ?? 0) < 3 || (interpreter.version?.minor ?? 0) <= 6) { + if ( + (interpreter.version?.major ?? 0) < 3 || + ((interpreter.version?.major ?? 0) <= 3 && (interpreter.version?.minor ?? 0) <= 6) + ) { this.showDeprecatedPythonMessage(); } return interpreter.path.length > 0 ? [interpreter.path] : []; From 2e936ef93b686fc7d1d7054fd1707798a0037606 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Thu, 22 Jun 2023 09:48:28 -0700 Subject: [PATCH 129/336] Adopt lifecycle managed by parent (#21467) closed: https://github.com/microsoft/vscode-python/issues/20376 --- src/client/common/application/types.ts | 3 ++- .../extension/hooks/childProcessAttachService.ts | 14 ++++++++++---- .../hooks/childProcessAttachService.unit.test.ts | 8 ++++---- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 72cd357d62d8..fa2ced6c45da 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -16,6 +16,7 @@ import { DebugConsole, DebugSession, DebugSessionCustomEvent, + DebugSessionOptions, DecorationRenderOptions, Disposable, DocumentSelector, @@ -975,7 +976,7 @@ export interface IDebugService { startDebugging( folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, - parentSession?: DebugSession, + parentSession?: DebugSession | DebugSessionOptions, ): Thenable; /** diff --git a/src/client/debugger/extension/hooks/childProcessAttachService.ts b/src/client/debugger/extension/hooks/childProcessAttachService.ts index c6556a62eaa1..08f44bc3cea5 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachService.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachService.ts @@ -5,7 +5,7 @@ import { inject, injectable } from 'inversify'; import { IDebugService } from '../../../common/application/types'; -import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder } from 'vscode'; +import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder, DebugSessionOptions } from 'vscode'; import { noop } from '../../../common/utils/misc'; import { captureTelemetry } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; @@ -28,11 +28,17 @@ export class ChildProcessAttachService implements IChildProcessAttachService { @captureTelemetry(EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS) public async attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise { const debugConfig: AttachRequestArguments & DebugConfiguration = data; - const processId = debugConfig.subProcessId!; const folder = this.getRelatedWorkspaceFolder(debugConfig); - const launched = await this.debugService.startDebugging(folder, debugConfig, parentSession); + const debugSessionOption: DebugSessionOptions = { + parentSession: parentSession, + lifecycleManagedByParent: true, + }; + const launched = await this.debugService.startDebugging(folder, debugConfig, debugSessionOption); if (!launched) { - showErrorMessage(l10n.t('Failed to launch debugger for child process {0}', processId)).then(noop, noop); + showErrorMessage(l10n.t('Failed to launch debugger for child process {0}', debugConfig.subProcessId!)).then( + noop, + noop, + ); } } diff --git a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts index 2ab9d3e30d2c..118efe416e94 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts @@ -117,7 +117,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); sinon.assert.notCalled(showErrorMessageStub); }); - test('Validate debug config is passed as is', async () => { + test('Validate debug config is passed with the correct params', async () => { const data: LaunchRequestArguments | AttachRequestArguments = { request: 'attach', type: 'python', @@ -140,7 +140,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal(session); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); sinon.assert.notCalled(showErrorMessageStub); }); test('Pass data as is if data is attach debug configuration', async () => { @@ -161,7 +161,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal(session); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); sinon.assert.notCalled(showErrorMessageStub); }); test('Validate debug config when parent/root parent was attached', async () => { @@ -189,7 +189,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal(session); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); sinon.assert.notCalled(showErrorMessageStub); }); }); From 55cd2e041efee42ace3460fb6b2533d299b171ff Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 22 Jun 2023 11:09:21 -0700 Subject: [PATCH 130/336] add test for import pytest error (#21472) fixes https://github.com/microsoft/vscode-python/issues/21470 --- .../.data/error_pytest_import.txt | 6 ++++ .../tests/pytestadapter/test_discovery.py | 30 +++++++++++++++++++ pythonFiles/vscode_pytest/__init__.py | 2 +- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/error_pytest_import.txt diff --git a/pythonFiles/tests/pytestadapter/.data/error_pytest_import.txt b/pythonFiles/tests/pytestadapter/.data/error_pytest_import.txt new file mode 100644 index 000000000000..7d65dee2ccc6 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/error_pytest_import.txt @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +@pytest.mark.parametrize("num", range(1, 89)) +def test_odd_even(num): + assert True diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 8c0f8dbae68c..3cbaa270cab9 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -10,6 +10,36 @@ from .helpers import TEST_DATA_PATH, runner +def test_import_error(tmp_path): + """Test pytest discovery on a file that has a pytest marker but does not import pytest. + + Copies the contents of a .txt file to a .py file in the temporary directory + to then run pytest discovery on. + + The json should still be returned but the errors list should be present. + + Keyword arguments: + tmp_path -- pytest fixture that creates a temporary directory. + """ + # Saving some files as .txt to avoid that file displaying a syntax error for + # the extension as a whole. Instead, rename it before running this test + # in order to test the error handling. + file_path = TEST_DATA_PATH / "error_pytest_import.txt" + temp_dir = tmp_path / "temp_data" + temp_dir.mkdir() + p = temp_dir / "error_pytest_import.py" + shutil.copyfile(file_path, p) + actual_list: Optional[List[Dict[str, Any]]] = runner( + ["--collect-only", os.fspath(p)] + ) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 2 + + def test_syntax_error(tmp_path): """Test pytest discovery on a file that has a syntax error. diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index f9f147c381a9..726dba8c9a97 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -79,7 +79,7 @@ def pytest_keyboard_interrupt(excinfo): Keyword arguments: excinfo -- the exception information of type ExceptionInfo. """ - # The function execonly() returns the exception as a string. + # The function exconly() returns the exception as a string. ERRORS.append(excinfo.exconly()) From 8c86417c55ff4317c9173d34600f4fc8885c58b2 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 23 Jun 2023 15:12:00 -0700 Subject: [PATCH 131/336] Add dynamic result (#21466) fixes, https://github.com/microsoft/vscode-python/issues/21148 and https://github.com/microsoft/vscode-python/issues/21149 --- pythonFiles/tests/pytestadapter/helpers.py | 69 ++++++++++++------- .../tests/pytestadapter/test_discovery.py | 24 +++---- .../tests/pytestadapter/test_execution.py | 42 +++++------ .../tests/unittestadapter/test_execution.py | 5 ++ pythonFiles/unittestadapter/discovery.py | 4 +- pythonFiles/unittestadapter/execution.py | 67 ++++++++++-------- pythonFiles/vscode_pytest/__init__.py | 66 +++++++++++++----- .../testController/common/resultResolver.ts | 19 +++-- .../testing/testController/common/types.ts | 2 +- .../pytest/pytestDiscoveryAdapter.ts | 5 +- .../pytest/pytestExecutionAdapter.ts | 19 +++-- .../unittest/testDiscoveryAdapter.ts | 15 ++-- .../unittest/testExecutionAdapter.ts | 21 +++--- .../resultResolver.unit.test.ts | 2 +- 14 files changed, 215 insertions(+), 145 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index 013e4bb31fca..c3e01d52170a 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + import io import json import os @@ -9,7 +10,7 @@ import sys import threading import uuid -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict @@ -72,21 +73,34 @@ def process_rpc_message(data: str) -> Tuple[Dict[str, Any], str]: str_stream: io.StringIO = io.StringIO(data) length: int = 0 + while True: line: str = str_stream.readline() if CONTENT_LENGTH.lower() in line.lower(): length = int(line[len(CONTENT_LENGTH) :]) break + if not line or line.isspace(): raise ValueError("Header does not contain Content-Length") + while True: line: str = str_stream.readline() if not line or line.isspace(): break raw_json: str = str_stream.read(length) - dict_json: Dict[str, Any] = json.loads(raw_json) - return dict_json, str_stream.read() + return json.loads(raw_json), str_stream.read() + + +def process_rpc_json(data: str) -> List[Dict[str, Any]]: + """Process the JSON data which comes from the server which runs the pytest discovery.""" + json_messages = [] + remaining = data + while remaining: + json_data, remaining = process_rpc_message(remaining) + json_messages.append(json_data) + + return json_messages def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: @@ -110,53 +124,58 @@ def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), } ) + completed = threading.Event() - result: list = [] + result = [] t1: threading.Thread = threading.Thread( - target=_listen_on_socket, args=(listener, result) + target=_listen_on_socket, args=(listener, result, completed) ) t1.start() t2 = threading.Thread( - target=lambda proc_args, proc_env, proc_cwd: subprocess.run( - proc_args, env=proc_env, cwd=proc_cwd - ), - args=(process_args, env, TEST_DATA_PATH), + target=_run_test_code, + args=(process_args, env, TEST_DATA_PATH, completed), ) t2.start() t1.join() t2.join() - a = process_rpc_json(result[0]) - return a if result else None + return process_rpc_json(result[0]) if result else None -def process_rpc_json(data: str) -> List[Dict[str, Any]]: - """Process the JSON data which comes from the server which runs the pytest discovery.""" - json_messages = [] - remaining = data - while remaining: - json_data, remaining = process_rpc_message(remaining) - json_messages.append(json_data) - - return json_messages - - -def _listen_on_socket(listener: socket.socket, result: List[str]): +def _listen_on_socket( + listener: socket.socket, result: List[str], completed: threading.Event +): """Listen on the socket for the JSON data from the server. - Created as a seperate function for clarity in threading. + Created as a separate function for clarity in threading. """ sock, (other_host, other_port) = listener.accept() + listener.settimeout(1) all_data: list = [] while True: data: bytes = sock.recv(1024 * 1024) if not data: - break + if completed.is_set(): + break + else: + try: + sock, (other_host, other_port) = listener.accept() + except socket.timeout: + result.append("".join(all_data)) + return all_data.append(data.decode("utf-8")) result.append("".join(all_data)) +def _run_test_code( + proc_args: List[str], proc_env, proc_cwd: str, completed: threading.Event +): + result = subprocess.run(proc_args, env=proc_env, cwd=proc_cwd) + completed.set() + return result + + def find_test_line_number(test_name: str, test_file_path) -> str: """Function which finds the correct line number for a test by looking for the "test_marker--[test_name]" string. diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 3cbaa270cab9..7f2355129c65 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -59,11 +59,10 @@ def test_syntax_error(tmp_path): temp_dir.mkdir() p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) - actual_list: Optional[List[Dict[str, Any]]] = runner( - ["--collect-only", os.fspath(p)] - ) - assert actual_list - for actual in actual_list: + actual = runner(["--collect-only", os.fspath(p)]) + if actual: + actual = actual[0] + assert actual assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) @@ -76,11 +75,9 @@ def test_parameterized_error_collect(): The json should still be returned but the errors list should be present. """ file_path_str = "error_parametrize_discovery.py" - actual_list: Optional[List[Dict[str, Any]]] = runner( - ["--collect-only", file_path_str] - ) - assert actual_list - for actual in actual_list: + actual = runner(["--collect-only", file_path_str]) + if actual: + actual = actual[0] assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) @@ -135,14 +132,15 @@ def test_pytest_collect(file, expected_const): file -- a string with the file or folder to run pytest discovery on. expected_const -- the expected output from running pytest discovery on the file. """ - actual_list: Optional[List[Dict[str, Any]]] = runner( + actual = runner( [ "--collect-only", os.fspath(TEST_DATA_PATH / file), ] ) - assert actual_list - for actual in actual_list: + if actual: + actual = actual[0] + assert actual assert all(item in actual for item in ("status", "cwd", "tests")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 5b2cbddb1644..d11766027a91 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. import os import shutil -from typing import Any, Dict, List, Optional import pytest from tests.pytestadapter import expected_execution_test_output @@ -29,11 +28,10 @@ def test_syntax_error_execution(tmp_path): temp_dir.mkdir() p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) - actual_list: Optional[List[Dict[str, Any]]] = runner( - ["error_syntax_discover.py::test_function"] - ) - assert actual_list - for actual in actual_list: + actual = runner(["error_syntax_discover.py::test_function"]) + if actual: + actual = actual[0] + assert actual assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) @@ -45,9 +43,10 @@ def test_bad_id_error_execution(): The json should still be returned but the errors list should be present. """ - actual_list: Optional[List[Dict[str, Any]]] = runner(["not/a/real::test_id"]) - assert actual_list - for actual in actual_list: + actual = runner(["not/a/real::test_id"]) + if actual: + actual = actual[0] + assert actual assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) @@ -156,14 +155,17 @@ def test_pytest_execution(test_ids, expected_const): expected_const -- a dictionary of the expected output from running pytest discovery on the files. """ args = test_ids - actual_list: Optional[List[Dict[str, Any]]] = runner(args) - assert actual_list - for actual in actual_list: - assert all(item in actual for item in ("status", "cwd", "result")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - result_data = actual["result"] - for key in result_data: - if result_data[key]["outcome"] == "failure": - result_data[key]["message"] = "ERROR MESSAGE" - assert result_data == expected_const + actual = runner(args) + assert actual + print(actual) + assert len(actual) == len(expected_const) + actual_result_dict = dict() + for a in actual: + assert all(item in a for item in ("status", "cwd", "result")) + assert a["status"] == "success" + assert a["cwd"] == os.fspath(TEST_DATA_PATH) + actual_result_dict.update(a["result"]) + for key in actual_result_dict: + if actual_result_dict[key]["outcome"] == "failure": + actual_result_dict[key]["message"] = "ERROR MESSAGE" + assert actual_result_dict == expected_const diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index d461ead9ad94..c7a5cb0eef61 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -3,9 +3,14 @@ import os import pathlib +import sys from typing import List import pytest + +PYTHON_FILES = pathlib.Path(__file__).parent.parent + +sys.path.insert(0, os.fspath(PYTHON_FILES)) from unittestadapter.execution import parse_execution_cli_args, run_tests TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index bcc2fd967f78..02490024b217 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -17,8 +17,8 @@ from typing_extensions import Literal # Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. -PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, PYTHON_FILES) +PYTHON_FILES = pathlib.Path(__file__).parent.parent +sys.path.insert(0, os.fspath(PYTHON_FILES)) from testing_tools import socket_manager diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 17c125e5843a..d9d5f41d624b 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -13,17 +13,14 @@ from types import TracebackType from typing import Dict, List, Optional, Tuple, Type, Union -script_dir = pathlib.Path(__file__).parent.parent -sys.path.append(os.fspath(script_dir)) -sys.path.append(os.fspath(script_dir / "lib" / "python")) -from testing_tools import process_json_util - +directory_path = pathlib.Path(__file__).parent.parent / "lib" / "python" # Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. -PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, PYTHON_FILES) +PYTHON_FILES = pathlib.Path(__file__).parent.parent + +sys.path.insert(0, os.fspath(PYTHON_FILES)) # Add the lib path to sys.path to find the typing_extensions module. sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) -from testing_tools import socket_manager +from testing_tools import process_json_util, socket_manager from typing_extensions import NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args @@ -54,6 +51,9 @@ def parse_execution_cli_args( ErrorType = Union[ Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] ] +PORT = 0 +UUID = 0 +START_DIR = "" class TestOutcomeEnum(str, enum.Enum): @@ -148,8 +148,10 @@ def formatResult( "traceback": tb, "subtest": subtest.id() if subtest else None, } - self.formatted[test_id] = result + if PORT == 0 or UUID == 0: + print("Error sending response, port or uuid unknown to python server.") + send_run_data(result, PORT, UUID) class TestExecutionStatus(str, enum.Enum): @@ -225,6 +227,33 @@ def run_tests( return payload +def send_run_data(raw_data, port, uuid): + # Build the request data (it has to be a POST request or the Node side will not process it), and send it. + status = raw_data["outcome"] + cwd = os.path.abspath(START_DIR) + if raw_data["subtest"]: + test_id = raw_data["subtest"] + else: + test_id = raw_data["test"] + test_dict = {} + test_dict[test_id] = raw_data + payload: PayloadDict = {"cwd": cwd, "status": status, "result": test_dict} + addr = ("localhost", port) + data = json.dumps(payload) + request = f"""Content-Length: {len(data)} +Content-Type: application/json +Request-uuid: {uuid} + +{data}""" + try: + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) + except Exception as e: + print(f"Error sending response: {e}") + print(f"Request data: {request}") + + if __name__ == "__main__": # Get unittest test execution arguments. argv = sys.argv[1:] @@ -270,11 +299,11 @@ def run_tests( print(f"Error: Could not connect to runTestIdsPort: {e}") print("Error: Could not connect to runTestIdsPort") - port, uuid = parse_execution_cli_args(argv[:index]) + PORT, UUID = parse_execution_cli_args(argv[:index]) if test_ids_from_buffer: # Perform test execution. payload = run_tests( - start_dir, test_ids_from_buffer, pattern, top_level_dir, uuid + start_dir, test_ids_from_buffer, pattern, top_level_dir, UUID ) else: cwd = os.path.abspath(start_dir) @@ -284,19 +313,3 @@ def run_tests( "status": status, "error": "No test ids received from buffer", } - - # Build the request data and send it. - addr = ("localhost", port) - data = json.dumps(payload) - request = f"""Content-Length: {len(data)} -Content-Type: application/json -Request-uuid: {uuid} - -{data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Error sending response: {e}") - print(f"Request data: {request}") diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 726dba8c9a97..1178abdb93d6 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -56,7 +56,7 @@ def pytest_internalerror(excrepr, excinfo): excinfo -- the exception information of type ExceptionInfo. """ # call.excinfo.exconly() returns the exception as a string. - ERRORS.append(excinfo.exconly()) + ERRORS.append(excinfo.exconly() + "\n Check Python Test Logs for more details.") def pytest_exception_interact(node, call, report): @@ -70,7 +70,13 @@ def pytest_exception_interact(node, call, report): # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. if call.excinfo and call.excinfo.typename != "AssertionError": - ERRORS.append(call.excinfo.exconly()) + ERRORS.append( + call.excinfo.exconly() + "\n Check Python Test Logs for more details." + ) + else: + ERRORS.append( + report.longreprtext + "\n Check Python Test Logs for more details." + ) def pytest_keyboard_interrupt(excinfo): @@ -79,8 +85,8 @@ def pytest_keyboard_interrupt(excinfo): Keyword arguments: excinfo -- the exception information of type ExceptionInfo. """ - # The function exconly() returns the exception as a string. - ERRORS.append(excinfo.exconly()) + # The function execonly() returns the exception as a string. + ERRORS.append(excinfo.exconly() + "\n Check Python Test Logs for more details.") class TestOutcome(Dict): @@ -120,7 +126,6 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): tests: Dict[str, TestOutcome] -collected_tests = testRunResultDict() IS_DISCOVERY = False @@ -130,6 +135,9 @@ def pytest_load_initial_conftests(early_config, parser, args): IS_DISCOVERY = True +collected_tests_so_far = list() + + def pytest_report_teststatus(report, config): """ A pytest hook that is called when a test is called. It is called 3 times per test, @@ -138,6 +146,7 @@ def pytest_report_teststatus(report, config): report -- the report on the test setup, call, and teardown. config -- configuration object. """ + cwd = pathlib.Path.cwd() if report.when == "call": traceback = None @@ -148,13 +157,22 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - item_result = create_test_outcome( - report.nodeid, - report_value, - message, - traceback, - ) - collected_tests[report.nodeid] = item_result + node_id = str(report.nodeid) + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) + item_result = create_test_outcome( + node_id, + report_value, + message, + traceback, + ) + collected_test = testRunResultDict() + collected_test[node_id] = item_result + execution_post( + os.fsdecode(cwd), + "success", + collected_test if collected_test else None, + ) ERROR_MESSAGE_CONST = { @@ -187,6 +205,15 @@ def pytest_sessionfinish(session, exitstatus): ) cwd = pathlib.Path.cwd() if IS_DISCOVERY: + if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5): + errorNode: TestNode = { + "name": "", + "path": cwd, + "type_": "error", + "children": [], + "id_": "", + } + post_response(os.fsdecode(cwd), errorNode) try: session_node: Union[TestNode, None] = build_test_tree(session) if not session_node: @@ -196,7 +223,9 @@ def pytest_sessionfinish(session, exitstatus): ) post_response(os.fsdecode(cwd), session_node) except Exception as e: - f"Error Occurred, description: {e.args[0] if e.args and e.args[0] else ''} traceback: {(traceback.format_exc() if e.__traceback__ else '')}" + ERRORS.append( + f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" + ) errorNode: TestNode = { "name": "", "path": cwd, @@ -213,11 +242,12 @@ def pytest_sessionfinish(session, exitstatus): f"Pytest exited with error status: {exitstatus}, {ERROR_MESSAGE_CONST[exitstatus]}" ) exitstatus_bool = "error" - execution_post( - os.fsdecode(cwd), - exitstatus_bool, - collected_tests if collected_tests else None, - ) + + execution_post( + os.fsdecode(cwd), + exitstatus_bool, + None, + ) def build_test_tree(session: pytest.Session) -> TestNode: diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 49243390ad0f..9b2ad6abdf78 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -24,6 +24,8 @@ export class PythonResultResolver implements ITestResultResolver { public vsIdToRunId: Map; + public subTestStats: Map = new Map(); + constructor(testController: TestController, testProvider: TestProvider, private workspaceUri: Uri) { this.testController = testController; this.testProvider = testProvider; @@ -47,13 +49,13 @@ export class PythonResultResolver implements ITestResultResolver { if (rawTestData.status === 'error') { const testingErrorConst = this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; - const { errors } = rawTestData; - traceError(testingErrorConst, '\r\n', errors!.join('\r\n\r\n')); + const { error } = rawTestData; + traceError(testingErrorConst, '\r\n', error?.join('\r\n\r\n') ?? ''); let errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); const message = util.format( `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, - errors!.join('\r\n\r\n'), + error?.join('\r\n\r\n') ?? '', ); if (errorNode === undefined) { @@ -88,7 +90,6 @@ export class PythonResultResolver implements ITestResultResolver { const rawTestExecData = payload; if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { // Map which holds the subtest information for each test item. - const subTestStats: Map = new Map(); // iterate through payload and update the UI accordingly. for (const keyTemp of Object.keys(rawTestExecData.result)) { @@ -163,11 +164,11 @@ export class PythonResultResolver implements ITestResultResolver { const data = rawTestExecData.result[keyTemp]; // find the subtest's parent test item if (parentTestItem) { - const subtestStats = subTestStats.get(parentTestCaseId); + const subtestStats = this.subTestStats.get(parentTestCaseId); if (subtestStats) { subtestStats.failed += 1; } else { - subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); + this.subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); @@ -200,11 +201,11 @@ export class PythonResultResolver implements ITestResultResolver { // find the subtest's parent test item if (parentTestItem) { - const subtestStats = subTestStats.get(parentTestCaseId); + const subtestStats = this.subTestStats.get(parentTestCaseId); if (subtestStats) { subtestStats.passed += 1; } else { - subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); + this.subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); @@ -229,5 +230,3 @@ export class PythonResultResolver implements ITestResultResolver { return Promise.resolve(); } } - -// had to switch the order of the original parameter since required param cannot follow optional. diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index cb7fda797c4a..d4e54951bfd7 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -231,7 +231,7 @@ export type DiscoveredTestPayload = { cwd: string; tests?: DiscoveredTestNode; status: 'success' | 'error'; - errors?: string[]; + error?: string[]; }; export type ExecutionTestPayload = { diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 4378c68b534c..211c322d67f1 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -8,7 +8,7 @@ import { SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; +import { createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { traceError, traceVerbose } from '../../../logging'; import { @@ -23,8 +23,6 @@ import { * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied */ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { - private promiseMap: Map> = new Map(); - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, @@ -55,7 +53,6 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); const uuid = this.testServer.createUUID(uri.fsPath); - this.promiseMap.set(uuid, deferred); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 1bf032b9b594..b68b80945ef3 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -4,7 +4,7 @@ import { TestRun, Uri } from 'vscode'; import * as path from 'path'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; +import { createDeferred } from '../../../common/utils/async'; import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, @@ -31,8 +31,6 @@ import { startTestIdServer } from '../common/utils'; */ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { - private promiseMap: Map> = new Map(); - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, @@ -48,6 +46,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, ): Promise { + const uuid = this.testServer.createUUID(uri.fsPath); traceVerbose(uri, testIds, debugBool); const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { @@ -55,18 +54,26 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } }); try { - await this.runTestsNew(uri, testIds, debugBool, executionFactory, debugLauncher); + await this.runTestsNew(uri, testIds, uuid, debugBool, executionFactory, debugLauncher); } finally { + this.testServer.deleteUUID(uuid); disposable.dispose(); // confirm with testing that this gets called (it must clean this up) } - const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + // placeholder until after the rewrite is adopted + // TODO: remove after adoption. + const executionPayload: ExecutionTestPayload = { + cwd: uri.fsPath, + status: 'success', + error: '', + }; return executionPayload; } private async runTestsNew( uri: Uri, testIds: string[], + uuid: string, debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, @@ -75,8 +82,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); this.configSettings.isTestExecution(); - const uuid = this.testServer.createUUID(uri.fsPath); - this.promiseMap.set(uuid, deferred); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 8d393a8da18d..69438e341f14 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -4,7 +4,6 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { DataReceivedEvent, @@ -20,8 +19,6 @@ import { * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. */ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { - private promiseMap: Map> = new Map(); - private cwd: string | undefined; constructor( @@ -32,7 +29,6 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { ) {} public async discoverTests(uri: Uri): Promise { - const deferred = createDeferred(); const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -49,18 +45,21 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { outChannel: this.outputChannel, }; - this.promiseMap.set(uuid, deferred); - const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); }); try { await this.callSendCommand(options); } finally { + this.testServer.deleteUUID(uuid); disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) } - const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; + // placeholder until after the rewrite is adopted + // TODO: remove after adoption. + const discoveryPayload: DiscoveredTestPayload = { + cwd: uri.fsPath, + status: 'success', + }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index ca88d3871706..bf3038817f8b 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { TestRun, Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { Deferred, createDeferred } from '../../../common/utils/async'; +import { createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { DataReceivedEvent, @@ -23,8 +23,6 @@ import { startTestIdServer } from '../common/utils'; */ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { - private promiseMap: Map> = new Map(); - private cwd: string | undefined; constructor( @@ -40,14 +38,16 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { debugBool?: boolean, runInstance?: TestRun, ): Promise { + const uuid = this.testServer.createUUID(uri.fsPath); const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); try { - await this.runTestsNew(uri, testIds, debugBool); + await this.runTestsNew(uri, testIds, uuid, debugBool); } finally { + this.testServer.deleteUUID(uuid); disposable.dispose(); // confirm with testing that this gets called (it must clean this up) } @@ -55,13 +55,17 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { return executionPayload; } - private async runTestsNew(uri: Uri, testIds: string[], debugBool?: boolean): Promise { + private async runTestsNew( + uri: Uri, + testIds: string[], + uuid: string, + debugBool?: boolean, + ): Promise { const settings = this.configSettings.getSettings(uri); const { cwd, unittestArgs } = settings.testing; const command = buildExecutionCommand(unittestArgs); this.cwd = cwd || uri.fsPath; - const uuid = this.testServer.createUUID(uri.fsPath); const options: TestCommandOptions = { workspaceFolder: uri, @@ -74,16 +78,15 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { }; const deferred = createDeferred(); - this.promiseMap.set(uuid, deferred); traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); const runTestIdsPort = await startTestIdServer(testIds); await this.testServer.sendCommand(options, runTestIdsPort.toString(), () => { - // disposable.dispose(); deferred.resolve(); }); - // return deferred.promise; + // placeholder until after the rewrite is adopted + // TODO: remove after adoption. const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 57b321c2e36c..94ec1cc10a9a 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -113,7 +113,7 @@ suite('Result Resolver tests', () => { const payload: DiscoveredTestPayload = { cwd: workspaceUri.fsPath, status: 'error', - errors: [errorMessage], + error: [errorMessage], }; const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { id: 'id', From c1ff6c3ceda1b06d5471837d1d9d74a2d1a52e9e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 23 Jun 2023 16:58:56 -0700 Subject: [PATCH 132/336] add to payload to allow error parsing on TS (#21483) partial fix for https://github.com/microsoft/vscode-python/issues/21476, make sure error display correctly. two part fix: fix "errors" to "error" in unittest payload to align with naming everywhere else add "result" and "test" to payload in run and discovery respectively even on error to havae resultResolver handle correctly. --- .../tests/unittestadapter/test_discovery.py | 8 +++--- .../tests/unittestadapter/test_execution.py | 12 ++++----- pythonFiles/unittestadapter/discovery.py | 25 ++++++++++--------- pythonFiles/unittestadapter/execution.py | 5 ++-- pythonFiles/unittestadapter/utils.py | 6 ++--- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/pythonFiles/tests/unittestadapter/test_discovery.py b/pythonFiles/tests/unittestadapter/test_discovery.py index 30ccb7ef4079..28dc51f55dcd 100644 --- a/pythonFiles/tests/unittestadapter/test_discovery.py +++ b/pythonFiles/tests/unittestadapter/test_discovery.py @@ -132,7 +132,7 @@ def test_simple_discovery() -> None: assert actual["status"] == "success" assert is_same_tree(actual.get("tests"), expected) - assert "errors" not in actual + assert "error" not in actual def test_empty_discovery() -> None: @@ -146,8 +146,8 @@ def test_empty_discovery() -> None: actual = discover_tests(start_dir, pattern, None, uuid) assert actual["status"] == "success" - assert "tests" not in actual - assert "errors" not in actual + assert "tests" in actual + assert "error" not in actual def test_error_discovery() -> None: @@ -213,4 +213,4 @@ def test_error_discovery() -> None: assert actual["status"] == "error" assert is_same_tree(expected, actual.get("tests")) - assert len(actual.get("errors", [])) == 1 + assert len(actual.get("error", [])) == 1 diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index c7a5cb0eef61..a822636bf479 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -65,7 +65,7 @@ def test_no_ids_run() -> None: assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - if "result" in actual: + if actual["result"] is not None: assert len(actual["result"]) == 0 else: raise AssertionError("actual['result'] is None") @@ -85,7 +85,7 @@ def test_single_ids_run() -> None: assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual + assert actual["result"] is not None result = actual["result"] assert len(result) == 1 assert id in result @@ -117,7 +117,7 @@ def test_subtest_run() -> None: assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual + assert actual["result"] is not None result = actual["result"] assert len(result) == 6 for id in subtests_ids: @@ -205,7 +205,7 @@ def test_multiple_ids_run(test_ids, pattern, cwd, expected_outcome) -> None: assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == cwd - assert "result" in actual + assert actual["result"] is not None result = actual["result"] assert len(result) == len(test_ids) for test_id in test_ids: @@ -230,7 +230,7 @@ def test_failed_tests(): assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual + assert actual["result"] is not None result = actual["result"] assert len(result) == len(test_ids) for test_id in test_ids: @@ -255,7 +255,7 @@ def test_unknown_id(): assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual + assert actual["result"] is not None result = actual["result"] assert len(result) == len(test_ids) assert "unittest.loader._FailedTest.unknown_id" in result diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index 02490024b217..ffcd2671addb 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -53,8 +53,8 @@ def parse_discovery_cli_args(args: List[str]) -> Tuple[int, Union[str, None]]: class PayloadDict(TypedDict): cwd: str status: Literal["success", "error"] - tests: NotRequired[TestNode] - errors: NotRequired[List[str]] + tests: Optional[TestNode] + error: NotRequired[List[str]] def discover_tests( @@ -68,7 +68,7 @@ def discover_tests( - uuid: UUID sent by the caller of the Python script, that needs to be sent back as an integrity check; - status: Test discovery status, can be "success" or "error"; - tests: Discoverered tests if any, not present otherwise. Note that the status can be "error" but the payload can still contain tests; - - errors: Discovery errors if any, not present otherwise. + - error: Discovery error if any, not present otherwise. Payload format for a successful discovery: { @@ -86,30 +86,31 @@ def discover_tests( Payload format when there are errors: { "cwd": - "errors": [list of errors] + "": [list of errors] "status": "error", } """ cwd = os.path.abspath(start_dir) - payload: PayloadDict = {"cwd": cwd, "status": "success"} + payload: PayloadDict = {"cwd": cwd, "status": "success", "tests": None} tests = None - errors: List[str] = [] + error: List[str] = [] try: loader = unittest.TestLoader() suite = loader.discover(start_dir, pattern, top_level_dir) - tests, errors = build_test_tree(suite, cwd) # test tree built succesfully here. + tests, error = build_test_tree(suite, cwd) # test tree built succesfully here. except Exception: - errors.append(traceback.format_exc()) + error.append(traceback.format_exc()) - if tests is not None: - payload["tests"] = tests + # Still include the tests in the payload even if there are errors so that the TS + # side can determine if it is from run or discovery. + payload["tests"] = tests if tests is not None else None - if len(errors): + if len(error): payload["status"] = "error" - payload["errors"] = errors + payload["error"] = error return payload diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index d9d5f41d624b..aa50cbc5d09f 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -165,7 +165,7 @@ class TestExecutionStatus(str, enum.Enum): class PayloadDict(TypedDict): cwd: str status: TestExecutionStatus - result: NotRequired[TestResultTypeAlias] + result: Optional[TestResultTypeAlias] not_found: NotRequired[List[str]] error: NotRequired[str] @@ -185,7 +185,7 @@ def run_tests( cwd = os.path.abspath(start_dir) status = TestExecutionStatus.error error = None - payload: PayloadDict = {"cwd": cwd, "status": status} + payload: PayloadDict = {"cwd": cwd, "status": status, "result": None} try: # If it's a file, split path and file name. @@ -312,4 +312,5 @@ def send_run_data(raw_data, port, uuid): "cwd": cwd, "status": status, "error": "No test ids received from buffer", + "result": None, } diff --git a/pythonFiles/unittestadapter/utils.py b/pythonFiles/unittestadapter/utils.py index 9c8b896a8d6e..a461baf7d870 100644 --- a/pythonFiles/unittestadapter/utils.py +++ b/pythonFiles/unittestadapter/utils.py @@ -151,14 +151,14 @@ def build_test_tree( "id_": } """ - errors = [] + error = [] directory_path = pathlib.PurePath(test_directory) root = build_test_node(test_directory, directory_path.name, TestNodeTypeEnum.folder) for test_case in get_test_case(suite): test_id = test_case.id() if test_id.startswith("unittest.loader._FailedTest"): - errors.append(str(test_case._exception)) # type: ignore + error.append(str(test_case._exception)) # type: ignore else: # Get the static test path components: filename, class name and function name. components = test_id.split(".") @@ -206,7 +206,7 @@ def build_test_tree( if not root["children"]: root = None - return root, errors + return root, error def parse_unittest_args(args: List[str]) -> Tuple[str, str, Union[str, None]]: From 9c0af357e6e402742a764531d444f040ba4b9f97 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 26 Jun 2023 09:05:12 -0700 Subject: [PATCH 133/336] Let pylance use SharedArrayBuffer cancellation in the browser (#21482) Dirk implemented a new cancellation method for the browser in the LSP library. About 6 months ago. We use this in the browser to provide cancellation. Since we moved creation of the pylance language client to Python, this is picking up the LSP library currently in the Python extension and it was before this change by Dirk. This change moves the LSP library up to the latest. --- package-lock.json | 114 +++++++++--------- package.json | 10 +- .../lspInteractiveWindowMiddlewareAddon.ts | 2 +- 3 files changed, 60 insertions(+), 66 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49376d6e6cab..f41a215a083e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,9 +38,9 @@ "vscode-debugadapter": "^1.28.0", "vscode-debugprotocol": "^1.28.0", "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageclient": "8.0.2-next.5", - "vscode-languageserver": "8.0.2-next.5", - "vscode-languageserver-protocol": "3.17.2-next.6", + "vscode-languageclient": "^8.1.0", + "vscode-languageserver": "^8.1.0", + "vscode-languageserver-protocol": "^3.17.3", "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", @@ -14318,29 +14318,18 @@ } }, "node_modules/vscode-languageclient": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2-next.5.tgz", - "integrity": "sha512-g87RJLHz0XlRyk6DOTbAk4JHcj8CKggXy4JiFL7OlhETkcYzTOR8d+Qdb4GqZr37PDs1Cl21omtTNK5LyR/RQg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", + "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", "dependencies": { - "minimatch": "^3.0.4", - "semver": "^7.3.5", - "vscode-languageserver-protocol": "3.17.2-next.6" + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.3" }, "engines": { "vscode": "^1.67.0" } }, - "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/vscode-languageclient/node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -14356,29 +14345,37 @@ } }, "node_modules/vscode-languageserver": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.0.2-next.5.tgz", - "integrity": "sha512-2ZDb7O/4atS9mJKufPPz637z+51kCyZfgnobFW5eSrUdS3c0UB/nMS4Ng1EavYTX84GVaVMKCrmP0f2ceLmR0A==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", + "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", "dependencies": { - "vscode-languageserver-protocol": "3.17.2-next.6" + "vscode-languageserver-protocol": "3.17.3" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.2-next.6", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2-next.6.tgz", - "integrity": "sha512-WtsebNOOkWyNn4oFYoAMPC8Q/ZDoJ/K7Ja53OzTixiitvrl/RpXZETrtzH79R8P5kqCyx6VFBPb6KQILJfkDkA==", + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", "dependencies": { - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageserver-types": "3.17.2-next.2" + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" } }, - "node_modules/vscode-languageserver-protocol/node_modules/vscode-languageserver-types": { - "version": "3.17.2-next.2", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2-next.2.tgz", - "integrity": "sha512-TiAkLABgqkVWdAlC3XlOfdhdjIAdVU4YntPUm9kKGbXr+MGwpVnKz2KZMNBcvG0CFx8Hi8qliL0iq+ndPB720w==" + "node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, "node_modules/vscode-tas-client": { "version": "0.1.63", @@ -26397,23 +26394,15 @@ "integrity": "sha512-sbbvGSWja7NVBLHPGawtgezc8DHYJaP4qfr/AaJiyDapWcSFtHyPtm18+LnYMLTmB7bhOUW/lf5PeeuLpP6bKA==" }, "vscode-languageclient": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2-next.5.tgz", - "integrity": "sha512-g87RJLHz0XlRyk6DOTbAk4JHcj8CKggXy4JiFL7OlhETkcYzTOR8d+Qdb4GqZr37PDs1Cl21omtTNK5LyR/RQg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", + "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", "requires": { - "minimatch": "^3.0.4", - "semver": "^7.3.5", - "vscode-languageserver-protocol": "3.17.2-next.6" + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.3" }, "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -26425,29 +26414,34 @@ } }, "vscode-languageserver": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.0.2-next.5.tgz", - "integrity": "sha512-2ZDb7O/4atS9mJKufPPz637z+51kCyZfgnobFW5eSrUdS3c0UB/nMS4Ng1EavYTX84GVaVMKCrmP0f2ceLmR0A==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", + "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", "requires": { - "vscode-languageserver-protocol": "3.17.2-next.6" + "vscode-languageserver-protocol": "3.17.3" } }, "vscode-languageserver-protocol": { - "version": "3.17.2-next.6", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2-next.6.tgz", - "integrity": "sha512-WtsebNOOkWyNn4oFYoAMPC8Q/ZDoJ/K7Ja53OzTixiitvrl/RpXZETrtzH79R8P5kqCyx6VFBPb6KQILJfkDkA==", + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", "requires": { - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageserver-types": "3.17.2-next.2" + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" }, "dependencies": { - "vscode-languageserver-types": { - "version": "3.17.2-next.2", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2-next.2.tgz", - "integrity": "sha512-TiAkLABgqkVWdAlC3XlOfdhdjIAdVU4YntPUm9kKGbXr+MGwpVnKz2KZMNBcvG0CFx8Hi8qliL0iq+ndPB720w==" + "vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==" } } }, + "vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + }, "vscode-tas-client": { "version": "0.1.63", "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.63.tgz", diff --git a/package.json b/package.json index ce407ed9ee4d..85d4baa31e4b 100644 --- a/package.json +++ b/package.json @@ -2008,9 +2008,9 @@ "vscode-debugadapter": "^1.28.0", "vscode-debugprotocol": "^1.28.0", "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageclient": "8.0.2-next.5", - "vscode-languageserver": "8.0.2-next.5", - "vscode-languageserver-protocol": "3.17.2-next.6", + "vscode-languageclient": "^8.1.0", + "vscode-languageserver": "^8.1.0", + "vscode-languageserver-protocol": "^3.17.3", "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", @@ -2038,13 +2038,13 @@ "@types/tmp": "^0.0.33", "@types/uuid": "^8.3.4", "@types/vscode": "^1.75.0", - "@vscode/vsce": "^2.18.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", "@vscode/test-electron": "^2.1.3", + "@vscode/vsce": "^2.18.0", "bent": "^7.3.12", "chai": "^4.1.2", "chai-arrays": "^2.0.0", @@ -2097,4 +2097,4 @@ "webpack-require-from": "^1.8.6", "yargs": "^15.3.1" } -} \ No newline at end of file +} diff --git a/src/client/activation/node/lspInteractiveWindowMiddlewareAddon.ts b/src/client/activation/node/lspInteractiveWindowMiddlewareAddon.ts index c68ebfe5a59c..0a40915d98eb 100644 --- a/src/client/activation/node/lspInteractiveWindowMiddlewareAddon.ts +++ b/src/client/activation/node/lspInteractiveWindowMiddlewareAddon.ts @@ -117,7 +117,7 @@ export class LspInteractiveWindowMiddlewareAddon implements Middleware, Disposab } private static _asTextContentChange(event: TextDocumentChangeEvent, c2pConverter: Converter): TextContent { - const params = c2pConverter.asChangeTextDocumentParams(event); + const params = c2pConverter.asChangeTextDocumentParams(event, event.document.uri, event.document.version); return { document: params.textDocument, changes: params.contentChanges }; } From 304dffa119ba6e8c9d61d8f64a1027869f32da1c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 26 Jun 2023 13:30:55 -0700 Subject: [PATCH 134/336] add cwd to python path during unittest sendCommand (#21490) fixes https://github.com/microsoft/vscode-python/issues/21476 --- .../testing/testController/common/server.ts | 6 +- .../testController/server.unit.test.ts | 708 +++++++++--------- 2 files changed, 364 insertions(+), 350 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 32829e355ccb..f854371ffc35 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -4,6 +4,7 @@ import * as net from 'net'; import * as crypto from 'crypto'; import { Disposable, Event, EventEmitter } from 'vscode'; +import * as path from 'path'; import { ExecutionFactoryCreateWithEnvironmentOptions, IPythonExecutionFactory, @@ -141,12 +142,15 @@ export class PythonTestServer implements ITestServer, Disposable { async sendCommand(options: TestCommandOptions, runTestIdPort?: string, callback?: () => void): Promise { const { uuid } = options; + + const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [options.cwd, ...pythonPathParts].join(path.delimiter); const spawnOptions: SpawnOptions = { token: options.token, cwd: options.cwd, throwOnStdErr: true, outputChannel: options.outChannel, - extraVariables: {}, + extraVariables: { PYTHONPATH: pythonPathCommand }, }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 38b71992aefb..1131c26c6444 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -1,349 +1,359 @@ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. - -// import * as assert from 'assert'; -// import * as net from 'net'; -// import * as sinon from 'sinon'; -// import * as crypto from 'crypto'; -// import { Uri } from 'vscode'; -// import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; -// import { PythonTestServer } from '../../../client/testing/testController/common/server'; -// import { ITestDebugLauncher } from '../../../client/testing/common/types'; -// import { createDeferred } from '../../../client/common/utils/async'; - -// suite('Python Test Server', () => { -// const fakeUuid = 'fake-uuid'; - -// let stubExecutionFactory: IPythonExecutionFactory; -// let stubExecutionService: IPythonExecutionService; -// let server: PythonTestServer; -// let sandbox: sinon.SinonSandbox; -// let execArgs: string[]; -// let v4Stub: sinon.SinonStub; -// let debugLauncher: ITestDebugLauncher; - -// setup(() => { -// sandbox = sinon.createSandbox(); -// v4Stub = sandbox.stub(crypto, 'randomUUID'); - -// v4Stub.returns(fakeUuid); -// stubExecutionService = ({ -// exec: (args: string[]) => { -// execArgs = args; -// return Promise.resolve({ stdout: '', stderr: '' }); -// }, -// } as unknown) as IPythonExecutionService; - -// stubExecutionFactory = ({ -// createActivatedEnvironment: () => Promise.resolve(stubExecutionService), -// } as unknown) as IPythonExecutionFactory; -// }); - -// teardown(() => { -// sandbox.restore(); -// execArgs = []; -// server.dispose(); -// }); - -// // test('sendCommand should add the port to the command being sent', async () => { -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // }; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); - -// // await server.sendCommand(options); -// // const port = server.getPort(); - -// // assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); -// // }); - -// // test('sendCommand should write to an output channel if it is provided as an option', async () => { -// // const output: string[] = []; -// // const outChannel = { -// // appendLine: (str: string) => { -// // output.push(str); -// // }, -// // } as OutputChannel; -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // outChannel, -// // }; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); - -// // await server.sendCommand(options); - -// // const port = server.getPort(); -// // const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); - -// // assert.deepStrictEqual(output, [expected]); -// // }); - -// // test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { -// // let eventData: { status: string; errors: string[] }; -// // stubExecutionService = ({ -// // exec: () => { -// // throw new Error('Failed to execute'); -// // }, -// // } as unknown) as IPythonExecutionService; - -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // }; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); - -// // server.onDataReceived(({ data }) => { -// // eventData = JSON.parse(data); -// // }); - -// // await server.sendCommand(options); - -// // assert.deepStrictEqual(eventData!.status, 'error'); -// // assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); -// // }); - -// // test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { -// // let eventData: string | undefined; -// // const client = new net.Socket(); -// // const deferred = createDeferred(); - -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // }; - -// // stubExecutionService = ({ -// // exec: async () => { -// // client.connect(server.getPort()); -// // return Promise.resolve({ stdout: '', stderr: '' }); -// // }, -// // } as unknown) as IPythonExecutionService; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); -// // server.onDataReceived(({ data }) => { -// // eventData = data; -// // deferred.resolve(); -// // }); - -// // client.on('connect', () => { -// // console.log('Socket connected, local port:', client.localPort); -// // client.write('malformed data'); -// // client.end(); -// // }); -// // client.on('error', (error) => { -// // console.log('Socket connection error:', error); -// // }); - -// // await server.sendCommand(options); -// // await deferred.promise; -// // assert.deepStrictEqual(eventData, ''); -// // }); - -// // test('If the server doesnt recognize the UUID it should ignore it', async () => { -// // let eventData: string | undefined; -// // const client = new net.Socket(); -// // const deferred = createDeferred(); - -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // }; - -// // stubExecutionService = ({ -// // exec: async () => { -// // client.connect(server.getPort()); -// // return Promise.resolve({ stdout: '', stderr: '' }); -// // }, -// // } as unknown) as IPythonExecutionService; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); -// // server.onDataReceived(({ data }) => { -// // eventData = data; -// // deferred.resolve(); -// // }); - -// // client.on('connect', () => { -// // console.log('Socket connected, local port:', client.localPort); -// // client.write('{"Request-uuid": "unknown-uuid"}'); -// // client.end(); -// // }); -// // client.on('error', (error) => { -// // console.log('Socket connection error:', error); -// // }); - -// // await server.sendCommand(options); -// // await deferred.promise; -// // assert.deepStrictEqual(eventData, ''); -// // }); - -// // required to have "tests" or "results" -// // the heading length not being equal and yes being equal -// // multiple payloads -// // test('Error if payload does not have a content length header', async () => { -// // let eventData: string | undefined; -// // const client = new net.Socket(); -// // const deferred = createDeferred(); - -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // }; - -// // stubExecutionService = ({ -// // exec: async () => { -// // client.connect(server.getPort()); -// // return Promise.resolve({ stdout: '', stderr: '' }); -// // }, -// // } as unknown) as IPythonExecutionService; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); -// // server.onDataReceived(({ data }) => { -// // eventData = data; -// // deferred.resolve(); -// // }); - -// // client.on('connect', () => { -// // console.log('Socket connected, local port:', client.localPort); -// // client.write('{"not content length": "5"}'); -// // client.end(); -// // }); -// // client.on('error', (error) => { -// // console.log('Socket connection error:', error); -// // }); - -// // await server.sendCommand(options); -// // await deferred.promise; -// // assert.deepStrictEqual(eventData, ''); -// // }); - -// const testData = [ -// { -// testName: 'fires discovery correctly on test payload', -// payload: `Content-Length: 52 -// Content-Type: application/json -// Request-uuid: UUID_HERE - -// {"cwd": "path", "status": "success", "tests": "xyz"}`, -// expectedResult: '{"cwd": "path", "status": "success", "tests": "xyz"}', -// }, -// // Add more test data as needed -// ]; - -// testData.forEach(({ testName, payload, expectedResult }) => { -// test(`test: ${testName}`, async () => { -// // Your test logic here -// let eventData: string | undefined; -// const client = new net.Socket(); -// const deferred = createDeferred(); - -// const options = { -// command: { script: 'myscript', args: ['-foo', 'foo'] }, -// workspaceFolder: Uri.file('/foo/bar'), -// cwd: '/foo/bar', -// uuid: fakeUuid, -// }; - -// stubExecutionService = ({ -// exec: async () => { -// client.connect(server.getPort()); -// return Promise.resolve({ stdout: '', stderr: '' }); -// }, -// } as unknown) as IPythonExecutionService; - -// server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// await server.serverReady(); -// const uuid = server.createUUID(); -// payload = payload.replace('UUID_HERE', uuid); -// server.onDiscoveryDataReceived(({ data }) => { -// eventData = data; -// deferred.resolve(); -// }); - -// client.on('connect', () => { -// console.log('Socket connected, local port:', client.localPort); -// client.write(payload); -// client.end(); -// }); -// client.on('error', (error) => { -// console.log('Socket connection error:', error); -// }); - -// await server.sendCommand(options); -// await deferred.promise; -// assert.deepStrictEqual(eventData, expectedResult); -// }); -// }); - -// test('Calls run resolver if the result header is in the payload', async () => { -// let eventData: string | undefined; -// const client = new net.Socket(); -// const deferred = createDeferred(); - -// const options = { -// command: { script: 'myscript', args: ['-foo', 'foo'] }, -// workspaceFolder: Uri.file('/foo/bar'), -// cwd: '/foo/bar', -// uuid: fakeUuid, -// }; - -// stubExecutionService = ({ -// exec: async () => { -// client.connect(server.getPort()); -// return Promise.resolve({ stdout: '', stderr: '' }); -// }, -// } as unknown) as IPythonExecutionService; - -// server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// await server.serverReady(); -// const uuid = server.createUUID(); -// server.onRunDataReceived(({ data }) => { -// eventData = data; -// deferred.resolve(); -// }); - -// const payload = `Content-Length: 87 -// Content-Type: application/json -// Request-uuid: ${uuid} - -// {"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}`; - -// client.on('connect', () => { -// console.log('Socket connected, local port:', client.localPort); -// client.write(payload); -// client.end(); -// }); -// client.on('error', (error) => { -// console.log('Socket connection error:', error); -// }); - -// await server.sendCommand(options); -// await deferred.promise; -// console.log('event data', eventData); -// const expectedResult = -// '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; -// assert.deepStrictEqual(eventData, expectedResult); -// }); -// }); +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as net from 'net'; +import * as sinon from 'sinon'; +import * as crypto from 'crypto'; +import { OutputChannel, Uri } from 'vscode'; +import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions } from '../../../client/common/process/types'; +import { PythonTestServer } from '../../../client/testing/testController/common/server'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { createDeferred } from '../../../client/common/utils/async'; + +suite('Python Test Server', () => { + const fakeUuid = 'fake-uuid'; + + let stubExecutionFactory: IPythonExecutionFactory; + let stubExecutionService: IPythonExecutionService; + let server: PythonTestServer; + let sandbox: sinon.SinonSandbox; + let execArgs: string[]; + let spawnOptions: SpawnOptions; + let v4Stub: sinon.SinonStub; + let debugLauncher: ITestDebugLauncher; + + setup(() => { + sandbox = sinon.createSandbox(); + v4Stub = sandbox.stub(crypto, 'randomUUID'); + + v4Stub.returns(fakeUuid); + stubExecutionService = ({ + exec: (args: string[], spawnOptionsProvided: SpawnOptions) => { + execArgs = args; + spawnOptions = spawnOptionsProvided; + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + stubExecutionFactory = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService), + } as unknown) as IPythonExecutionFactory; + }); + + teardown(() => { + sandbox.restore(); + execArgs = []; + server.dispose(); + }); + + test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + const expectedSpawnOptions = { + cwd: '/foo/bar', + outputChannel: undefined, + token: undefined, + throwOnStdErr: true, + extraVariables: { PYTHONPATH: '/foo/bar', RUN_TEST_IDS_PORT: '56789' }, + } as SpawnOptions; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + + await server.sendCommand(options, '56789'); + const port = server.getPort(); + + assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); + assert.deepStrictEqual(spawnOptions, expectedSpawnOptions); + }); + + test('sendCommand should write to an output channel if it is provided as an option', async () => { + const output: string[] = []; + const outChannel = { + appendLine: (str: string) => { + output.push(str); + }, + } as OutputChannel; + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + outChannel, + }; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + + await server.sendCommand(options); + + const port = server.getPort(); + const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); + + assert.deepStrictEqual(output, [expected]); + }); + + test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { + let eventData: { status: string; errors: string[] }; + stubExecutionService = ({ + exec: () => { + throw new Error('Failed to execute'); + }, + } as unknown) as IPythonExecutionService; + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + + server.onDataReceived(({ data }) => { + eventData = JSON.parse(data); + }); + + await server.sendCommand(options); + + assert.deepStrictEqual(eventData!.status, 'error'); + assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); + }); + + test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { + let eventData: string | undefined; + const client = new net.Socket(); + const deferred = createDeferred(); + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + server.onDataReceived(({ data }) => { + eventData = data; + deferred.resolve(); + }); + + client.on('connect', () => { + console.log('Socket connected, local port:', client.localPort); + client.write('malformed data'); + client.end(); + }); + client.on('error', (error) => { + console.log('Socket connection error:', error); + }); + + await server.sendCommand(options); + await deferred.promise; + assert.deepStrictEqual(eventData, ''); + }); + + test('If the server doesnt recognize the UUID it should ignore it', async () => { + let eventData: string | undefined; + const client = new net.Socket(); + const deferred = createDeferred(); + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + server.onDataReceived(({ data }) => { + eventData = data; + deferred.resolve(); + }); + + client.on('connect', () => { + console.log('Socket connected, local port:', client.localPort); + client.write('{"Request-uuid": "unknown-uuid"}'); + client.end(); + }); + client.on('error', (error) => { + console.log('Socket connection error:', error); + }); + + await server.sendCommand(options); + await deferred.promise; + assert.deepStrictEqual(eventData, ''); + }); + + // required to have "tests" or "results" + // the heading length not being equal and yes being equal + // multiple payloads + test('Error if payload does not have a content length header', async () => { + let eventData: string | undefined; + const client = new net.Socket(); + const deferred = createDeferred(); + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + server.onDataReceived(({ data }) => { + eventData = data; + deferred.resolve(); + }); + + client.on('connect', () => { + console.log('Socket connected, local port:', client.localPort); + client.write('{"not content length": "5"}'); + client.end(); + }); + client.on('error', (error) => { + console.log('Socket connection error:', error); + }); + + await server.sendCommand(options); + await deferred.promise; + assert.deepStrictEqual(eventData, ''); + }); + + const testData = [ + { + testName: 'fires discovery correctly on test payload', + payload: `Content-Length: 52 +Content-Type: application/json +Request-uuid: UUID_HERE + +{"cwd": "path", "status": "success", "tests": "xyz"}`, + expectedResult: '{"cwd": "path", "status": "success", "tests": "xyz"}', + }, + // Add more test data as needed + ]; + + testData.forEach(({ testName, payload, expectedResult }) => { + test(`test: ${testName}`, async () => { + // Your test logic here + let eventData: string | undefined; + const client = new net.Socket(); + const deferred = createDeferred(); + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + const uuid = server.createUUID(); + payload = payload.replace('UUID_HERE', uuid); + server.onDiscoveryDataReceived(({ data }) => { + eventData = data; + deferred.resolve(); + }); + + client.on('connect', () => { + console.log('Socket connected, local port:', client.localPort); + client.write(payload); + client.end(); + }); + client.on('error', (error) => { + console.log('Socket connection error:', error); + }); + + await server.sendCommand(options); + await deferred.promise; + assert.deepStrictEqual(eventData, expectedResult); + }); + }); + + test('Calls run resolver if the result header is in the payload', async () => { + let eventData: string | undefined; + const client = new net.Socket(); + const deferred = createDeferred(); + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + const uuid = server.createUUID(); + server.onRunDataReceived(({ data }) => { + eventData = data; + deferred.resolve(); + }); + + const payload = `Content-Length: 87 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}`; + + client.on('connect', () => { + console.log('Socket connected, local port:', client.localPort); + client.write(payload); + client.end(); + }); + client.on('error', (error) => { + console.log('Socket connection error:', error); + }); + + await server.sendCommand(options); + await deferred.promise; + console.log('event data', eventData); + const expectedResult = + '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; + assert.deepStrictEqual(eventData, expectedResult); + }); +}); From bd5b622a2456be181d215a1d758a0456412d7c72 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:35:11 -0700 Subject: [PATCH 135/336] Dev Container for VS Code Python Extension (#21435) Contains dockerfile, devcontainer.json, post run script needed to create dev container. Base image used is the latest version of Fedora, and the docker file contains installation for: python version(s), node, npm, conda. --------- authored-by: Anthony Kim --- .devcontainer/Dockerfile | 26 ++++++++++++++++++++++++++ .devcontainer/devcontainer.json | 29 +++++++++++++++++++++++++++++ scripts/postCreateCommand.sh | 15 +++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 scripts/postCreateCommand.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000000..f5f49445b399 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,26 @@ +# This image will serve as a starting point for devcontainer.json. +# Get latest image of Fedora as the base image. +FROM docker.io/library/fedora:latest + +# Install supported python versions and nodejs. +RUN dnf -y --nodocs install /usr/bin/{python3.7,python3.8,python3.9,python3.10,python3.11,git,conda,clang} && \ + dnf clean all + +ENV NVM_VERSION=0.39.3 +ENV NODE_VERSION=16.17.1 +ENV NPM_VERSION=8.19.3 + +# Installation instructions from https://github.com/nvm-sh/nvm . +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v$NVM_VERSION/install.sh | bash +RUN export NVM_DIR="$HOME/.nvm" && \ + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ + [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" && \ + nvm install $NODE_VERSION && \ + npm install -g npm@$NPM_VERSION + +# For clean open source builds. +ENV DISABLE_TRANSLATIONS=true + + + + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000000..6435ba5bbda8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,29 @@ +// For format details, see https://aka.ms/devcontainer.json. +{ + "name": "VS Code Python Dev Container", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "build": { + "dockerfile": "./Dockerfile", + "context": ".." + }, + "customizations": { + "vscode": { + "extensions": [ + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "ms-python.python", + "ms-python.black-formatter", + "ms-python.vscode-pylance", + "charliermarsh.ruff" + ] + } + }, + // Commands to execute on container creation,start. + "postCreateCommand": "bash scripts/postCreateCommand.sh", + // Environment variable placed inside containerEnv following: https://containers.dev/implementors/json_reference/#general-properties + "containerEnv": { + "CI_PYTHON_PATH": "/workspaces/vscode-python/.venv/bin/python" + } + +} diff --git a/scripts/postCreateCommand.sh b/scripts/postCreateCommand.sh new file mode 100644 index 000000000000..85462caf7fad --- /dev/null +++ b/scripts/postCreateCommand.sh @@ -0,0 +1,15 @@ +#!/bin/bash +npm ci +# Create Virutal environment. +python3.7 -m venv /workspaces/vscode-python/.venv + +# Activate Virtual environment. +source /workspaces/vscode-python/.venv/bin/activate + +# Install required Python libraries. +npx gulp installPythonLibs + +# Install testing requirement using python in .venv . +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/test-requirements.txt +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/smoke-test-requirements.txt +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/functional-test-requirements.txt From 0307e91f9943d660d5c03ae01f8c3c78961dde4e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 26 Jun 2023 18:16:15 -0700 Subject: [PATCH 136/336] split subtest name to be unique second half (#21497) fixes: https://github.com/microsoft/vscode-python/issues/21461 --- .../testController/common/resultResolver.ts | 4 +- .../resultResolver.unit.test.ts | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 9b2ad6abdf78..8e08ffbeedf5 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -160,6 +160,7 @@ export class PythonResultResolver implements ITestResultResolver { } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { // split on " " since the subtest ID has the parent test ID in the first part of the ID. const parentTestCaseId = keyTemp.split(' ')[0]; + const subtestId = keyTemp.split(' ')[1]; const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); const data = rawTestExecData.result[keyTemp]; // find the subtest's parent test item @@ -173,7 +174,6 @@ export class PythonResultResolver implements ITestResultResolver { // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); } - const subtestId = keyTemp; const subTestItem = this.testController?.createTestItem(subtestId, subtestId); runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); // create a new test item for the subtest @@ -197,6 +197,7 @@ export class PythonResultResolver implements ITestResultResolver { } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { // split on " " since the subtest ID has the parent test ID in the first part of the ID. const parentTestCaseId = keyTemp.split(' ')[0]; + const subtestId = keyTemp.split(' ')[1]; const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); // find the subtest's parent test item @@ -210,7 +211,6 @@ export class PythonResultResolver implements ITestResultResolver { // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); } - const subtestId = keyTemp; const subTestItem = this.testController?.createTestItem(subtestId, subtestId); // create a new test item for the subtest if (subTestItem) { diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 94ec1cc10a9a..0fc7eb3ab353 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -4,6 +4,7 @@ import { TestController, Uri, TestItem, CancellationToken, TestRun, TestItemCollection, Range } from 'vscode'; import * as typemoq from 'typemoq'; import * as sinon from 'sinon'; +import * as assert from 'assert'; import { TestProvider } from '../../../client/testing/types'; import { DiscoveredTestNode, @@ -205,6 +206,58 @@ suite('Result Resolver tests', () => { teardown(() => { sinon.restore(); }); + test('resolveExecution create correct subtest item for unittest', async () => { + // test specific constants used expected values + sinon.stub(testItemUtilities, 'clearAllChildren').callsFake(() => undefined); + testProvider = 'unittest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + const mockSubtestItem = createMockTestItem('parentTest subTest'); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + // creates a mock test item with a space which will be used to split the runId + resultResolver.runIdToVSid.set('parentTest subTest', 'parentTest subTest'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('parentTest', mockTestItem2); + resultResolver.runIdToTestItem.set('parentTest subTest', mockSubtestItem); + + let generatedId: string | undefined; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((id: string) => { + generatedId = id; + console.log('createTestItem function called with id:', id); + }) + .returns(() => ({ id: 'id_this', label: 'label_this', uri: workspaceUri } as TestItem)); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + 'parentTest subTest': { + test: 'test', + outcome: 'subtest-success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + assert.ok(generatedId); + assert.strictEqual(generatedId, 'subTest'); + }); test('resolveExecution handles failed tests correctly', async () => { // test specific constants used expected values testProvider = 'pytest'; From 84f334f5a58e00377296168dc50197ac34bc73b2 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 26 Jun 2023 19:45:39 -0700 Subject: [PATCH 137/336] fix bug that caused dup in param test cases (#21498) fixes https://github.com/microsoft/vscode-python/issues/21491 --- .../.data/param_same_name/test_param1.py | 8 ++ .../.data/param_same_name/test_param2.py | 8 ++ .../expected_discovery_test_output.py | 111 ++++++++++++++++++ .../tests/pytestadapter/test_discovery.py | 4 + .../tests/pytestadapter/test_execution.py | 7 -- pythonFiles/vscode_pytest/__init__.py | 9 +- 6 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/param_same_name/test_param1.py create mode 100644 pythonFiles/tests/pytestadapter/.data/param_same_name/test_param2.py diff --git a/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param1.py b/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param1.py new file mode 100644 index 000000000000..a16d0f49f411 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param1.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +@pytest.mark.parametrize("num", ["a", "b", "c"]) +def test_odd_even(num): + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param2.py b/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param2.py new file mode 100644 index 000000000000..c0ea8010e359 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param2.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +@pytest.mark.parametrize("num", range(1, 4)) +def test_odd_even(num): + assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 494d8794ca73..33440a6d42fa 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -474,3 +474,114 @@ ], "id_": TEST_DATA_PATH_STR, } + +# This is the expected output for the param_same_name tests. +# └── param_same_name +# └── test_param1.py +# └── test_odd_even +# └── [a] +# └── [b] +# └── [c] +# └── test_param2.py +# └── test_odd_even +# └── [1] +# └── [2] +# └── [3] +param1_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param1.py") +param2_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param2.py") +param_same_name_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "param_same_name", + "path": os.fspath(TEST_DATA_PATH / "param_same_name"), + "type_": "folder", + "id_": os.fspath(TEST_DATA_PATH / "param_same_name"), + "children": [ + { + "name": "test_param1.py", + "path": param1_path, + "type_": "file", + "id_": param1_path, + "children": [ + { + "name": "test_odd_even", + "path": param1_path, + "type_": "function", + "children": [ + { + "name": "[a]", + "path": param1_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param1.py::test_odd_even[a]", + "runID": "param_same_name/test_param1.py::test_odd_even[a]", + }, + { + "name": "[b]", + "path": param1_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param1.py::test_odd_even[b]", + "runID": "param_same_name/test_param1.py::test_odd_even[b]", + }, + { + "name": "[c]", + "path": param1_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param1.py::test_odd_even[c]", + "runID": "param_same_name/test_param1.py::test_odd_even[c]", + }, + ], + "id_": "param_same_name/test_param1.py::test_odd_even", + } + ], + }, + { + "name": "test_param2.py", + "path": param2_path, + "type_": "file", + "id_": param2_path, + "children": [ + { + "name": "test_odd_even", + "path": param2_path, + "type_": "function", + "children": [ + { + "name": "[1]", + "path": param2_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param2.py::test_odd_even[1]", + "runID": "param_same_name/test_param2.py::test_odd_even[1]", + }, + { + "name": "[2]", + "path": param2_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param2.py::test_odd_even[2]", + "runID": "param_same_name/test_param2.py::test_odd_even[2]", + }, + { + "name": "[3]", + "path": param2_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param2.py::test_odd_even[3]", + "runID": "param_same_name/test_param2.py::test_odd_even[3]", + }, + ], + "id_": "param_same_name/test_param2.py::test_odd_even", + } + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 7f2355129c65..02ea1ddcd871 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -87,6 +87,10 @@ def test_parameterized_error_collect(): @pytest.mark.parametrize( "file, expected_const", [ + ( + "param_same_name", + expected_discovery_test_output.param_same_name_expected_output, + ), ( "parametrize_tests.py", expected_discovery_test_output.parametrize_tests_expected_output, diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index d11766027a91..400ef9f883bc 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -125,12 +125,6 @@ def test_bad_id_error_execution(): ], expected_execution_test_output.doctest_pytest_expected_execution_output, ), - ( - [ - "", - ], - expected_execution_test_output.no_test_ids_pytest_execution_expected_output, - ), ], ) def test_pytest_execution(test_ids, expected_const): @@ -147,7 +141,6 @@ def test_pytest_execution(test_ids, expected_const): 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. 10. doctest_pytest_expected_execution_output: test run on doctest file. - 11. no_test_ids_pytest_execution_expected_output: test run with no inputted test ids. Keyword arguments: diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 1178abdb93d6..2aa2f6052d79 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -288,11 +288,12 @@ def build_test_tree(session: pytest.Session) -> TestNode: elif hasattr(test_case, "callspec"): # This means it is a parameterized test. function_name: str = "" # parameterized test cases cut the repetitive part of the name off. - name_split = test_node["name"].split("[")[1] - test_node["name"] = "[" + name_split + name_split = test_node["name"].split("[") + test_node["name"] = "[" + name_split[1] + parent_path = os.fspath(test_case.path) try: function_name = test_case.originalname # type: ignore - function_test_case = function_nodes_dict[function_name] + function_test_case = function_nodes_dict[parent_path] except AttributeError: # actual error has occurred ERRORS.append( f"unable to find original name for {test_case.name} with parameterization detected." @@ -304,7 +305,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: function_test_case: TestNode = create_parameterized_function_node( function_name, test_case.path, test_case.nodeid ) - function_nodes_dict[function_name] = function_test_case + function_nodes_dict[parent_path] = function_test_case function_test_case["children"].append(test_node) # Now, add the function node to file node. try: From 8e61977a710a95f00011beee89bcf351ec747400 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 27 Jun 2023 23:24:47 -0700 Subject: [PATCH 138/336] Fix filtering with QuickPick (#21524) Fixes https://github.com/microsoft/vscode-python/issues/21518 --- .../pythonEnvironments/creation/common/workspaceSelection.ts | 2 ++ src/client/pythonEnvironments/creation/provider/condaUtils.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts index d145e38134d1..14246eabd768 100644 --- a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts +++ b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts @@ -68,6 +68,8 @@ export async function pickWorkspaceFolder( placeHolder: CreateEnv.pickWorkspacePlaceholder, ignoreFocusOut: true, canPickMany: options?.allowMultiSelect, + matchOnDescription: true, + matchOnDetail: true, }, options?.token, ); diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts index 4c4e816f18f1..e00a1c8dca09 100644 --- a/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -47,6 +47,8 @@ export async function pickPythonVersion(token?: CancellationToken): Promise Date: Wed, 28 Jun 2023 14:45:33 -0700 Subject: [PATCH 139/336] Add GDPR tag to method property (#21522) --- src/client/telemetry/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 7881ada5653c..50d7ebb0f9b1 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1543,7 +1543,9 @@ export interface IEventNamePropertyMapping { * This event also has a measure, "resultLength", which records the number of completions provided. */ /* __GDPR__ - "jedi_language_server.request" : { "owner": "karthiknadig" } + "jedi_language_server.request" : { + "method": {"classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig"} + } */ [EventName.JEDI_LANGUAGE_SERVER_REQUEST]: unknown; /** From 26748427355bbf7cd0fd2528ea5627c137f6944f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 29 Jun 2023 13:15:00 -0700 Subject: [PATCH 140/336] rewrite should respect settings.testing.cwd (#21539) fixes https://github.com/microsoft/vscode-python/issues/21531 --- .../pytest/pytestDiscoveryAdapter.ts | 3 +- .../pytest/pytestExecutionAdapter.ts | 7 +-- .../unittest/testDiscoveryAdapter.ts | 8 ++- .../unittest/testExecutionAdapter.ts | 10 ++-- .../pytestDiscoveryAdapter.unit.test.ts | 5 +- .../pytestExecutionAdapter.unit.test.ts | 49 +++++++++++++++++++ .../testDiscoveryAdapter.unit.test.ts | 37 ++++++++++++++ .../testExecutionAdapter.unit.test.ts | 39 +++++++++++++++ 8 files changed, 141 insertions(+), 17 deletions(-) diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 211c322d67f1..b83224d4161b 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -55,12 +55,13 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const uuid = this.testServer.createUUID(uri.fsPath); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); const spawnOptions: SpawnOptions = { - cwd: uri.fsPath, + cwd, throwOnStdErr: true, extraVariables: { PYTHONPATH: pythonPathCommand, diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index b68b80945ef3..a75a6089627c 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -84,12 +84,13 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { this.configSettings.isTestExecution(); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); const spawnOptions: SpawnOptions = { - cwd: uri.fsPath, + cwd, throwOnStdErr: true, extraVariables: { PYTHONPATH: pythonPathCommand, @@ -131,7 +132,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const pytestPort = this.testServer.getPort().toString(); const pytestUUID = uuid.toString(); const launchOptions: LaunchOptions = { - cwd: uri.fsPath, + cwd, args: testArgs, token: spawnOptions.token, testProvider: PYTEST_PROVIDER, @@ -156,7 +157,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { return Promise.reject(ex); } - const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + const executionPayload: ExecutionTestPayload = { cwd, status: 'success', error: '' }; return executionPayload; } } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 69438e341f14..6deca55117ea 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -19,8 +19,6 @@ import { * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. */ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { - private cwd: string | undefined; - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, @@ -31,16 +29,16 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { public async discoverTests(uri: Uri): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const command = buildDiscoveryCommand(unittestArgs); - this.cwd = uri.fsPath; const uuid = this.testServer.createUUID(uri.fsPath); const options: TestCommandOptions = { workspaceFolder: uri, command, - cwd: this.cwd, + cwd, uuid, outChannel: this.outputChannel, }; @@ -57,7 +55,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { - cwd: uri.fsPath, + cwd, status: 'success', }; return discoveryPayload; diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index bf3038817f8b..4cab941c2608 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -23,8 +23,6 @@ import { startTestIdServer } from '../common/utils'; */ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { - private cwd: string | undefined; - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, @@ -62,15 +60,15 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { debugBool?: boolean, ): Promise { const settings = this.configSettings.getSettings(uri); - const { cwd, unittestArgs } = settings.testing; + const { unittestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const command = buildExecutionCommand(unittestArgs); - this.cwd = cwd || uri.fsPath; const options: TestCommandOptions = { workspaceFolder: uri, command, - cwd: this.cwd, + cwd, uuid, debugBool, testIds, @@ -87,7 +85,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. - const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + const executionPayload: ExecutionTestPayload = { cwd, status: 'success', error: '' }; return executionPayload; } } diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 0286235be1bf..18212b2d1032 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -105,9 +105,10 @@ suite('pytest test discovery adapter', () => { }); test('Test discovery correctly pulls pytest args from config service settings', async () => { // set up a config service with different pytest args + const expectedPathNew = path.join('other', 'path'); const configServiceNew: IConfigurationService = ({ getSettings: () => ({ - testing: { pytestArgs: ['.', 'abc', 'xyz'] }, + testing: { pytestArgs: ['.', 'abc', 'xyz'], cwd: expectedPathNew }, }), } as unknown) as IConfigurationService; @@ -120,7 +121,7 @@ suite('pytest test discovery adapter', () => { expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); - assert.equal(options.cwd, expectedPath); + assert.equal(options.cwd, expectedPathNew); assert.equal(options.throwOnStdErr, true); return true; }), diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 9f4c41ba8309..44116fd753b0 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -138,6 +138,55 @@ suite('pytest test execution adapter', () => { typeMoq.Times.once(), ); }); + test('pytest execution respects settings.testing.cwd when present', async () => { + const newCwd = path.join('new', 'path'); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], cwd: newCwd }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + const uri = Uri.file(myTestPath); + const uuid = 'uuid123'; + testServer + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); + adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); + await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const expectedArgs = [pathToPythonScript, '--rootdir', myTestPath]; + const expectedExtraVariables = { + PYTHONPATH: pathToPythonFiles, + TEST_UUID: 'uuid123', + TEST_PORT: '12345', + }; + + execService.verify( + (x) => + x.exec( + expectedArgs, + typeMoq.It.is((options) => { + assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.extraVariables?.TEST_UUID, expectedExtraVariables.TEST_UUID); + assert.equal(options.extraVariables?.TEST_PORT, expectedExtraVariables.TEST_PORT); + assert.equal(options.extraVariables?.RUN_TEST_IDS_PORT, '54321'); + assert.equal(options.cwd, newCwd); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); test('Debug launched correctly for pytest', async () => { const uri = Uri.file(myTestPath); const uuid = 'uuid123'; diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index ef21655e93e4..dc883afdf441 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -54,4 +54,41 @@ suite('Unittest test discovery adapter', () => { uuid: '123456789', }); }); + test('DiscoverTests should respect settings.testings.cwd when present', async () => { + let options: TestCommandOptions | undefined; + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'], cwd: '/foo' }, + }), + } as unknown) as IConfigurationService; + + const stubTestServer = ({ + sendCommand(opt: TestCommandOptions): Promise { + delete opt.outChannel; + options = opt; + return Promise.resolve(); + }, + onDiscoveryDataReceived: () => { + // no body + }, + createUUID: () => '123456789', + } as unknown) as ITestServer; + + const uri = Uri.file('/foo/bar'); + const newCwd = '/foo'; + const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'discovery.py'); + + const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + adapter.discoverTests(uri); + + assert.deepStrictEqual(options, { + workspaceFolder: uri, + cwd: newCwd, + command: { + script, + args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'], + }, + uuid: '123456789', + }); + }); }); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index 88126225a177..4d4a8d0ebee4 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -62,4 +62,43 @@ suite('Unittest test execution adapter', () => { assert.deepStrictEqual(options, expectedOptions); }); }); + test('runTests should respect settings.testing.cwd when present', async () => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'], cwd: '/foo' }, + }), + } as unknown) as IConfigurationService; + let options: TestCommandOptions | undefined; + + const stubTestServer = ({ + sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { + delete opt.outChannel; + options = opt; + assert(runTestIdPort !== undefined); + return Promise.resolve(); + }, + onRunDataReceived: () => { + // no body + }, + createUUID: () => '123456789', + } as unknown) as ITestServer; + + const newCwd = '/foo'; + const uri = Uri.file('/foo/bar'); + const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + const testIds = ['test1id', 'test2id']; + adapter.runTests(uri, testIds, false).then(() => { + const expectedOptions: TestCommandOptions = { + workspaceFolder: uri, + command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, + cwd: newCwd, + uuid: '123456789', + debugBool: false, + testIds, + }; + assert.deepStrictEqual(options, expectedOptions); + }); + }); }); From b1931a3e7fbe3379ef1a820bf71e33af7db90168 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 29 Jun 2023 15:31:06 -0700 Subject: [PATCH 141/336] fix bug which merges all parametrize test classes (#21542) fixes https://github.com/microsoft/vscode-python/issues/21541 --- .../pytestadapter/.data/parametrize_tests.py | 7 ++++++ .../expected_discovery_test_output.py | 24 +++++++++++++++++++ pythonFiles/vscode_pytest/__init__.py | 2 +- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py index 9421e0cc0691..6911c9aec7f0 100644 --- a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py +++ b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py @@ -8,3 +8,10 @@ ) def test_adding(actual, expected): assert eval(actual) == expected + + +# Testing pytest with parametrized tests. All three pass. +# The tests ids are parametrize_tests.py::test_under_ten[1] and so on. +@pytest.mark.parametrize("num", range(1, 3)) # test_marker--test_under_ten +def test_under_ten(num): + assert num < 10 diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 33440a6d42fa..0f733c94d141 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -438,6 +438,30 @@ }, ], }, + { + "name": "test_under_ten", + "path": parameterize_tests_path, + "type_": "function", + "children": [ + { + "name": "[1]", + "path": parameterize_tests_path, + "lineno": "15", + "type_": "test", + "id_": "parametrize_tests.py::test_under_ten[1]", + "runID": "parametrize_tests.py::test_under_ten[1]", + }, + { + "name": "[2]", + "path": parameterize_tests_path, + "lineno": "15", + "type_": "test", + "id_": "parametrize_tests.py::test_under_ten[2]", + "runID": "parametrize_tests.py::test_under_ten[2]", + }, + ], + "id_": "parametrize_tests.py::test_under_ten", + }, ], }, ], diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 2aa2f6052d79..3f3aca6a2a64 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -290,7 +290,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: # parameterized test cases cut the repetitive part of the name off. name_split = test_node["name"].split("[") test_node["name"] = "[" + name_split[1] - parent_path = os.fspath(test_case.path) + parent_path = os.fspath(test_case.path) + "::" + name_split[0] try: function_name = test_case.originalname # type: ignore function_test_case = function_nodes_dict[parent_path] From 79bb94f310162df6148141d05d27e1079db1237a Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 29 Jun 2023 15:41:36 -0700 Subject: [PATCH 142/336] fix bug where session path is undefined (#21544) fixes https://github.com/microsoft/vscode-python/issues/21540 --- pythonFiles/vscode_pytest/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 3f3aca6a2a64..bdbe16ff8523 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -399,9 +399,10 @@ def create_session_node(session: pytest.Session) -> TestNode: Keyword arguments: session -- the pytest session. """ + session_path = session.path if session.path else pathlib.Path.cwd() return { "name": session.name, - "path": session.path, + "path": session_path, "type_": "folder", "children": [], "id_": os.fspath(session.path), From 1d58cefc1b04252fa3c382dbd40e7ddc09175bb1 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Thu, 29 Jun 2023 17:52:43 -0700 Subject: [PATCH 143/336] Fix syspath in discovery unittest (#21546) --- pythonFiles/unittestadapter/discovery.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index ffcd2671addb..cbad40ad1838 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -12,23 +12,14 @@ script_dir = pathlib.Path(__file__).parent.parent sys.path.append(os.fspath(script_dir)) -sys.path.append(os.fspath(script_dir / "lib" / "python")) - -from typing_extensions import Literal - -# Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. -PYTHON_FILES = pathlib.Path(__file__).parent.parent -sys.path.insert(0, os.fspath(PYTHON_FILES)) +sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) from testing_tools import socket_manager # If I use from utils then there will be an import error in test_discovery.py. from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args -# Add the lib path to sys.path to find the typing_extensions module. -sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) - -from typing_extensions import NotRequired, TypedDict +from typing_extensions import NotRequired, TypedDict, Literal DEFAULT_PORT = "45454" From af7eb568b0ed9272a9f7e3274571c379c7a89a83 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 29 Jun 2023 23:08:38 -0700 Subject: [PATCH 144/336] update to align with sys path appends (#21547) changes to align with https://github.com/microsoft/vscode-python/pull/21546 --- pythonFiles/tests/unittestadapter/test_execution.py | 4 ++-- pythonFiles/unittestadapter/execution.py | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index a822636bf479..057f64d7396a 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -8,9 +8,9 @@ import pytest -PYTHON_FILES = pathlib.Path(__file__).parent.parent +script_dir = pathlib.Path(__file__).parent.parent +sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -sys.path.insert(0, os.fspath(PYTHON_FILES)) from unittestadapter.execution import parse_execution_cli_args, run_tests TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index aa50cbc5d09f..dfb6928a2074 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -13,13 +13,10 @@ from types import TracebackType from typing import Dict, List, Optional, Tuple, Type, Union -directory_path = pathlib.Path(__file__).parent.parent / "lib" / "python" -# Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. -PYTHON_FILES = pathlib.Path(__file__).parent.parent +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -sys.path.insert(0, os.fspath(PYTHON_FILES)) -# Add the lib path to sys.path to find the typing_extensions module. -sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) from testing_tools import process_json_util, socket_manager from typing_extensions import NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args From f37f88367bedaae035c95d6d45763d9218bed933 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 30 Jun 2023 07:41:59 -0700 Subject: [PATCH 145/336] Fix path for pytest Nodes (#21548) Fixes https://github.com/microsoft/vscode-python/issues/21540 --- pythonFiles/vscode_pytest/__init__.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index bdbe16ff8523..b880b26fd5b8 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -290,7 +290,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: # parameterized test cases cut the repetitive part of the name off. name_split = test_node["name"].split("[") test_node["name"] = "[" + name_split[1] - parent_path = os.fspath(test_case.path) + "::" + name_split[0] + parent_path = os.fspath(get_node_path(test_case)) + "::" + name_split[0] try: function_name = test_case.originalname # type: ignore function_test_case = function_nodes_dict[parent_path] @@ -303,7 +303,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: ) except KeyError: function_test_case: TestNode = create_parameterized_function_node( - function_name, test_case.path, test_case.nodeid + function_name, get_node_path(test_case), test_case.nodeid ) function_nodes_dict[parent_path] = function_test_case function_test_case["children"].append(test_node) @@ -354,7 +354,7 @@ def build_nested_folders( # Begin the iterator_path one level above the current file. iterator_path = file_node["path"].parent - while iterator_path != session.path: + while iterator_path != get_node_path(session): curr_folder_name = iterator_path.name try: curr_folder_node: TestNode = created_files_folders_dict[ @@ -385,7 +385,7 @@ def create_test_node( ) return { "name": test_case.name, - "path": test_case.path, + "path": get_node_path(test_case), "lineno": test_case_loc, "type_": "test", "id_": test_case.nodeid, @@ -399,13 +399,13 @@ def create_session_node(session: pytest.Session) -> TestNode: Keyword arguments: session -- the pytest session. """ - session_path = session.path if session.path else pathlib.Path.cwd() + node_path = get_node_path(session) return { "name": session.name, - "path": session_path, + "path": node_path, "type_": "folder", "children": [], - "id_": os.fspath(session.path), + "id_": os.fspath(node_path), } @@ -417,7 +417,7 @@ def create_class_node(class_module: pytest.Class) -> TestNode: """ return { "name": class_module.name, - "path": class_module.path, + "path": get_node_path(class_module), "type_": "class", "children": [], "id_": class_module.nodeid, @@ -451,11 +451,12 @@ def create_file_node(file_module: Any) -> TestNode: Keyword arguments: file_module -- the pytest file module. """ + node_path = get_node_path(file_module) return { - "name": file_module.path.name, - "path": file_module.path, + "name": node_path.name, + "path": node_path, "type_": "file", - "id_": os.fspath(file_module.path), + "id_": os.fspath(node_path), "children": [], } @@ -497,6 +498,10 @@ class ExecutionPayloadDict(Dict): error: Union[str, None] # Currently unused need to check +def get_node_path(node: Any) -> pathlib.Path: + return getattr(node, "path", pathlib.Path(node.fspath)) + + def execution_post( cwd: str, status: Literal["success", "error"], From a5ef6a26bcaa823a16203eb2b6ac1974a5fee8c0 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 5 Jul 2023 10:36:03 -0700 Subject: [PATCH 146/336] Update version for release candidate (#21556) --- package-lock.json | 2625 ++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 1399 insertions(+), 1228 deletions(-) diff --git a/package-lock.json b/package-lock.json index f41a215a083e..937531ade78f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.11.0-dev", + "version": "2023.12.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.11.0-dev", + "version": "2023.12.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -131,6 +131,28 @@ "vscode": "^1.79.0-20230526" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@azure/abort-controller": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", @@ -283,22 +305,260 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/@babel/compat-data": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.6.tgz", + "integrity": "sha512-HPIyDa6n+HKw5dEuway3vVAhBboYCtREBMp+IWeseZy6TFtzn6MHkCH2KKYUOC/vKKwgSMHQW4htBOrmuRPXfw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", + "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-validator-option": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.15.7", + "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -306,6 +566,18 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/parser": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.6.tgz", + "integrity": "sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.17.8", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", @@ -328,6 +600,96 @@ "regenerator-runtime": "^0.13.4" } }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", + "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cspotcode/source-map-consumer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", @@ -585,13 +947,13 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "node_modules/@microsoft/1ds-core-js": { @@ -719,6 +1081,15 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "node_modules/@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1194,9 +1565,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1318,9 +1689,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1927,13 +2298,6 @@ } } }, - "node_modules/aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, "node_modules/arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -1980,17 +2344,6 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, - "node_modules/are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "dev": true, - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "node_modules/arg": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", @@ -2769,9 +3122,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "dev": true, "funding": [ { @@ -2781,14 +3134,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001317", - "electron-to-chromium": "^1.4.84", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" }, "bin": { "browserslist": "cli.js" @@ -2976,9 +3332,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001320", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001320.tgz", - "integrity": "sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==", + "version": "1.0.30001512", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz", + "integrity": "sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==", "dev": true, "funding": [ { @@ -2988,6 +3344,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -3401,9 +3761,9 @@ } }, "node_modules/chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, "optional": true }, @@ -3475,7 +3835,7 @@ "node_modules/cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", "dev": true, "dependencies": { "string-width": "^1.0.1", @@ -3486,7 +3846,7 @@ "node_modules/cliui/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -3495,7 +3855,7 @@ "node_modules/cliui/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "dependencies": { "ansi-regex": "^2.0.0" @@ -3578,7 +3938,7 @@ "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -3720,13 +4080,6 @@ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", "dev": true }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true - }, "node_modules/constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -3755,13 +4108,10 @@ } }, "node_modules/convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "node_modules/copy-descriptor": { "version": "0.1.1", @@ -4357,13 +4707,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, "node_modules/des.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", @@ -4384,16 +4727,13 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "dev": true, "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/diagnostic-channel": { @@ -4572,9 +4912,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.92", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.92.tgz", - "integrity": "sha512-YAVbvQIcDE/IJ/vzDMjD484/hsRbFPW2qXJPaYTfOhtligmfYEYOep+5QojpaEU9kq6bMvNeC2aG7arYvTHYsA==", + "version": "1.4.450", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.450.tgz", + "integrity": "sha512-BLG5HxSELlrMx7dJ2s+8SFlsCtJp37Zpk2VAxyC6CZtbc+9AJeZHfYHbrlSgdXp6saQ8StMqOTEDaBKgA7u1sw==", "dev": true }, "node_modules/elliptic": { @@ -4694,12 +5034,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, "node_modules/es-abstract": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.2.tgz", @@ -5412,19 +5746,6 @@ "node": ">= 4" } }, - "node_modules/eslint/node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5437,23 +5758,6 @@ "node": "*" } }, - "node_modules/eslint/node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5463,15 +5767,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -5482,9 +5777,9 @@ } }, "node_modules/eslint/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -5529,18 +5824,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -6216,6 +6499,23 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -6566,44 +6866,13 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, - "node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "optional": true, - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "optional": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" } }, "node_modules/get-caller-file": { @@ -6693,7 +6962,7 @@ "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, "optional": true }, @@ -7029,64 +7298,7 @@ "node": ">= 0.10" } }, - "node_modules/gulp-typescript": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.1.tgz", - "integrity": "sha512-YuMMlylyJtUSHG1/wuSVTrZp60k1dMEFKYOvDf7OvbAJWrDtxxD4oZon4ancdWwzjj30ztiidhe4VXJniF0pIQ==", - "dev": true, - "dependencies": { - "ansi-colors": "^3.0.5", - "plugin-error": "^1.0.1", - "source-map": "^0.7.3", - "through2": "^3.0.0", - "vinyl": "^2.1.0", - "vinyl-fs": "^3.0.3" - }, - "engines": { - "node": ">= 8" - }, - "peerDependencies": { - "typescript": "~2.7.1 || >=2.8.0-dev || >=2.9.0-dev || ~3.0.0 || >=3.0.0-dev || >=3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev" - } - }, - "node_modules/gulp-typescript/node_modules/ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/gulp-typescript/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/gulp-typescript/node_modules/through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" - } - }, - "node_modules/gulp/node_modules/camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp/node_modules/gulp-cli": { + "node_modules/gulp-cli": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", @@ -7118,28 +7330,25 @@ "node": ">= 0.10" } }, - "node_modules/gulp/node_modules/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "node_modules/gulp-cli/node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/gulp/node_modules/y18n": { + "node_modules/gulp-cli/node_modules/y18n": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", "dev": true }, - "node_modules/gulp/node_modules/yargs": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", - "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", + "node_modules/gulp-cli/node_modules/yargs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", "dev": true, "dependencies": { "camelcase": "^3.0.0", @@ -7154,19 +7363,67 @@ "string-width": "^1.0.2", "which-module": "^1.0.0", "y18n": "^3.2.1", - "yargs-parser": "5.0.0-security.0" + "yargs-parser": "^5.0.1" } }, - "node_modules/gulp/node_modules/yargs-parser": { - "version": "5.0.0-security.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", - "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", + "node_modules/gulp-cli/node_modules/yargs-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", "dev": true, "dependencies": { "camelcase": "^3.0.0", "object.assign": "^4.1.0" } }, + "node_modules/gulp-typescript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.1.tgz", + "integrity": "sha512-YuMMlylyJtUSHG1/wuSVTrZp60k1dMEFKYOvDf7OvbAJWrDtxxD4oZon4ancdWwzjj30ztiidhe4VXJniF0pIQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^3.0.5", + "plugin-error": "^1.0.1", + "source-map": "^0.7.3", + "through2": "^3.0.0", + "vinyl": "^2.1.0", + "vinyl-fs": "^3.0.3" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "typescript": "~2.7.1 || >=2.8.0-dev || >=2.9.0-dev || ~3.0.0 || >=3.0.0-dev || >=3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev" + } + }, + "node_modules/gulp-typescript/node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/gulp-typescript/node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/gulp-typescript/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, "node_modules/gulplog": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", @@ -7272,13 +7529,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, "node_modules/has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -7652,7 +7902,7 @@ "node_modules/invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -7711,6 +7961,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-bigint": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", @@ -7844,7 +8100,7 @@ "node_modules/is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", "dev": true, "dependencies": { "number-is-nan": "^1.0.0" @@ -8127,258 +8383,95 @@ } }, "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "node_modules/is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-hook": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", - "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", - "dev": true, - "dependencies": { - "append-transform": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz", - "integrity": "sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/core": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.7.tgz", - "integrity": "sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.7", - "@babel/helpers": "^7.7.4", - "@babel/parser": "^7.7.7", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/core/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/generator": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.7.tgz", - "integrity": "sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.7.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-function-name": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", - "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", - "dev": true, - "dependencies": { - "@babel/helper-get-function-arity": "^7.7.4", - "@babel/template": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-get-function-arity": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", - "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.7.4" - } + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-split-export-declaration": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", - "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", "dev": true, - "dependencies": { - "@babel/types": "^7.7.4" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helpers": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", - "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, "dependencies": { - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4" + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/parser": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", - "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, "engines": { - "node": ">=6.0.0" + "node": ">=0.10.0" } }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/template": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", - "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4" - } + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/traverse": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", - "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true, - "dependencies": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.4", - "@babel/helper-function-name": "^7.7.4", - "@babel/helper-split-export-declaration": "^7.7.4", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/types": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", - "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true, - "dependencies": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/istanbul-lib-instrument/node_modules/convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", "dev": true, "dependencies": { - "safe-buffer": "~5.1.1" + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/istanbul-lib-instrument/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { @@ -8390,37 +8483,27 @@ "semver": "bin/semver.js" } }, - "node_modules/istanbul-lib-instrument/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/istanbul-lib-processinfo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", - "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, "dependencies": { "archy": "^1.0.0", - "cross-spawn": "^7.0.0", - "istanbul-lib-coverage": "^3.0.0-alpha.1", - "make-dir": "^3.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", - "uuid": "^3.3.3" + "uuid": "^8.3.2" }, "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-processinfo/node_modules/cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -8473,16 +8556,6 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-processinfo/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", @@ -8543,9 +8616,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -8723,15 +8796,15 @@ "dev": true }, "node_modules/keytar": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", - "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "hasInstallScript": true, "optional": true, "dependencies": { - "node-addon-api": "^3.0.0", - "prebuild-install": "^6.0.0" + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" } }, "node_modules/keyv": { @@ -8795,7 +8868,7 @@ "node_modules/lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", "dev": true, "dependencies": { "invert-kv": "^1.0.0" @@ -8825,6 +8898,19 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -8862,7 +8948,7 @@ "node_modules/load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", "dev": true, "dependencies": { "graceful-fs": "^4.1.2", @@ -8875,22 +8961,10 @@ "node": ">=0.10.0" } }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/load-json-file/node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "engines": { "node": ">=0.10.0" @@ -9995,19 +10069,38 @@ } }, "node_modules/node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", "dev": true, "optional": true, "dependencies": { - "semver": "^5.4.1" + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, "optional": true }, @@ -10253,9 +10346,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", "dev": true }, "node_modules/node-stream-zip": { @@ -10350,19 +10443,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "optional": true, - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "node_modules/nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", @@ -10378,7 +10458,7 @@ "node_modules/number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10425,32 +10505,6 @@ "node": ">=8.9" } }, - "node_modules/nyc/node_modules/convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/nyc/node_modules/find-cache-dir": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", - "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.0", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, "node_modules/nyc/node_modules/p-map": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", @@ -10764,6 +10818,23 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -10782,7 +10853,7 @@ "node_modules/os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", "dev": true, "dependencies": { "lcid": "^1.0.0" @@ -10965,6 +11036,18 @@ "node": ">=0.8" } }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -10986,7 +11069,7 @@ "node_modules/parse-semver": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "dependencies": { "semver": "^5.1.0" @@ -11231,23 +11314,22 @@ } }, "node_modules/prebuild-install": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", - "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^2.21.0", - "npmlog": "^4.0.1", + "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^3.0.3", + "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, @@ -11255,7 +11337,7 @@ "prebuild-install": "bin.js" }, "engines": { - "node": ">=6" + "node": ">=10" } }, "node_modules/prebuild-install/node_modules/pump": { @@ -11269,6 +11351,15 @@ "once": "^1.3.1" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", @@ -11490,7 +11581,7 @@ "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "optional": true, "engines": { @@ -11518,7 +11609,7 @@ "node_modules/read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", "dev": true, "dependencies": { "load-json-file": "^1.0.0", @@ -11532,7 +11623,7 @@ "node_modules/read-pkg-up": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", "dev": true, "dependencies": { "find-up": "^1.0.0", @@ -11545,7 +11636,7 @@ "node_modules/read-pkg-up/node_modules/find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", "dev": true, "dependencies": { "path-exists": "^2.0.0", @@ -11558,7 +11649,7 @@ "node_modules/read-pkg-up/node_modules/path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", "dev": true, "dependencies": { "pinkie-promise": "^2.0.0" @@ -11570,7 +11661,7 @@ "node_modules/read-pkg/node_modules/path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", "dev": true, "dependencies": { "graceful-fs": "^4.1.2", @@ -11584,7 +11675,7 @@ "node_modules/read-pkg/node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "engines": { "node": ">=0.10.0" @@ -11797,7 +11888,7 @@ "node_modules/require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", "dev": true }, "node_modules/resolve": { @@ -12214,38 +12305,55 @@ "optional": true }, "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "optional": true, "dependencies": { - "decompress-response": "^4.2.0", + "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "node_modules/simple-get/node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "optional": true, "dependencies": { - "mimic-response": "^2.0.0" + "mimic-response": "^3.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/simple-get/node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "optional": true, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -12612,9 +12720,9 @@ } }, "node_modules/spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -12622,15 +12730,15 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "node_modules/spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "dependencies": { "spdx-exceptions": "^2.1.0", @@ -12638,9 +12746,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "node_modules/split-string": { @@ -12775,7 +12883,7 @@ "node_modules/string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", "dev": true, "dependencies": { "code-point-at": "^1.0.0", @@ -12789,7 +12897,7 @@ "node_modules/string-width/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -12798,7 +12906,7 @@ "node_modules/string-width/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "dependencies": { "ansi-regex": "^2.0.0" @@ -12867,7 +12975,7 @@ "node_modules/strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", "dev": true, "dependencies": { "is-utf8": "^0.2.0" @@ -13064,9 +13172,9 @@ } }, "node_modules/tar-fs/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "optional": true, "dependencies": { @@ -13300,7 +13408,7 @@ "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, "engines": { "node": ">=4" @@ -13524,9 +13632,9 @@ } }, "node_modules/ts-loader/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -13766,7 +13874,7 @@ "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "optional": true, "dependencies": { @@ -13782,6 +13890,18 @@ "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==", "dev": true }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -14073,6 +14193,36 @@ "yarn": "*" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -14183,6 +14333,18 @@ "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", "dev": true }, + "node_modules/v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -14331,9 +14493,9 @@ } }, "node_modules/vscode-languageclient/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -14734,7 +14896,7 @@ "node_modules/which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", "dev": true }, "node_modules/which-typed-array": { @@ -14757,16 +14919,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, "node_modules/wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", @@ -14793,15 +14945,6 @@ "wipe-node-cache": "^2.1.0" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/workerpool": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", @@ -14811,7 +14954,7 @@ "node_modules/wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", "dev": true, "dependencies": { "string-width": "^1.0.1", @@ -14824,7 +14967,7 @@ "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -14833,7 +14976,7 @@ "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "dependencies": { "ansi-regex": "^2.0.0" @@ -15163,6 +15306,22 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@azure/abort-controller": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", @@ -15295,23 +15454,211 @@ "@babel/highlight": "^7.10.4" } }, + "@babel/compat-data": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", + "dev": true + }, + "@babel/core": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.6.tgz", + "integrity": "sha512-HPIyDa6n+HKw5dEuway3vVAhBboYCtREBMp+IWeseZy6TFtzn6MHkCH2KKYUOC/vKKwgSMHQW4htBOrmuRPXfw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, + "@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", + "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-validator-option": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, "@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", "dev": true }, + "@babel/helpers": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + } + }, "@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.15.7", + "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, + "@babel/parser": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.6.tgz", + "integrity": "sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==", + "dev": true + }, "@babel/runtime": { "version": "7.17.8", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", @@ -15331,6 +15678,77 @@ "regenerator-runtime": "^0.13.4" } }, + "@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + } + } + }, + "@babel/traverse": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", + "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, + "@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + } + }, "@cspotcode/source-map-consumer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", @@ -15519,13 +15937,13 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "@microsoft/1ds-core-js": { @@ -15638,6 +16056,12 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -16050,9 +16474,9 @@ } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -16118,9 +16542,9 @@ } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -16611,13 +17035,6 @@ "diagnostic-channel-publishers": "1.0.5" } }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, "arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -16646,17 +17063,6 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, - "are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "arg": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", @@ -17296,16 +17702,15 @@ } }, "browserslist": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001317", - "electron-to-chromium": "^1.4.84", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" } }, "buffer": { @@ -17451,9 +17856,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001320", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001320.tgz", - "integrity": "sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==", + "version": "1.0.30001512", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz", + "integrity": "sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==", "dev": true }, "caseless": { @@ -17755,9 +18160,9 @@ } }, "chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, "optional": true }, @@ -17818,7 +18223,7 @@ "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", "dev": true, "requires": { "string-width": "^1.0.1", @@ -17829,13 +18234,13 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "requires": { "ansi-regex": "^2.0.0" @@ -17905,7 +18310,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "dev": true }, "collection-map": { @@ -18028,13 +18433,6 @@ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", "dev": true }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true - }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -18060,13 +18458,10 @@ } }, "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "copy-descriptor": { "version": "0.1.1", @@ -18557,13 +18952,6 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, "des.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", @@ -18581,9 +18969,9 @@ "dev": true }, "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "dev": true, "optional": true }, @@ -18741,9 +19129,9 @@ } }, "electron-to-chromium": { - "version": "1.4.92", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.92.tgz", - "integrity": "sha512-YAVbvQIcDE/IJ/vzDMjD484/hsRbFPW2qXJPaYTfOhtligmfYEYOep+5QojpaEU9kq6bMvNeC2aG7arYvTHYsA==", + "version": "1.4.450", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.450.tgz", + "integrity": "sha512-BLG5HxSELlrMx7dJ2s+8SFlsCtJp37Zpk2VAxyC6CZtbc+9AJeZHfYHbrlSgdXp6saQ8StMqOTEDaBKgA7u1sw==", "dev": true }, "elliptic": { @@ -18844,14 +19232,6 @@ "dev": true, "requires": { "is-arrayish": "^0.2.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - } } }, "es-abstract": { @@ -19129,16 +19509,6 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -19148,32 +19518,12 @@ "brace-expansion": "^1.1.7" } }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -19181,9 +19531,9 @@ "dev": true }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -19213,15 +19563,6 @@ "has-flag": "^4.0.0" } }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -20034,6 +20375,17 @@ "integrity": "sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==", "dev": true }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -20292,59 +20644,29 @@ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, "requires": { - "glob": "^7.1.3" - } - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" + "glob": "^7.1.3" } } } }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -20405,7 +20727,7 @@ "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, "optional": true }, @@ -20675,49 +20997,40 @@ "gulp-cli": "^2.2.0", "undertaker": "^1.2.1", "vinyl-fs": "^3.0.0" + } + }, + "gulp-cli": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.4.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.2.0", + "yargs": "^7.1.0" }, "dependencies": { "camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", "dev": true }, - "gulp-cli": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", - "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", - "dev": true, - "requires": { - "ansi-colors": "^1.0.1", - "archy": "^1.0.0", - "array-sort": "^1.0.0", - "color-support": "^1.1.3", - "concat-stream": "^1.6.0", - "copy-props": "^2.0.1", - "fancy-log": "^1.3.2", - "gulplog": "^1.0.0", - "interpret": "^1.4.0", - "isobject": "^3.0.1", - "liftoff": "^3.1.0", - "matchdep": "^2.0.0", - "mute-stdout": "^1.0.0", - "pretty-hrtime": "^1.0.0", - "replace-homedir": "^1.0.0", - "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.2.0", - "yargs": "^7.1.0" - } - }, - "v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, "y18n": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", @@ -20725,9 +21038,9 @@ "dev": true }, "yargs": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", - "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", "dev": true, "requires": { "camelcase": "^3.0.0", @@ -20742,13 +21055,13 @@ "string-width": "^1.0.2", "which-module": "^1.0.0", "y18n": "^3.2.1", - "yargs-parser": "5.0.0-security.0" + "yargs-parser": "^5.0.1" } }, "yargs-parser": { - "version": "5.0.0-security.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", - "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", "dev": true, "requires": { "camelcase": "^3.0.0", @@ -20864,13 +21177,6 @@ "has-symbols": "^1.0.2" } }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -21152,7 +21458,7 @@ "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", "dev": true }, "is-absolute": { @@ -21195,6 +21501,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "is-bigint": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", @@ -21293,7 +21605,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", "dev": true, "requires": { "number-is-nan": "^1.0.0" @@ -21532,9 +21844,9 @@ "dev": true }, "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true }, "istanbul-lib-hook": { @@ -21547,198 +21859,43 @@ } }, "istanbul-lib-instrument": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz", - "integrity": "sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "requires": { "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.0.0", "semver": "^6.3.0" }, "dependencies": { - "@babel/core": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.7.tgz", - "integrity": "sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.7", - "@babel/helpers": "^7.7.4", - "@babel/parser": "^7.7.7", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.7.tgz", - "integrity": "sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==", - "dev": true, - "requires": { - "@babel/types": "^7.7.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", - "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.7.4", - "@babel/template": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", - "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", - "dev": true, - "requires": { - "@babel/types": "^7.7.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", - "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", - "dev": true, - "requires": { - "@babel/types": "^7.7.4" - } - }, - "@babel/helpers": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", - "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", - "dev": true, - "requires": { - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "@babel/parser": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", - "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", - "dev": true - }, - "@babel/template": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", - "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "@babel/traverse": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", - "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.4", - "@babel/helper-function-name": "^7.7.4", - "@babel/helper-split-export-declaration": "^7.7.4", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", - "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true } } }, "istanbul-lib-processinfo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", - "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, "requires": { "archy": "^1.0.0", - "cross-spawn": "^7.0.0", - "istanbul-lib-coverage": "^3.0.0-alpha.1", - "make-dir": "^3.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", - "uuid": "^3.3.3" + "uuid": "^8.3.2" }, "dependencies": { "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -21775,12 +21932,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true } } }, @@ -21835,9 +21986,9 @@ } }, "istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -21976,14 +22127,14 @@ "dev": true }, "keytar": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", - "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "optional": true, "requires": { - "node-addon-api": "^3.0.0", - "prebuild-install": "^6.0.0" + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" } }, "keyv": { @@ -22038,7 +22189,7 @@ "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", "dev": true, "requires": { "invert-kv": "^1.0.0" @@ -22059,6 +22210,16 @@ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, "liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -22093,7 +22254,7 @@ "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -22103,19 +22264,10 @@ "strip-bom": "^2.0.0" }, "dependencies": { - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true } } @@ -22986,19 +23138,31 @@ } }, "node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", "dev": true, "optional": true, "requires": { - "semver": "^5.4.1" + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, "optional": true }, @@ -23202,9 +23366,9 @@ } }, "node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", "dev": true }, "node-stream-zip": { @@ -23278,19 +23442,6 @@ } } }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", @@ -23303,7 +23454,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", "dev": true }, "nyc": { @@ -23341,26 +23492,6 @@ "yargs": "^15.0.2" }, "dependencies": { - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "find-cache-dir": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", - "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.0", - "pkg-dir": "^4.1.0" - } - }, "p-map": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", @@ -23597,6 +23728,20 @@ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, "ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -23615,7 +23760,7 @@ "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", "dev": true, "requires": { "lcid": "^1.0.0" @@ -23749,6 +23894,15 @@ "path-root": "^0.1.1" } }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, "parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -23764,7 +23918,7 @@ "parse-semver": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "requires": { "semver": "^5.1.0" @@ -23958,23 +24112,22 @@ "dev": true }, "prebuild-install": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", - "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, "optional": true, "requires": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^2.21.0", - "npmlog": "^4.0.1", + "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^3.0.3", + "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, @@ -23992,6 +24145,12 @@ } } }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", @@ -24159,7 +24318,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "optional": true } @@ -24183,7 +24342,7 @@ "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", "dev": true, "requires": { "load-json-file": "^1.0.0", @@ -24194,7 +24353,7 @@ "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -24205,7 +24364,7 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true } } @@ -24213,7 +24372,7 @@ "read-pkg-up": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", "dev": true, "requires": { "find-up": "^1.0.0", @@ -24223,7 +24382,7 @@ "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", "dev": true, "requires": { "path-exists": "^2.0.0", @@ -24233,7 +24392,7 @@ "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", "dev": true, "requires": { "pinkie-promise": "^2.0.0" @@ -24402,7 +24561,7 @@ "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", "dev": true }, "resolve": { @@ -24719,31 +24878,31 @@ "optional": true }, "simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, "optional": true, "requires": { - "decompress-response": "^4.2.0", + "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" }, "dependencies": { "decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "optional": true, "requires": { - "mimic-response": "^2.0.0" + "mimic-response": "^3.1.0" } }, "mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "optional": true } @@ -25033,9 +25192,9 @@ } }, "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -25043,15 +25202,15 @@ } }, "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -25059,9 +25218,9 @@ } }, "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "split-string": { @@ -25171,7 +25330,7 @@ "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", "dev": true, "requires": { "code-point-at": "^1.0.0", @@ -25182,13 +25341,13 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "requires": { "ansi-regex": "^2.0.0" @@ -25244,7 +25403,7 @@ "strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", "dev": true, "requires": { "is-utf8": "^0.2.0" @@ -25403,9 +25562,9 @@ } }, "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "optional": true, "requires": { @@ -25586,7 +25745,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true }, "to-object-path": { @@ -25748,9 +25907,9 @@ } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -25926,7 +26085,7 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "optional": true, "requires": { @@ -25939,6 +26098,15 @@ "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==", "dev": true }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -26177,6 +26345,16 @@ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -26278,6 +26456,15 @@ "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", "dev": true }, + "v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -26404,9 +26591,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "requires": { "lru-cache": "^6.0.0" } @@ -26690,7 +26877,7 @@ "which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", "dev": true }, "which-typed-array": { @@ -26707,16 +26894,6 @@ "is-typed-array": "^1.1.7" } }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, "wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", @@ -26743,12 +26920,6 @@ "wipe-node-cache": "^2.1.0" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, "workerpool": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", @@ -26758,7 +26929,7 @@ "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", "dev": true, "requires": { "string-width": "^1.0.1", @@ -26768,13 +26939,13 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "requires": { "ansi-regex": "^2.0.0" diff --git a/package.json b/package.json index 85d4baa31e4b..769728ac1549 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.11.0-dev", + "version": "2023.12.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From 66270f0a44cb8693766cea5ff1f184dc24ad146c Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 5 Jul 2023 11:02:08 -0700 Subject: [PATCH 147/336] Update main to next pre-release (#21559) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 937531ade78f..b042f0d35cfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.12.0-rc", + "version": "2023.13.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.12.0-rc", + "version": "2023.13.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 769728ac1549..93db9d7263e8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.12.0-rc", + "version": "2023.13.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 4f15a504f7c4467b9c6e727c0afbdff4fe5e1c75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jul 2023 11:02:58 -0700 Subject: [PATCH 148/336] Bump semver from 5.7.1 to 7.5.2 (#21489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 7.5.2.

Release notes

Sourced from semver's releases.

v7.5.2

7.5.2 (2023-06-15)

Bug Fixes

v7.5.1

7.5.1 (2023-05-12)

Bug Fixes

v7.5.0

7.5.0 (2023-04-17)

Features

Bug Fixes

v7.4.0

7.4.0 (2023-04-10)

Features

Bug Fixes

Documentation

... (truncated)

Changelog

Sourced from semver's changelog.

7.5.2 (2023-06-15)

Bug Fixes

7.5.1 (2023-05-12)

Bug Fixes

7.5.0 (2023-04-17)

Features

Bug Fixes

7.4.0 (2023-04-10)

Features

Bug Fixes

Documentation

7.3.8 (2022-10-04)

Bug Fixes

... (truncated)

Commits
  • e7b78de chore: release 7.5.2
  • 58c791f fix: diff when detecting major change from prerelease (#566)
  • 5c8efbc fix: preserve build in raw after inc (#565)
  • 717534e fix: better handling of whitespace (#564)
  • 2f738e9 chore: bump @​npmcli/template-oss from 4.14.1 to 4.15.1 (#558)
  • aa016a6 chore: release 7.5.1
  • d30d25a fix: show type on invalid semver error (#559)
  • 09c69e2 chore: bump @​npmcli/template-oss from 4.13.0 to 4.14.1 (#555)
  • 5b02ad7 chore: release 7.5.0
  • e219bb4 fix: throw on bad version with correct error message (#552)
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by npm-cli-ops, a new releaser for semver since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=semver&package-manager=npm_and_yarn&previous-version=5.7.1&new-version=7.5.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 164 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 157 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index b042f0d35cfe..e6042d7638d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "reflect-metadata": "^0.1.12", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", - "semver": "^5.5.0", + "semver": "^7.5.2", "stack-trace": "0.0.10", "sudo-prompt": "^9.2.1", "tmp": "^0.0.33", @@ -1850,6 +1850,15 @@ "node": "*" } }, + "node_modules/@vscode/vsce/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/@vscode/vsce/node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -2684,6 +2693,14 @@ "node": "<=0.11.8 || >0.11.10" } }, + "node_modules/async-listener/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/async-settle": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", @@ -3935,6 +3952,14 @@ "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1" } }, + "node_modules/cls-hooked/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -4249,6 +4274,15 @@ "node": ">=4.8" } }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/cross-spawn/node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -4752,6 +4786,14 @@ "diagnostic-channel": "*" } }, + "node_modules/diagnostic-channel/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -4868,6 +4910,15 @@ "once": "^1.3.1" } }, + "node_modules/download/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -10068,6 +10119,15 @@ "ms": "^2.1.1" } }, + "node_modules/nock/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/node-abi": { "version": "3.45.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", @@ -10375,6 +10435,15 @@ "validate-npm-package-license": "^3.0.1" } }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -11075,6 +11144,15 @@ "semver": "^5.1.0" } }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", @@ -12137,11 +12215,17 @@ } }, "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { - "semver": "bin/semver" + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/semver-greatest-satisfied-range": { @@ -16666,6 +16750,12 @@ "brace-expansion": "^1.1.7" } }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -17328,6 +17418,13 @@ "requires": { "semver": "^5.3.0", "shimmer": "^1.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "async-settle": { @@ -18305,6 +18402,13 @@ "async-hook-jl": "^1.7.6", "emitter-listener": "^1.0.1", "semver": "^5.4.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "code-point-at": { @@ -18580,6 +18684,12 @@ "which": "^1.2.9" }, "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -18981,6 +19091,13 @@ "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", "requires": { "semver": "^5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "diagnostic-channel-publishers": { @@ -19082,6 +19199,12 @@ "end-of-stream": "^1.1.0", "once": "^1.3.1" } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -23134,6 +23257,12 @@ "requires": { "ms": "^2.1.1" } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -23386,6 +23515,14 @@ "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "normalize-path": { @@ -23922,6 +24059,14 @@ "dev": true, "requires": { "semver": "^5.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "parse5-htmlparser2-tree-adapter": { @@ -24749,9 +24894,12 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "requires": { + "lru-cache": "^6.0.0" + } }, "semver-greatest-satisfied-range": { "version": "1.1.0", diff --git a/package.json b/package.json index 93db9d7263e8..715179858e24 100644 --- a/package.json +++ b/package.json @@ -1998,7 +1998,7 @@ "reflect-metadata": "^0.1.12", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", - "semver": "^5.5.0", + "semver": "^7.5.2", "stack-trace": "0.0.10", "sudo-prompt": "^9.2.1", "tmp": "^0.0.33", From 724a0fd6625cc36ac38abb8720b2fdbad96f2c73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jul 2023 11:06:52 -0700 Subject: [PATCH 149/336] Bump typing-extensions from 4.6.3 to 4.7.1 (#21552) Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.6.3 to 4.7.1.
Release notes

Sourced from typing-extensions's releases.

4.7.1

  • Fix support for TypedDict, NamedTuple and is_protocol on PyPy-3.7 and PyPy-3.8. Patch by Alex Waygood. Note that PyPy-3.7 and PyPy-3.8 are unsupported by the PyPy project. The next feature release of typing-extensions will drop support for PyPy-3.7 and may also drop support for PyPy-3.8.

4.7.0

This is a feature release. Major changes include:

  • All non-deprecated names from typing are now re-exported by typing_extensions for convenience
  • Add typing_extensions.get_protocol_members and typing_extensions.is_protocol
  • Declare support for Python 3.12
  • This will be the last feature release to support Python 3.7, which recently reached its end-of-life

Full changelog of versions 4.7.0 and 4.7.0rc1:

Release 4.7.0 (June 28, 2023)

  • This is expected to be the last feature release supporting Python 3.7, which reaches its end of life on June 27, 2023. Version 4.8.0 will support only Python 3.8.0 and up.
  • Fix bug where a typing_extensions.Protocol class that had one or more non-callable members would raise TypeError when issubclass() was called against it, even if it defined a custom __subclasshook__ method. The correct behaviour -- which has now been restored -- is not to raise TypeError in these situations if a custom __subclasshook__ method is defined. Patch by Alex Waygood (backporting python/cpython#105976).

Release 4.7.0rc1 (June 21, 2023)

  • Add typing_extensions.get_protocol_members and typing_extensions.is_protocol (backport of CPython PR #104878). Patch by Jelle Zijlstra.
  • typing_extensions now re-exports all names in the standard library's typing module, except the deprecated ByteString. Patch by Jelle Zijlstra.
  • Due to changes in the implementation of typing_extensions.Protocol, typing.runtime_checkable can now be used on typing_extensions.Protocol (previously, users had to use typing_extensions.runtime_checkable if they were using typing_extensions.Protocol).
  • Align the implementation of TypedDict with the implementation in the standard library on Python 3.9 and higher. typing_extensions.TypedDict is now a function instead of a class. The private functions _check_fails, _dict_new, and _typeddict_new have been removed. is_typeddict now returns False when called with TypedDict itself as the argument. Patch by Jelle Zijlstra.
  • Declare support for Python 3.12. Patch by Jelle Zijlstra.
  • Fix tests on Python 3.13, which removes support for creating TypedDict classes through the keyword-argument syntax. Patch by

... (truncated)

Changelog

Sourced from typing-extensions's changelog.

Release 4.7.1 (July 2, 2023)

  • Fix support for TypedDict, NamedTuple and is_protocol on PyPy-3.7 and PyPy-3.8. Patch by Alex Waygood. Note that PyPy-3.7 and PyPy-3.8 are unsupported by the PyPy project. The next feature release of typing-extensions will drop support for PyPy-3.7 and may also drop support for PyPy-3.8.

Release 4.7.0 (June 28, 2023)

  • This is expected to be the last feature release supporting Python 3.7, which reaches its end of life on June 27, 2023. Version 4.8.0 will support only Python 3.8.0 and up.
  • Fix bug where a typing_extensions.Protocol class that had one or more non-callable members would raise TypeError when issubclass() was called against it, even if it defined a custom __subclasshook__ method. The correct behaviour -- which has now been restored -- is not to raise TypeError in these situations if a custom __subclasshook__ method is defined. Patch by Alex Waygood (backporting python/cpython#105976).

Release 4.7.0rc1 (June 21, 2023)

  • Add typing_extensions.get_protocol_members and typing_extensions.is_protocol (backport of CPython PR #104878). Patch by Jelle Zijlstra.
  • typing_extensions now re-exports all names in the standard library's typing module, except the deprecated ByteString. Patch by Jelle Zijlstra.
  • Due to changes in the implementation of typing_extensions.Protocol, typing.runtime_checkable can now be used on typing_extensions.Protocol (previously, users had to use typing_extensions.runtime_checkable if they were using typing_extensions.Protocol).
  • Align the implementation of TypedDict with the implementation in the standard library on Python 3.9 and higher. typing_extensions.TypedDict is now a function instead of a class. The private functions _check_fails, _dict_new, and _typeddict_new have been removed. is_typeddict now returns False when called with TypedDict itself as the argument. Patch by Jelle Zijlstra.
  • Declare support for Python 3.12. Patch by Jelle Zijlstra.
  • Fix tests on Python 3.13, which removes support for creating TypedDict classes through the keyword-argument syntax. Patch by Jelle Zijlstra.
  • Fix a regression introduced in v4.6.3 that meant that issubclass(object, typing_extensions.Protocol) would erroneously raise TypeError. Patch by Alex Waygood (backporting the CPython PR python/cpython#105239).
  • Allow Protocol classes to inherit from typing_extensions.Buffer or collections.abc.Buffer. Patch by Alex Waygood (backporting python/cpython#104827, by Jelle Zijlstra).
  • Allow classes to inherit from both typing.Protocol and typing_extensions.Protocol

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typing-extensions&package-manager=pip&previous-version=4.6.3&new-version=4.7.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.in b/requirements.in index 37e15028bedd..6701a1ef1b77 100644 --- a/requirements.in +++ b/requirements.in @@ -4,7 +4,7 @@ # 2) pip-compile --generate-hashes requirements.in # Unittest test adapter -typing-extensions==4.6.3 +typing-extensions==4.7.1 # Fallback env creator for debian microvenv diff --git a/requirements.txt b/requirements.txt index 2436fe08810f..2747490fdba8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,9 +20,9 @@ tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via -r requirements.in -typing-extensions==4.6.3 \ - --hash=sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26 \ - --hash=sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5 +typing-extensions==4.7.1 \ + --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ + --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 # via -r requirements.in zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ From b1c3837e11d62a1593ee8eba15d89fe60d6398f0 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 5 Jul 2023 13:32:11 -0700 Subject: [PATCH 150/336] Correct PATH env var name for non-Windows when applying env var collection for microvenv (#21564) Closes https://github.com/microsoft/vscode-python/issues/21422 This is likely the cause for: ``` ## Extension: ms-python.python - `PATH=/path/to/my/python/bin:undefined` ``` --- .../interpreter/activation/terminalEnvVarCollectionService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index dbf77e72379a..85b393425836 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -32,6 +32,7 @@ import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; import { IEnvironmentActivationService } from './types'; import { EnvironmentType } from '../../pythonEnvironments/info'; +import { getSearchPathEnvVarNames } from '../../common/utils/exec'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService { @@ -172,9 +173,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const activatePath = path.join(path.dirname(interpreter.path), 'activate'); if (!(await pathExists(activatePath))) { const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); + const pathVarName = getSearchPathEnvVarNames()[0]; envVarCollection.replace( 'PATH', - `${path.dirname(interpreter.path)}${path.delimiter}${process.env.Path}`, + `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, { applyAtShellIntegration: true }, ); return; From 27ad70ab5361142348e9d448987e2c0e3e504111 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 5 Jul 2023 14:08:08 -0700 Subject: [PATCH 151/336] Disable creating gh tags for pre-releases (#21563) --- build/azure-pipeline.pre-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index f2e4c2461a6d..7e8c106c2aec 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -28,6 +28,7 @@ extends: template: azure-pipelines/extension/pre-release.yml@templates parameters: publishExtension: ${{ parameters.publishExtension }} + ghCreateTag: false l10nSourcePaths: ./src/client buildSteps: - task: NodeTool@0 From cfee3ddcdbce4d7ce05db554ddad03f2090df625 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 6 Jul 2023 13:36:48 -0700 Subject: [PATCH 152/336] Remove needs labels (#21572) closes https://github.com/microsoft/vscode-python/issues/21366 --- .github/workflows/remove-needs-labels.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/remove-needs-labels.yml diff --git a/.github/workflows/remove-needs-labels.yml b/.github/workflows/remove-needs-labels.yml new file mode 100644 index 000000000000..fea7eafd2152 --- /dev/null +++ b/.github/workflows/remove-needs-labels.yml @@ -0,0 +1,22 @@ +name: 'Remove Needs Label' +on: + issues: + types: [closed] + +jobs: + classify: + name: 'Remove needs labels on issue closing' + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v3 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: Remove Any Needs Labels + uses: ./actions/python-remove-needs-label From 202463600c712853e56bb650e99e4334d1937818 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 6 Jul 2023 15:24:30 -0700 Subject: [PATCH 153/336] Info needed closer (#21575) closes https://github.com/microsoft/vscode-python/issues/21574 --- .github/workflows/info-needed-closer.yml | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/info-needed-closer.yml diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml new file mode 100644 index 000000000000..07104d6bc4be --- /dev/null +++ b/.github/workflows/info-needed-closer.yml @@ -0,0 +1,28 @@ +name: Info-Needed Closer +on: + schedule: + - cron: 20 12 * * * # 5:20am Redmond + repository_dispatch: + types: [trigger-needs-more-info] + workflow_dispatch: + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v2 + with: + repository: 'microsoft/vscode-github-triage-actions' + path: ./actions + ref: stable + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Run info-needed Closer + uses: ./actions/needs-more-info-closer + with: + label: info-needed + closeDays: 30 + closeComment: "Because we have not heard back with the information we requested, we are closing this issue for now. If you are able to provide the info later on, then we will be happy to re-open this issue to pick up where we left off. \n\nHappy Coding!" + pingDays: 30 + pingComment: "Hey @${assignee}, this issue might need further attention.\n\n@${author}, you can help us out by closing this issue if the problem no longer exists, or adding more information." From abcb5cee20fdeac9ba1fdcdc2b6abe55a4fdfc63 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 7 Jul 2023 08:16:02 -0700 Subject: [PATCH 154/336] Move lock issues (#21580) move logic for lock issue workflow back to python repo to avoid extra work of checking out and cloning vscode-github-triage-actions repo. Should reduce time it takes to run github actions. --- .github/workflows/lock-issues.yml | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/.github/workflows/lock-issues.yml b/.github/workflows/lock-issues.yml index 8c828ff766cb..bcd9ea267f9f 100644 --- a/.github/workflows/lock-issues.yml +++ b/.github/workflows/lock-issues.yml @@ -15,17 +15,10 @@ jobs: lock-issues: runs-on: ubuntu-latest steps: - - name: Checkout Actions - uses: actions/checkout@v3 - with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions - - name: 'Lock Issues' - uses: ./actions/python-lock-issues + uses: dessant/lock-threads@v4 with: - token: ${{ github.token }} + github-token: ${{ github.token }} + issue-inactive-days: '30' + process-only: 'issues' + log-output: true From 674459018770b77824fff2038be6c40e47634f29 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 10 Jul 2023 08:19:11 -0700 Subject: [PATCH 155/336] Move more workflows to python (#21581) move logic for lock issue workflow back to python repo to avoid extra work of checking out and cloning vscode-github-triage-actions repo. Should reduce time it takes to run github actions. --- .../community-feedback-auto-comment.yml | 23 +++++++------ .github/workflows/pr-file-check.yml | 33 ++++++++++++++----- .github/workflows/pr-labels.yml | 16 +++------ .github/workflows/remove-needs-labels.yml | 18 ++++------ 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/.github/workflows/community-feedback-auto-comment.yml b/.github/workflows/community-feedback-auto-comment.yml index 57bbd97bf430..1bb8ca9b10da 100644 --- a/.github/workflows/community-feedback-auto-comment.yml +++ b/.github/workflows/community-feedback-auto-comment.yml @@ -8,22 +8,21 @@ jobs: add-comment: if: github.event.label.name == 'needs community feedback' runs-on: ubuntu-latest - permissions: issues: write - steps: - - name: Checkout Actions - uses: actions/checkout@v3 + - name: Check For Existing Comment + uses: peter-evans/find-comment@v2 + id: finder with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions + issue-number: ${{ github.event.issue.number }} + comment-author: 'github-actions[bot]' + body-includes: 'Thanks for the feature request! We are going to give the community' - - name: Add Community Feedback Comment if applicable - uses: ./actions/python-community-feedback-auto-comment + - name: Add Community Feedback Comment + if: steps.finder.outputs.comment-id == '' + uses: peter-evans/create-or-update-comment@v3 with: issue-number: ${{ github.event.issue.number }} + body: | + Thanks for the feature request! We are going to give the community 60 days from when this issue was created to provide 7 👍 upvotes on the opening comment to gauge general interest in this idea. If there's enough upvotes then we will consider this feature request in our future planning. If there's unfortunately not enough upvotes then we will close this issue. diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index 258e07daace7..c0bf09f2cd24 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -1,4 +1,5 @@ name: PR files + on: pull_request: types: @@ -15,15 +16,29 @@ jobs: name: 'Check for changed files' runs-on: ubuntu-latest steps: - - name: Checkout Actions - uses: actions/checkout@v3 + - name: 'package-lock.json matches package.json' + uses: brettcannon/check-for-changed-files@v1.1.0 with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions + prereq-pattern: 'package.json' + file-pattern: 'package-lock.json' + skip-label: 'skip package*.json' + failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - - name: Install Actions - run: npm install --production --prefix ./actions + - name: 'package.json matches package-lock.json' + uses: brettcannon/check-for-changed-files@v1.1.0 + with: + prereq-pattern: 'package-lock.json' + file-pattern: 'package.json' + skip-label: 'skip package*.json' + failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - - name: Check for changed files - uses: ./actions/python-pr-file-check + - name: 'Tests' + uses: brettcannon/check-for-changed-files@v1.1.0 + with: + prereq-pattern: src/**/*.ts + file-pattern: | + src/**/*.test.ts + src/**/*.testvirtualenvs.ts + .github/test_plan.md + skip-label: 'skip tests' + failure-message: 'TypeScript code was edited without also editing a ${file-pattern} file; see the Testing page in our wiki on testing guidelines (the ${skip-label} label can be used to pass this check)' diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 7ddb781e2a85..e953f62d2011 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -13,15 +13,9 @@ jobs: name: 'Classify PR' runs-on: ubuntu-latest steps: - - name: Checkout Actions - uses: actions/checkout@v3 + - name: 'PR impact specified' + uses: mheap/github-action-required-labels@v4 with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions - - - name: Classify PR - uses: ./actions/python-pr-labels + mode: exactly + count: 1 + labels: 'bug, debt, feature-request, no-changelog' diff --git a/.github/workflows/remove-needs-labels.yml b/.github/workflows/remove-needs-labels.yml index fea7eafd2152..3d218e297a11 100644 --- a/.github/workflows/remove-needs-labels.yml +++ b/.github/workflows/remove-needs-labels.yml @@ -8,15 +8,11 @@ jobs: name: 'Remove needs labels on issue closing' runs-on: ubuntu-latest steps: - - name: Checkout Actions - uses: actions/checkout@v3 + - name: 'Removes needs labels on issue close' + uses: actions-ecosystem/action-remove-labels@v1 with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions - - - name: Remove Any Needs Labels - uses: ./actions/python-remove-needs-label + labels: | + needs PR + needs spike + needs community feedback + needs proposal From 523b1f69ebb3bd2a897b8aa12436ced8e9cd107c Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 10 Jul 2023 08:25:34 -0700 Subject: [PATCH 156/336] Update list of repo labels (#21597) For https://github.com/microsoft/vscode-python/issues/20697 --- .github/workflows/issue-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index 98ac4eaca81d..ec2c5eb002fd 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -6,7 +6,7 @@ on: env: # To update the list of labels, see `getLabels.js`. - REPO_LABELS: '["area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' + REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd"]' permissions: From 1308730eea2485ded7e8492ff97bc6a217d8776b Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Mon, 10 Jul 2023 09:40:43 -0700 Subject: [PATCH 157/336] Add pythonwelcome2 walkthrough to activation reason (#21588) pythonWelcomeWithDS no longer exists, but we're currently rolling out a second version of the walkthrough so it'd be nice to get the Python extension to auto activate like it does with pythonWelcome so we can compare apples to apples --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 715179858e24..dfd6a640b3c4 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "onDebugDynamicConfigurations:python", "onDebugResolve:python", "onWalkthrough:pythonWelcome", - "onWalkthrough:pythonWelcomeWithDS", + "onWalkthrough:pythonWelcome2", "onWalkthrough:pythonDataScienceWelcome", "workspaceContains:mspythonconfig.json", "workspaceContains:pyproject.toml", From f77a0117435504a2db7b76512572da74c4b71941 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 10 Jul 2023 15:34:17 -0700 Subject: [PATCH 158/336] fix duplicate class names processed as single node in build test tree pytest (#21601) fixes https://github.com/microsoft/vscode-python/issues/21578 --- .../.data/unittest_folder/test_add.py | 8 ++++ .../.data/unittest_folder/test_subtract.py | 8 ++++ .../expected_discovery_test_output.py | 47 ++++++++++++++++++- pythonFiles/vscode_pytest/__init__.py | 4 +- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py index a96c7f2fa392..e9bdda0ad2ad 100644 --- a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py +++ b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py @@ -19,3 +19,11 @@ def test_add_positive_numbers(self): # test_marker--test_add_positive_numbers def test_add_negative_numbers(self): # test_marker--test_add_negative_numbers result = add(-2, -3) self.assertEqual(result, -5) + + +class TestDuplicateFunction(unittest.TestCase): + # This test's id is unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_a. It has the same class name as + # another test, but it's in a different file, so it should not be confused. + # This test passes. + def test_dup_a(self): # test_marker--test_dup_a + self.assertEqual(1, 1) diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py index 087e5140def4..634a6d81f9eb 100644 --- a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py +++ b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py @@ -24,3 +24,11 @@ def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbe result = subtract(-2, -3) # This is intentional to test assertion failures self.assertEqual(result, 100000) + + +class TestDuplicateFunction(unittest.TestCase): + # This test's id is unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s. It has the same class name as + # another test, but it's in a different file, so it should not be confused. + # This test passes. + def test_dup_s(self): # test_marker--test_dup_s + self.assertEqual(1, 1) diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 0f733c94d141..b05ed5e9f00c 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -106,10 +106,14 @@ # │ └── TestAddFunction # │ ├── test_add_negative_numbers # │ └── test_add_positive_numbers +# │ └── TestDuplicateFunction +# │ └── test_dup_a # └── test_subtract.py # └── TestSubtractFunction # ├── test_subtract_negative_numbers # └── test_subtract_positive_numbers +# │ └── TestDuplicateFunction +# │ └── test_dup_s unittest_folder_path = os.fspath(TEST_DATA_PATH / "unittest_folder") test_add_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_add.py") test_subtract_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_subtract.py") @@ -159,7 +163,26 @@ }, ], "id_": "unittest_folder/test_add.py::TestAddFunction", - } + }, + { + "name": "TestDuplicateFunction", + "path": test_add_path, + "type_": "class", + "children": [ + { + "name": "test_dup_a", + "path": test_add_path, + "lineno": find_test_line_number( + "test_dup_a", + test_add_path, + ), + "type_": "test", + "id_": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + "runID": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + }, + ], + "id_": "unittest_folder/test_add.py::TestDuplicateFunction", + }, ], }, { @@ -197,7 +220,26 @@ }, ], "id_": "unittest_folder/test_subtract.py::TestSubtractFunction", - } + }, + { + "name": "TestDuplicateFunction", + "path": test_subtract_path, + "type_": "class", + "children": [ + { + "name": "test_dup_s", + "path": test_subtract_path, + "lineno": find_test_line_number( + "test_dup_s", + test_subtract_path, + ), + "type_": "test", + "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + "runID": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + }, + ], + "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction", + }, ], }, ], @@ -206,6 +248,7 @@ "id_": TEST_DATA_PATH_STR, } + # This is the expected output for the dual_level_nested_folder tests # └── dual_level_nested_folder # └── test_top_folder.py diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index b880b26fd5b8..d5d7d0e6a9f2 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -266,10 +266,10 @@ def build_test_tree(session: pytest.Session) -> TestNode: test_node = create_test_node(test_case) if isinstance(test_case.parent, pytest.Class): try: - test_class_node = class_nodes_dict[test_case.parent.name] + test_class_node = class_nodes_dict[test_case.parent.nodeid] except KeyError: test_class_node = create_class_node(test_case.parent) - class_nodes_dict[test_case.parent.name] = test_class_node + class_nodes_dict[test_case.parent.nodeid] = test_class_node test_class_node["children"].append(test_node) if test_case.parent.parent: parent_module = test_case.parent.parent From 049ca8b50960473e339ec8b14955ebe340eafc6f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 10 Jul 2023 15:34:33 -0700 Subject: [PATCH 159/336] Return exceptions & skips correctly pytest (#21603) fixes https://github.com/microsoft/vscode-python/issues/21579 --- .../.data/error_raise_exception.py | 14 ++++ .../pytestadapter/.data/parametrize_tests.py | 3 + .../tests/pytestadapter/.data/skip_tests.py | 30 +++++++++ .../expected_discovery_test_output.py | 10 ++- .../expected_execution_test_output.py | 51 ++++++++++++++ .../tests/pytestadapter/test_execution.py | 15 +++++ pythonFiles/vscode_pytest/__init__.py | 66 +++++++++++++++++-- 7 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/error_raise_exception.py create mode 100644 pythonFiles/tests/pytestadapter/.data/skip_tests.py diff --git a/pythonFiles/tests/pytestadapter/.data/error_raise_exception.py b/pythonFiles/tests/pytestadapter/.data/error_raise_exception.py new file mode 100644 index 000000000000..2506089abe07 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/error_raise_exception.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + + +@pytest.fixture +def raise_fixture(): + raise Exception("Dummy exception") + + +class TestSomething: + def test_a(self, raise_fixture): + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py index 6911c9aec7f0..a39b7c26de9f 100644 --- a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py +++ b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + import pytest diff --git a/pythonFiles/tests/pytestadapter/.data/skip_tests.py b/pythonFiles/tests/pytestadapter/.data/skip_tests.py new file mode 100644 index 000000000000..113e3506932a --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/skip_tests.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + +# Testing pytest with skipped tests. The first passes, the second three are skipped. + + +def test_something(): # test_marker--test_something + # This tests passes successfully. + assert 1 + 1 == 2 + + +def test_another_thing(): # test_marker--test_another_thing + # Skip this test with a reason. + pytest.skip("Skipping this test for now") + + +@pytest.mark.skip( + reason="Skipping this test as it requires additional setup" # test_marker--test_complex_thing +) +def test_decorator_thing(): + # Skip this test as well, with a reason. This one uses a decorator. + assert True + + +@pytest.mark.skipif(1 < 5, reason="is always true") # test_marker--test_complex_thing_2 +def test_decorator_thing_2(): + # Skip this test as well, with a reason. This one uses a decorator with a condition. + assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index b05ed5e9f00c..fb8234350fb4 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -489,7 +489,10 @@ { "name": "[1]", "path": parameterize_tests_path, - "lineno": "15", + "lineno": find_test_line_number( + "test_under_ten[1]", + parameterize_tests_path, + ), "type_": "test", "id_": "parametrize_tests.py::test_under_ten[1]", "runID": "parametrize_tests.py::test_under_ten[1]", @@ -497,7 +500,10 @@ { "name": "[2]", "path": parameterize_tests_path, - "lineno": "15", + "lineno": find_test_line_number( + "test_under_ten[2]", + parameterize_tests_path, + ), "type_": "test", "id_": "parametrize_tests.py::test_under_ten[2]", "runID": "parametrize_tests.py::test_under_ten[2]", diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index abe27ffc79ce..3d24f036fe2a 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -150,6 +150,57 @@ }, } +# This is the expected output for the error_raised_exception.py file. +# └── error_raise_exception.py +# ├── TestSomething +# │ └── test_a: failure +error_raised_exception_execution_expected_output = { + "error_raise_exception.py::TestSomething::test_a": { + "test": "error_raise_exception.py::TestSomething::test_a", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": "TRACEBACK", + "subtest": None, + } +} + +# This is the expected output for the skip_tests.py file. +# └── test_something: success +# └── test_another_thing: skipped +# └── test_decorator_thing: skipped +# └── test_decorator_thing_2: skipped +skip_tests_execution_expected_output = { + "skip_tests.py::test_something": { + "test": "skip_tests.py::test_something", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "skip_tests.py::test_another_thing": { + "test": "skip_tests.py::test_another_thing", + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + "skip_tests.py::test_decorator_thing": { + "test": "skip_tests.py::test_decorator_thing", + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + "skip_tests.py::test_decorator_thing_2": { + "test": "skip_tests.py::test_decorator_thing_2", + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, +} + + # This is the expected output for the dual_level_nested_folder.py tests # └── dual_level_nested_folder # └── test_top_folder.py diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 400ef9f883bc..f147a0462f38 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -56,6 +56,19 @@ def test_bad_id_error_execution(): @pytest.mark.parametrize( "test_ids, expected_const", [ + ( + [ + "skip_tests.py::test_something", + "skip_tests.py::test_another_thing", + "skip_tests.py::test_decorator_thing", + "skip_tests.py::test_decorator_thing_2", + ], + expected_execution_test_output.skip_tests_execution_expected_output, + ), + ( + ["error_raise_exception.py::TestSomething::test_a"], + expected_execution_test_output.error_raised_exception_execution_expected_output, + ), ( [ "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", @@ -161,4 +174,6 @@ def test_pytest_execution(test_ids, expected_const): for key in actual_result_dict: if actual_result_dict[key]["outcome"] == "failure": actual_result_dict[key]["message"] = "ERROR MESSAGE" + if actual_result_dict[key]["traceback"] != None: + actual_result_dict[key]["traceback"] = "TRACEBACK" assert actual_result_dict == expected_const diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index d5d7d0e6a9f2..b14a79aef7fd 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -69,14 +69,37 @@ def pytest_exception_interact(node, call, report): """ # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. - if call.excinfo and call.excinfo.typename != "AssertionError": - ERRORS.append( - call.excinfo.exconly() + "\n Check Python Test Logs for more details." - ) + # See if it is during discovery or execution. + # if discovery, then add the error to error logs. + if type(report) == pytest.CollectReport: + if call.excinfo and call.excinfo.typename != "AssertionError": + ERRORS.append( + call.excinfo.exconly() + "\n Check Python Test Logs for more details." + ) + else: + ERRORS.append( + report.longreprtext + "\n Check Python Test Logs for more details." + ) else: - ERRORS.append( - report.longreprtext + "\n Check Python Test Logs for more details." - ) + # if execution, send this data that the given node failed. + report_value = "failure" + node_id = str(node.nodeid) + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) + item_result = create_test_outcome( + node_id, + report_value, + "Test failed with exception", + report.longreprtext, + ) + collected_test = testRunResultDict() + collected_test[node_id] = item_result + cwd = pathlib.Path.cwd() + execution_post( + os.fsdecode(cwd), + "success", + collected_test if collected_test else None, + ) def pytest_keyboard_interrupt(excinfo): @@ -183,6 +206,35 @@ def pytest_report_teststatus(report, config): } +def pytest_runtest_protocol(item, nextitem): + if item.own_markers: + for marker in item.own_markers: + # If the test is marked with skip then it will not hit the pytest_report_teststatus hook, + # therefore we need to handle it as skipped here. + skip_condition = False + if marker.name == "skipif": + skip_condition = any(marker.args) + if marker.name == "skip" or skip_condition: + node_id = str(item.nodeid) + report_value = "skipped" + cwd = pathlib.Path.cwd() + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) + item_result = create_test_outcome( + node_id, + report_value, + None, + None, + ) + collected_test = testRunResultDict() + collected_test[node_id] = item_result + execution_post( + os.fsdecode(cwd), + "success", + collected_test if collected_test else None, + ) + + def pytest_sessionfinish(session, exitstatus): """A pytest hook that is called after pytest has fulled finished. From d3b89852716808d7de6e3f711bf125a221052271 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 10 Jul 2023 16:39:45 -0700 Subject: [PATCH 160/336] Refactor API code in a separate module (#21604) For https://github.com/microsoft/vscode-python/issues/20949 We plan to publish the "api" module as an npm package. Inspired from https://insiders.vscode.dev/github/microsoft/vscode-wasi/blob/main/wasm-wasi/src/api/main.ts#L408. --- src/client/api.ts | 8 +-- src/client/{apiTypes.ts => api/main.ts} | 65 +++++++++++++++++-- src/client/api/package-lock.json | 63 ++++++++++++++++++ src/client/api/package.json | 26 ++++++++ src/client/deprecatedProposedApiTypes.ts | 2 +- src/client/environmentApi.ts | 8 +-- src/client/extension.ts | 8 +-- .../creation/proposed.createEnvApis.ts | 2 +- src/test/api.test.ts | 4 +- src/test/common.ts | 4 +- src/test/environmentApi.unit.test.ts | 6 +- src/test/initialize.ts | 4 +- 12 files changed, 172 insertions(+), 28 deletions(-) rename src/client/{apiTypes.ts => api/main.ts} (87%) create mode 100644 src/client/api/package-lock.json create mode 100644 src/client/api/package.json diff --git a/src/client/api.ts b/src/client/api.ts index 7bc3fc81373b..7cf580d9f78f 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -10,7 +10,7 @@ import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient import { LanguageClient } from 'vscode-languageclient/node'; import { PYLANCE_NAME } from './activation/node/languageClientFactory'; import { ILanguageServerOutputChannel } from './activation/types'; -import { IExtensionApi } from './apiTypes'; +import { PythonExtension } from './api/main'; import { isTestExecution, PYTHON_LANGUAGE } from './common/constants'; import { IConfigurationService, Resource } from './common/types'; import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers'; @@ -29,14 +29,14 @@ export function buildApi( serviceManager: IServiceManager, serviceContainer: IServiceContainer, discoveryApi: IDiscoveryAPI, -): IExtensionApi { +): PythonExtension { const configurationService = serviceContainer.get(IConfigurationService); const interpreterService = serviceContainer.get(IInterpreterService); serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); - const api: IExtensionApi & { + const api: PythonExtension & { /** * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an * iteration or two. @@ -44,7 +44,7 @@ export function buildApi( pylance: ApiForPylance; } & { /** - * @deprecated Use IExtensionApi.environments API instead. + * @deprecated Use PythonExtension.environments API instead. * * Return internal settings within the extension which are stored in VSCode storage */ diff --git a/src/client/apiTypes.ts b/src/client/api/main.ts similarity index 87% rename from src/client/apiTypes.ts rename to src/client/api/main.ts index d30a81582a7e..b9266a732826 100644 --- a/src/client/apiTypes.ts +++ b/src/client/api/main.ts @@ -1,19 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, Event, Uri, WorkspaceFolder } from 'vscode'; -import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types'; +import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem, extensions } from 'vscode'; /* * Do not introduce any breaking changes to this API. * This is the public API for other extensions to interact with this extension. */ - -export interface IExtensionApi { +export interface PythonExtension { /** * Promise indicating whether all parts of the extension have completed loading or not. * @type {Promise} - * @memberof IExtensionApi */ ready: Promise; jupyter: { @@ -128,6 +125,47 @@ export interface IExtensionApi { }; } +interface IJupyterServerUri { + baseUrl: string; + token: string; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + authorizationHeader: any; // JSON object for authorization header. + expiration?: Date; // Date/time when header expires and should be refreshed. + displayName: string; +} + +type JupyterServerUriHandle = string; + +export interface IJupyterUriProvider { + readonly id: string; // Should be a unique string (like a guid) + getQuickPickEntryItems(): QuickPickItem[]; + handleQuickPick(item: QuickPickItem, backEnabled: boolean): Promise; + getServerUri(handle: JupyterServerUriHandle): Promise; +} + +interface IDataFrameInfo { + columns?: { key: string; type: ColumnType }[]; + indexColumn?: string; + rowCount?: number; +} + +export interface IDataViewerDataProvider { + dispose(): void; + getDataFrameInfo(): Promise; + getAllRows(): Promise; + getRows(start: number, end: number): Promise; +} + +enum ColumnType { + String = 'string', + Number = 'number', + Bool = 'bool', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type IRowsResponse = any[]; + export type RefreshOptions = { /** * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so @@ -349,3 +387,20 @@ export type EnvironmentVariablesChangeEvent = { */ readonly env: EnvironmentVariables; }; + +export const PVSC_EXTENSION_ID = 'ms-python.python'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PythonExtension { + export async function api(): Promise { + const extension = extensions.getExtension(PVSC_EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: PythonExtension = extension.exports; + return pythonApi; + } +} diff --git a/src/client/api/package-lock.json b/src/client/api/package-lock.json new file mode 100644 index 000000000000..137262c6523b --- /dev/null +++ b/src/client/api/package-lock.json @@ -0,0 +1,63 @@ +{ + "name": "@vscode/python-extension", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@vscode/python-extension", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/vscode": "^1.78.0" + }, + "devDependencies": { + "@types/node": "^16.11.7", + "typescript": "^4.7.2" + } + }, + "node_modules/@types/node": { + "version": "16.18.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.27.tgz", + "integrity": "sha512-GFfndd/RINWD19W+xNJ9Qh/sOZ5ieTiOSagA86ER/12i/l+MEnQxsbldGRF23azWjRfe7zUlAldyrwN84a1E5w==", + "dev": true + }, + "node_modules/@types/vscode": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.78.0.tgz", + "integrity": "sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + }, + "dependencies": { + "@types/node": { + "version": "16.18.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.27.tgz", + "integrity": "sha512-GFfndd/RINWD19W+xNJ9Qh/sOZ5ieTiOSagA86ER/12i/l+MEnQxsbldGRF23azWjRfe7zUlAldyrwN84a1E5w==", + "dev": true + }, + "@types/vscode": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.78.0.tgz", + "integrity": "sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==" + }, + "typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true + } + } +} diff --git a/src/client/api/package.json b/src/client/api/package.json new file mode 100644 index 000000000000..901ab3f13cfd --- /dev/null +++ b/src/client/api/package.json @@ -0,0 +1,26 @@ +{ + "name": "@vscode/python-extension", + "description": "VSCode Python extension's public API", + "version": "1.0.0", + "publisher": "ms-python", + "author": { + "name": "Microsoft Corporation" + }, + "types": "./index.d.ts", + "license": "MIT", + "homepage": "https://github.com/Microsoft/vscode-python", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-python" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-python/issues" + }, + "dependencies": { + "@types/vscode": "^1.78.0" + }, + "devDependencies": { + "@types/node": "^16.11.7", + "typescript": "^4.7.2" + } +} diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts index 14cabe1d09ae..407cb1dab394 100644 --- a/src/client/deprecatedProposedApiTypes.ts +++ b/src/client/deprecatedProposedApiTypes.ts @@ -4,7 +4,7 @@ import { Uri, Event } from 'vscode'; import { PythonEnvKind, EnvPathType } from './pythonEnvironments/base/info'; import { ProgressNotificationEvent, GetRefreshEnvironmentsOptions } from './pythonEnvironments/base/locator'; -import { Resource } from './apiTypes'; +import { Resource } from './api/main'; export interface EnvironmentDetailsOptions { useCache: boolean; diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts index 3e846eb2772f..ee0b28ae2ff4 100644 --- a/src/client/environmentApi.ts +++ b/src/client/environmentApi.ts @@ -26,11 +26,11 @@ import { EnvironmentTools, EnvironmentType, EnvironmentVariablesChangeEvent, - IExtensionApi, + PythonExtension, RefreshOptions, ResolvedEnvironment, Resource, -} from './apiTypes'; +} from './api/main'; import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi'; type ActiveEnvironmentChangeEvent = { @@ -114,7 +114,7 @@ function filterUsingVSCodeContext(e: PythonEnvInfo) { export function buildEnvironmentApi( discoveryApi: IDiscoveryAPI, serviceContainer: IServiceContainer, -): IExtensionApi['environments'] { +): PythonExtension['environments'] { const interpreterPathService = serviceContainer.get(IInterpreterPathService); const configService = serviceContainer.get(IConfigurationService); const disposables = serviceContainer.get(IDisposableRegistry); @@ -180,7 +180,7 @@ export function buildEnvironmentApi( onEnvironmentVariablesChanged, ); - const environmentApi: IExtensionApi['environments'] = { + const environmentApi: PythonExtension['environments'] = { getEnvironmentVariables: (resource?: Resource) => { sendApiTelemetry('getEnvironmentVariables'); resource = resource && 'uri' in resource ? resource.uri : resource; diff --git a/src/client/extension.ts b/src/client/extension.ts index 5fcb63e2d322..89649d377c74 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -41,7 +41,7 @@ import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; import { IStartupDurations } from './types'; import { runAfterActivation } from './common/utils/runAfterActivation'; import { IInterpreterService } from './interpreter/contracts'; -import { IExtensionApi } from './apiTypes'; +import { PythonExtension } from './api/main'; import { WorkspaceService } from './common/application/workspace'; import { disposeAll } from './common/utils/resourceLifecycle'; import { ProposedExtensionAPI } from './proposedApiTypes'; @@ -58,8 +58,8 @@ let activatedServiceContainer: IServiceContainer | undefined; ///////////////////////////// // public functions -export async function activate(context: IExtensionContext): Promise { - let api: IExtensionApi; +export async function activate(context: IExtensionContext): Promise { + let api: PythonExtension; let ready: Promise; let serviceContainer: IServiceContainer; try { @@ -103,7 +103,7 @@ async function activateUnsafe( context: IExtensionContext, startupStopWatch: StopWatch, startupDurations: IStartupDurations, -): Promise<[IExtensionApi & ProposedExtensionAPI, Promise, IServiceContainer]> { +): Promise<[PythonExtension & ProposedExtensionAPI, Promise, IServiceContainer]> { // Add anything that we got from initializing logs to dispose. context.subscriptions.push(...logDispose); diff --git a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts index 52209a5a31d0..e0e79134fc56 100644 --- a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts +++ b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License import { Event, Disposable, WorkspaceFolder } from 'vscode'; -import { EnvironmentTools } from '../../apiTypes'; +import { EnvironmentTools } from '../../api/main'; export type CreateEnvironmentUserActions = 'Back' | 'Cancel'; export type EnvironmentProviderId = string; diff --git a/src/test/api.test.ts b/src/test/api.test.ts index 24eb78c11bf0..488ce79073c2 100644 --- a/src/test/api.test.ts +++ b/src/test/api.test.ts @@ -2,12 +2,12 @@ // Licensed under the MIT License. import { expect } from 'chai'; -import { IExtensionApi } from '../client/apiTypes'; +import { PythonExtension } from '../client/api/main'; import { ProposedExtensionAPI } from '../client/proposedApiTypes'; import { initialize } from './initialize'; suite('Python API tests', () => { - let api: IExtensionApi & ProposedExtensionAPI; + let api: PythonExtension & ProposedExtensionAPI; suiteSetup(async () => { api = await initialize(); }); diff --git a/src/test/common.ts b/src/test/common.ts index 0a76c495830a..0168ee47fc37 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -10,7 +10,7 @@ import * as glob from 'glob'; import * as path from 'path'; import { coerce, SemVer } from 'semver'; import { ConfigurationTarget, Event, TextDocument, Uri } from 'vscode'; -import type { IExtensionApi } from '../client/apiTypes'; +import type { PythonExtension } from '../client/api/main'; import { IProcessService } from '../client/common/process/types'; import { IDisposable } from '../client/common/types'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; @@ -438,7 +438,7 @@ export async function isPythonVersion(...versions: string[]): Promise { } } -export interface IExtensionTestApi extends IExtensionApi, ProposedExtensionAPI { +export interface IExtensionTestApi extends PythonExtension, ProposedExtensionAPI { serviceContainer: IServiceContainer; serviceManager: IServiceManager; } diff --git a/src/test/environmentApi.unit.test.ts b/src/test/environmentApi.unit.test.ts index a4ea73fb6c92..76441247db49 100644 --- a/src/test/environmentApi.unit.test.ts +++ b/src/test/environmentApi.unit.test.ts @@ -36,8 +36,8 @@ import { ActiveEnvironmentPathChangeEvent, EnvironmentVariablesChangeEvent, EnvironmentsChangeEvent, - IExtensionApi, -} from '../client/apiTypes'; + PythonExtension, +} from '../client/api/main'; suite('Python Environment API', () => { const workspacePath = 'path/to/workspace'; @@ -57,7 +57,7 @@ suite('Python Environment API', () => { let onDidChangeEnvironments: EventEmitter; let onDidChangeEnvironmentVariables: EventEmitter; - let environmentApi: IExtensionApi['environments']; + let environmentApi: PythonExtension['environments']; setup(() => { serviceContainer = typemoq.Mock.ofType(); diff --git a/src/test/initialize.ts b/src/test/initialize.ts index f4f37204da85..8a7abca0d91c 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import type { IExtensionApi } from '../client/apiTypes'; +import type { PythonExtension } from '../client/api/main'; import { clearPythonPathInWorkspaceFolder, IExtensionTestApi, @@ -42,7 +42,7 @@ export async function initialize(): Promise { return (api as any) as IExtensionTestApi; } export async function activateExtension() { - const extension = vscode.extensions.getExtension(PVSC_EXTENSION_ID_FOR_TESTS)!; + const extension = vscode.extensions.getExtension(PVSC_EXTENSION_ID_FOR_TESTS)!; const api = await extension.activate(); // Wait until its ready to use. await api.ready; From cd10e3898e591cfafc76f2ccc08935c3e3f5b265 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 10 Jul 2023 17:02:23 -0700 Subject: [PATCH 161/336] Download `get-pip.py` during build (#21589) --- .github/actions/build-vsix/action.yml | 7 +- .github/workflows/build.yml | 9 +- .github/workflows/pr-check.yml | 9 +- build/azure-pipeline.pre-release.yml | 5 +- build/azure-pipeline.stable.yml | 5 +- build/build-install-requirements.txt | 2 + build/debugger-install-requirements.txt | 2 - gulpfile.js | 14 +- pythonFiles/download_get_pip.py | 46 + pythonFiles/get-pip.py | 27086 ---------------- .../common/process/internal/scripts/index.ts | 3 - 11 files changed, 86 insertions(+), 27102 deletions(-) create mode 100644 build/build-install-requirements.txt delete mode 100644 build/debugger-install-requirements.txt create mode 100644 pythonFiles/download_get_pip.py delete mode 100644 pythonFiles/get-pip.py diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index b84d3e0871f0..6c4621c7eb9b 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -29,7 +29,7 @@ runs: cache: 'pip' cache-dependency-path: | requirements.txt - build/debugger-install-requirements.txt + build/build-install-requirements.txt pythonFiles/jedilsp_requirements/requirements.txt - name: Upgrade Pip @@ -46,10 +46,11 @@ runs: with: options: '-t ./pythonFiles/lib/python --implementation py' - - name: Install debugpy + - name: Install debugpy and get-pip run: | - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python -m pip --disable-pip-version-check install packaging python ./pythonFiles/install_debugpy.py + python ./pythonFiles/download_get_pip.py shell: bash - name: Install Jedi LSP diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f418a802ff8e..24d91b94da10 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -147,6 +147,13 @@ jobs: with: python-version: ${{ matrix.python }} + - name: Download get-pip.py + run: | + python -m pip install wheel + python -m pip install -r build/build-install-requirements.txt + python ./pythonFiles/download_get_pip.py + shell: bash + - name: Install debugpy run: | # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. @@ -172,7 +179,7 @@ jobs: - name: Install debugpy wheels (Python ${{ matrix.python }}) run: | python -m pip install wheel - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python -m pip install -r build/build-install-requirements.txt python ./pythonFiles/install_debugpy.py shell: bash if: matrix.test-suite == 'debugger' diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 84903463e204..aa9cae2aa474 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -127,6 +127,13 @@ jobs: # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + - name: Download get-pip.py + run: | + python -m pip install wheel + python -m pip install -r build/build-install-requirements.txt + python ./pythonFiles/download_get_pip.py + shell: bash + - name: Install base Python requirements uses: brettcannon/pip-secure-install@v1 with: @@ -147,7 +154,7 @@ jobs: - name: Install debugpy wheels (Python ${{ matrix.python }}) run: | python -m pip install wheel - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python -m pip --disable-pip-version-check install -r build/build-install-requirements.txt python ./pythonFiles/install_debugpy.py shell: bash if: matrix.test-suite == 'debugger' diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index 7e8c106c2aec..d4fe0ac376fb 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -53,9 +53,10 @@ extends: displayName: Install wheel - script: | - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python -m pip --disable-pip-version-check install -r build/build-install-requirements.txt python ./pythonFiles/install_debugpy.py - displayName: Install debugpy + python ./pythonFiles/download_get_pip.py + displayName: Install debugpy and get-pip.py - script: | python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --implementation py -r ./requirements.txt diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 5c8444de9b2e..05f83aa81824 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -48,9 +48,10 @@ extends: displayName: Install wheel - script: | - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python -m pip --disable-pip-version-check install -r build/build-install-requirements.txt python ./pythonFiles/install_debugpy.py - displayName: Install debugpy + python ./pythonFiles/download_get_pip.py + displayName: Install debugpy and get-pip.py - script: | python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --implementation py -r ./requirements.txt diff --git a/build/build-install-requirements.txt b/build/build-install-requirements.txt new file mode 100644 index 000000000000..8baaa59ded67 --- /dev/null +++ b/build/build-install-requirements.txt @@ -0,0 +1,2 @@ +# Requirements needed to run install_debugpy.py and download_get_pip.py +packaging diff --git a/build/debugger-install-requirements.txt b/build/debugger-install-requirements.txt deleted file mode 100644 index 6ee0765db4b3..000000000000 --- a/build/debugger-install-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Requirements needed to run install_debugpy.py -packaging diff --git a/gulpfile.js b/gulpfile.js index a344b165a6cc..0d47127f187e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -286,7 +286,7 @@ gulp.task('installDebugpy', async () => { '-t', './pythonFiles/lib/temp', '-r', - './build/debugger-install-requirements.txt', + './build/build-install-requirements.txt', ]; await spawnAsync(process.env.CI_PYTHON_PATH || 'python', depsArgs, undefined, true) .then(() => true) @@ -295,7 +295,7 @@ gulp.task('installDebugpy', async () => { return false; }); - // Install new DEBUGPY with wheels for python 3.7 + // Install new DEBUGPY with wheels for python const wheelsArgs = ['./pythonFiles/install_debugpy.py']; const wheelsEnv = { PYTHONPATH: './pythonFiles/lib/temp' }; await spawnAsync(process.env.CI_PYTHON_PATH || 'python', wheelsArgs, wheelsEnv, true) @@ -305,6 +305,16 @@ gulp.task('installDebugpy', async () => { return false; }); + // Download get-pip.py + const getPipArgs = ['./pythonFiles/download_get_pip.py']; + const getPipEnv = { PYTHONPATH: './pythonFiles/lib/temp' }; + await spawnAsync(process.env.CI_PYTHON_PATH || 'python', getPipArgs, getPipEnv, true) + .then(() => true) + .catch((ex) => { + console.error("Failed to download get-pip wheels using 'python'", ex); + return false; + }); + rmrf.sync('./pythonFiles/lib/temp'); }); diff --git a/pythonFiles/download_get_pip.py b/pythonFiles/download_get_pip.py new file mode 100644 index 000000000000..b8238d60f261 --- /dev/null +++ b/pythonFiles/download_get_pip.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import os +import pathlib +import urllib.request as url_lib +from packaging.version import parse as version_parser + +EXTENSION_ROOT = pathlib.Path(__file__).parent.parent +GET_PIP_DEST = EXTENSION_ROOT / "pythonFiles" +PIP_PACKAGE = "pip" +PIP_VERSION = "latest" # Can be "latest", or specific version "23.1.2" + + +def _get_package_data(): + json_uri = "https://pypi.org/pypi/{0}/json".format(PIP_PACKAGE) + # Response format: https://warehouse.readthedocs.io/api-reference/json/#project + # Release metadata format: https://github.com/pypa/interoperability-peps/blob/master/pep-0426-core-metadata.rst + with url_lib.urlopen(json_uri) as response: + return json.loads(response.read()) + + +def _download_and_save(root, version): + root = os.getcwd() if root is None or root == "." else root + url = f"https://raw.githubusercontent.com/pypa/get-pip/{version}/public/get-pip.py" + print(url) + with url_lib.urlopen(url) as response: + data = response.read() + get_pip_file = pathlib.Path(root) / "get-pip.py" + get_pip_file.write_bytes(data) + + +def main(root): + data = _get_package_data() + + if PIP_VERSION == "latest": + use_version = max(data["releases"].keys(), key=version_parser) + else: + use_version = PIP_VERSION + + _download_and_save(root, use_version) + + +if __name__ == "__main__": + main(GET_PIP_DEST) diff --git a/pythonFiles/get-pip.py b/pythonFiles/get-pip.py deleted file mode 100644 index 2c411ecf21e3..000000000000 --- a/pythonFiles/get-pip.py +++ /dev/null @@ -1,27086 +0,0 @@ -#!/usr/bin/env python -# -# Hi There! -# -# You may be wondering what this giant blob of binary data here is, you might -# even be worried that we're up to something nefarious (good for you for being -# paranoid!). This is a base85 encoding of a zip file, this zip file contains -# an entire copy of pip (version 21.3.1). -# -# Pip is a thing that installs packages, pip itself is a package that someone -# might want to install, especially if they're looking to run this get-pip.py -# script. Pip has a lot of code to deal with the security of installing -# packages, various edge cases on various platforms, and other such sort of -# "tribal knowledge" that has been encoded in its code base. Because of this -# we basically include an entire copy of pip inside this blob. We do this -# because the alternatives are attempt to implement a "minipip" that probably -# doesn't do things correctly and has weird edge cases, or compress pip itself -# down into a single file. -# -# If you're wondering how this is created, it is generated using -# `scripts/generate.py` in https://github.com/pypa/get-pip. - -import sys - -this_python = sys.version_info[:2] -min_version = (3, 6) -if this_python < min_version: - message_parts = [ - "This script does not work on Python {}.{}".format(*this_python), - "The minimum supported Python version is {}.{}.".format(*min_version), - "Please use https://bootstrap.pypa.io/pip/{}.{}/get-pip.py instead.".format( - *this_python - ), - ] - print("ERROR: " + " ".join(message_parts)) - sys.exit(1) - - -import os.path -import pkgutil -import shutil -import tempfile -from base64 import b85decode - - -def determine_pip_install_arguments(): - implicit_pip = True - implicit_setuptools = True - implicit_wheel = True - - # Check if the user has requested us not to install setuptools - if "--no-setuptools" in sys.argv or os.environ.get("PIP_NO_SETUPTOOLS"): - args = [x for x in sys.argv[1:] if x != "--no-setuptools"] - implicit_setuptools = False - else: - args = sys.argv[1:] - - # Check if the user has requested us not to install wheel - if "--no-wheel" in args or os.environ.get("PIP_NO_WHEEL"): - args = [x for x in args if x != "--no-wheel"] - implicit_wheel = False - - # We only want to implicitly install setuptools and wheel if they don't - # already exist on the target platform. - if implicit_setuptools: - try: - import setuptools # noqa - - implicit_setuptools = False - except ImportError: - pass - if implicit_wheel: - try: - import wheel # noqa - - implicit_wheel = False - except ImportError: - pass - - # Add any implicit installations to the end of our args - if implicit_pip: - args += ["pip"] - if implicit_setuptools: - args += ["setuptools"] - if implicit_wheel: - args += ["wheel"] - - return ["install", "--upgrade", "--force-reinstall"] + args - - -def monkeypatch_for_cert(tmpdir): - """Patches `pip install` to provide default certificate with the lowest priority. - - This ensures that the bundled certificates are used unless the user specifies a - custom cert via any of pip's option passing mechanisms (config, env-var, CLI). - - A monkeypatch is the easiest way to achieve this, without messing too much with - the rest of pip's internals. - """ - from pip._internal.commands.install import InstallCommand - - # We want to be using the internal certificates. - cert_path = os.path.join(tmpdir, "cacert.pem") - with open(cert_path, "wb") as cert: - cert.write(pkgutil.get_data("pip._vendor.certifi", "cacert.pem")) - - install_parse_args = InstallCommand.parse_args - - def cert_parse_args(self, args): - if not self.parser.get_default_values().cert: - # There are no user provided cert -- force use of bundled cert - self.parser.defaults["cert"] = cert_path # calculated above - return install_parse_args(self, args) - - InstallCommand.parse_args = cert_parse_args - - -def bootstrap(tmpdir): - monkeypatch_for_cert(tmpdir) - - # Execute the included pip and use it to install the latest pip and - # setuptools from PyPI - from pip._internal.cli.main import main as pip_entry_point - - args = determine_pip_install_arguments() - sys.exit(pip_entry_point(args)) - - -def main(): - tmpdir = None - try: - # Create a temporary working directory - tmpdir = tempfile.mkdtemp() - - # Unpack the zipfile into the temporary directory - pip_zip = os.path.join(tmpdir, "pip.zip") - with open(pip_zip, "wb") as fp: - fp.write(b85decode(DATA.replace(b"\n", b""))) - - # Add the zipfile to sys.path so that we can import it - sys.path.insert(0, pip_zip) - - # Run the bootstrap - bootstrap(tmpdir=tmpdir) - finally: - # Clean up our temporary working directory - if tmpdir: - shutil.rmtree(tmpdir, ignore_errors=True) - - -DATA = b""" -P)h>@6aWAK2mt$eR#TliG(h+O003nH000jF003}la4%n9X>MtBUtcb8c|B0UO2j}6z0X&KUUXrdvMRV -16ubz6s0VM$QfAw<4YV^ulDhQoop$MlK*;0ehKz6 -^g4|bOsV`^+*aO7_tw^Cd$4zs{Pl#j>6{|X*AaQ6!2wJ?w>%d+2&1X4Rc!^r6h-hMtH_d5{IF3D`nKTt~p1QY-O00;p4c~(;+8{@xi0ssK6 -1ONaJ0001RX>c!JUu|J&ZeL$6aCu!*!ET%|5WVviBXU@FMMw_qp;5O|rCxIBA*z%^Qy~|I#aghDZI*1 -mzHbcdCgFtbH*em&nbG}VT_EcdJ^%Uh<#$rfXmjvMazjtt+Y{4fL(0@tjn1(F!nz|6RBOjouLCQKB%tCsn -f_O;(TkT9D!5I2G1vZWcORK< -*}iONjWAr8Zm1&KuL0jC{@?djd+x5R}RGfYPBawx08>U(W?WmDk1T9S4?epCt{Z(ueTz)EC*E`5mT15 --&2~-DsS-6=uU3I|BmObEPJI*Sr)^2!Om@h-$wOJl_c@O>A_3OHg5wqIeD(E7`y@m0ou*N^~8Scf|wu -`N_HtL5`*k&gASg%W(oQp9a7<~IpnR_S}F8z9z|q{`1rb)-o!>My0eex)q(ByedFLGyO7=Ikq8}(HcH -6i;acy-%V$hD`fEosH@wgA+8z#{H{ToXOd_?&uMj~(yRVmD7BE?-`X6FU!78rkLs#HE1jqSOWnjp~Z3(}j4wN{#<0DmEaw -w2fbN$l@K=F!>KqO9KQH000080Q-4XQ_aV~HNXG>03HDV01N;C0B~t=FK~G-ba`-PWF?NVPQ@?`MfZN -i-B_Ob4-5=!Z$M%;t!XVKc9b}U{5?(?Egj!;iWEo#VY8e`cO+3psdiM#D?U$24DrcGE{QX%^A1rwho7 -bo%%^4nEOe11`ih5ds}r~C4-D(by*bnzy~VhcmspFPs+92he4iKm495?R6(6IB9*bzqWO6Z``e?dj4> -$ei>cuLo8^bh>J0qwmAsn45g@9MQ{TAMQ=}M~B1K+Woqz5;+g_LK&{q3XhT~awQHE!$j2T)4`1QY-O0 -0;p4c~(=FwQ!+P0RR9!0ssIR0001RX>c!JX>N37a&BR4FJE72ZfSI1UoLQYb&)Yo#4rqn_xuX$SgsPJ -3leY=j7%q3*bq8})@_Z_B-k#f{~ot+ASB2V>&bck^4xJALFYoL2O3Leg*}O$!hKQ7DMaVKUUslOCh)if@+itrPZeClT~ -1iR*^N=_&VilHX7ezR{Ys!P3i6v#8#CnCLX(r^h#(D9Q2`wcYz#AqB@vfzGIq$A8sk{)NWEK&TeAplO -P?6fq6Q1^6a*0l)grsP?n#H~**AHt%UnWjY1bq&q0|@WSC{?>xZeNm!(&pOOE&dqH}AXz$)6~;-HFq; -xJFdD4^T@31QY-O00;p4c~(c!JX>N37a&BR4FJg6RY-C?$ZgwtkdDU9 -obKAHPf7f4uG7lkBlGE!=dmYW`dihW;o~E`ZcCNi@JUohoY@8{Q1wh-1$NzhG@j(J4?IbgOIlV{(b{C -7?A9fc@1wrttV^vAk^$p`qy{EM#ouDPzHJmWfRJmkLP0Eh5`jUu}2}!od0gsCy2o?*rZyPR2(bSUO$% -<|5NYz|kB9(b;g#Fd#^2(tThkgbn-15A&&!1SkV-;QOc(aEUs)`n$97g+j+@~e@<#e6Bez$)8kE7$CVsa!Y&$ksdzhuK>@*WHllam(J%Bz^1 -QFuJ>TBK59wcM7qX?8>Fvf*h#xnw(L7rDKHEljCe&?`s#rJVk^W1OOEdeuJ+V^6W(P%hAYhU;hjIOt? -2vJB0fWh56koK;Ps{O-tR;9d?}OpA)80<2VnFw5Vxw9d@n9FLVJJhuS000yys;B?3CXqmx?Fhd=u2$L -Ckdn)rXm$@sB4hWuO=_IQ}D!OgUn}Uj7lOnIGY#4r=RnmQ%m5le`faf>hg931HhzU-^Y`1Ea-aQB*l>FFREh)$5jY2R>#sl -UWuDTJ2(W2$w`i9+Bh+a@^EZli~*{QY3)2@XMbNRCX=Qyv-{?{i!Xhm5ElvxeI#=`~ -D?LO(6baf!usZ{VAodthv$pdbX0 -|q%mA+?{9piAA{!!stmrt0q3V$EuC6g`A+nT|BM2+3s>qh=U=AFthLvH+k0!N|r9wJ!j!=fA$q`6UAS{-Ga -(eL*g?2>_1%t|33HNce3`{^o3NV21-EIponyvONX0_bnWq$thPGHCZ|R4{P7TcWCqn9dCn}ym+Ans=a ->N`DS#m=&*%-GN%tdJ~G8IL_C>r_Dv+ba=a! -VgIRWan$LZbsL6xMVoz~81qqTbPQPn$khB?V(*t%SlRv3Mo`_qk>@hbk}Cq^~|6y?>LfkAH@&35JAK5 -1H1mT%Gd{5bFm(8}bk^PW|M^=@6rHY?DanXt&!q{>@Tx$k!rQJLg}2s(t4ScUH=DGin8&9C__68M{q(n|SlK>S*QE*7Gx8RY1 -y_%fH?47QqMfUPB}6DyjinRLhBK%obEtt2A~Q7~W)A$hSzb)&uj}Tv)}7c^_QTFovq0k$sq=1AQ=^!BI_En|a4Ggg|)oU`+0c4al`EZ^4``D&Jo4JP3a?0bO$_CR%5e3 -;C?%8VZV>f;=0;gV_JE2j!!vqIu#XFj{2k`%(_hZtog5#Zd^}r!I6FFD4`YhLjqyVzYvPBOINd1HRAuH7*&S^3x&tM`*%12XpXMC8OX0OawPi)z@ur}DW+Ps43vA!#)8oaph8K7Qr=zY?W=&dW*ZMPZ18Dt|G;kK{ -AiVjxgnL587Vp4G7UWB8xZ(w71#7OpxuiK^#`{kx*0~-@h@ox+-R-gUCgZ+yuT3l!DoqOa7ypKZ>Yg> -z|kR2?e8i|`TDmVHU%*J>b1&?5-QBhwE>Opiex9vY;4iv*of}Mn21$91#TvwkZQj%szLUU)K5W{bCh( -2Yclp_+C7LKSr6XH=Z$l@y0|F&G?qQO;cJjb*=-vK(_jaq);Q-KvB1#&Vlm%a>)MdAlWL9EkQ4GqgQ3 -#cym3KhY)n&Bg7+YWJ#OscgtVdS0DZRpX()g-PD3 -%YgU&v7i+@)gc%pnw*b)O0bLkv_MU&s$$2iVCK{?YTI$3Ud-omnn$bD)cA_YTv0sIx$}Gdd -HE%@ukz>K$yxvWLX&7GC<|s~W~5iG3FtTNLAap0_*CC#LCUAJrYs>D8_w*_}zSS*WgOg}n3GpON#EHz -!Lt<@@G_s`eq-R!wn@Z((Y6c~b9xzD@s1MAzcYD$_XxN;c0jQirMpBeZU6GkcjOB60 -4Q`YS~WSoHZvD_R*%G~z9xkChTKxJ1E^rGw9VcCl1l(C164}0Jz$lMqMWYkNE*J~={u=?%UHGEOpX0q -^cAsRY|r%$zgMlp?`EP0w$iWxjR176^{_S`Zof(V1*vr;4qTY(QrNgTe60CC*NJ)h%*`!1Eywg(oQ}I -Pr?VR6({Xd?{0URA{RqlRR%j;=CEU}SaFrh&@c(BNS=wIUSH=(Q1dn=jzMl@*CZk0sr`CVmFM+YisCo -{Pgk555Ea`*%l%j5uPIzW86SMD~Otez{h#5(@Q2izPO;ch~?nv;iGy3%%Rt)Ri4qs&7(D(F)RuHScAK -vM`S-<-DlYcDGhPL|{Bsb303kw^4`BcY)HCSI|*hkQHnVZOsfv!E-gd539!X|u&fb&!9z5|JbT+#&cS!ES;s51 -gt5Rd=K63LopF(|!6rx;Y+xYW{ORIiT95)Y&of1ZPIJh=Szb)z;%J3Lu_uZv0WMh35$G&8jj}$R5XFj -T1gnbG*Ql2T1bk&U_VmURq)QZC5GxrMj-65NRU@P$SMpAP6EhtCjA%oeASnpPuM6+0U_`>XZ*DV;m~e -PGttebj=R^-C0J>mK5*~iYJo@Z>P6ALS_LMCiAsf$_-MMkt@tpFfx#M7mC&*5ZPP4P~m&b2jzCSr$XR -p^E&V!}?2T2$W4S>G2ZU2)Inpmx>A~WXiXY@CS5Y>w<>B@Y^zD_IeX?Tft+?=%I7ir;mAnISOy@Z-CX -52MBZ -08mQ<1QY-O00;p4c~(;e)76y~3jhG&Bme*w0001RX>c!JX>N37a&BR4FJob2Xk{*NdEHuVkK4u({(iq -=s~|8{$h^QvT~vSyB+j`u;G{+D!XFL?Vnwc`wJ9#kUEWy<`rrG^?2=rP(#1`IHmE-6#C@5a_jzV{i^b -xF%nwR@FDtoMM^(A2#bR-FrH{2~oH$5(DD}2`{9sMh{VvUZud99cXzbOlF-PG}HAY1k{iZst#CJM(EA -d8KeE+p}+ElV!iMPsK`7O1s)9hYVg=x}S<{u@|O`Y7^j?6o`UkP0~)zpo`cUH-x8jswo#)9%=6kDguo -@6d7Q|Vlm`X|NYVrG~yxJ=cjTrtP}zSq?~_7v|AN|i5lsd(#|okvrs(xyAp9Hq;0Q@O^J9g&wj`oa%B -vb)sP$8OIX{C;HV12NRCW$w-`W)-AP9qX*nO|M=&f2SLjJJY~kG>zHpqpk{jnM&IX+N`BJWX@z5ySgI -JP>tAhE|Tt*d&6T%#;VS;<<-?yp>`r82Lmg)ONuo+%B^+HO5p2mDW3kBeypzqKJdyPm1~le#qdQhJVy;s&HBxYVpYXwJHFUdEMVhhn^4oBqqr=o7my)Kl6X -Hq~G!5$hTa3WDiCk5MroWg=I(OQ!LN56$Ex)$%Sw=u?%S{#1!R2nZHyW|=%D$Mo+4x=q2&kVddgENoX -F%kM~H55&ZZ573Oqh#S(JAa@oOY@+L%pYvm;^Cn4L*T>GsXGLc9d^UArY#HD*))L^eUc}9@ac(=RUw{ -O(>A%nL!)@BsmfD#mOzlU$}T&Fdu_4D!Hu=cvZN<#Rk>TmDr66wYH6gH)m$c|GjiQKCd;n-gQjZMOL5RNQliq-}*DPR8_>VzZeX5t$RYCG%o)4uZ!yn|PB_psYD>vOTB(v55wwz%%}$D0?;D5 -9tSdNjhJ$TAS*}lvR9QtW>N4|ft*M~t;G{gX`A5WNJJ~~fJish -6W8sG$bBFd8ugSml7IjG$2Z_8m-MW`q23>;K;MH&Oe2{iZzCaBym;5hJy-LA9tBN*TuxCVx27d|jg6u -VYxFWW-Jm_v9Ja+DtsZ6x7QSNIi>2w9>xbVLZQkegb0SJGwqbgRgS$aV!B)Oz>Zwi -@}5%Gz$H8n7a`zDHyVRRiBp`ZeC-^$Dh_`qMFlG+a;$Q%0selk?yP2#4o&4_)5mqx`T7Ya+o8cK -yP)a-ANEKOCtgY=W4sYzTQKkcAH}Hb$zPkH9*6)wibE#`j5~4^nC7Nw~HyJV}ncwqf~ieYY=+2JB(8u -9@xF{Qc@s-9ET^{!WVQ3$tPtgeAKbp`jCV735!Zt$|j;`Ro*tF7gTWMct?d1MkaE9c)o%uou@CUtTp5 -}&Nx|u0as%#f&OFnIJ8!v8d^_REmQFdDe|7SjqGBB(T?&X;tN=d>UZoQg9Qmckhmm9F7btQ5k))&75r -}#qp@Dm%L^H<0=@|x4M4>j#62dV{(Ev-I5zp5#8}0E#MBYB5{t^w{s&@=l9W$w?5i!~62#|dCFs#%5w -(|Z2Z_4;b?ZgDT|c{91u<`*t-l@~zFt2c9-go7?gnWC++$L+dV|OVAXD>7vl<$U%y%BXyI@o?lp*v*Q -5nLP3<=TKF|bX^aZ=l1L5~m45$|S+jW|1w=#CR&knT1Tcqrdzzye|TY*MY0^V}?B7J5;pm7c>CE>5UD -=_>s%@;GRota}$3903*>Cr%j)fNDl6N$6|D)qt#^+k}2jj;4s|&!P-ys2Q{F!tya|sjMkCCrLlFVg{G -XsdEi`1`nIFe-_R3jS+p~=BO`YoP-EL`!ZAv0D+|As@Jtj%#zf|3_lq6`dF8I6QGKlrZG*IJp*$S;M_ -k&F%X$0j)1QBXAm|l0y3r^623u&W$gn59e-F7f(FFr;#x|4)FVSw8H-6qM#5H~sHCnuKzbngny?Q85t -l%u0iVQYe4c7TgZEa`95>$F>m~fX99q5r29V3Rl>4r3*Mc2#FtoHKdg}A7%CG29KBoie2~KIP0Q@@Gz -Wi@^W~33$Vg2@RL-BQ77znd|A1{{GNf5kb=)_<;bkXr?JuyOFYy(h(hg2(uK6oI2gPy+(*ni;jf#ynM -K2jBH>nIHonS(|i8+d&mT2L5MaT|e66mB?n=xjB2k`6gy2|KouR2;)d)#7(t}HxH18|3lB!DL -6rGYF1jBsLD-wX~;Nf@2gKU$TFn{=Ow^g2Xl+(i`TCslD9)VU)a>Cr>f{FBZ)c-nzY?D;DFDosr33<_8}GlmACBq!Hdaop=IM+oB4)ND&j^o8Pi6S?Wv*N{(TJEe -mfa^TDPYVVRY;{5HL;)7huq4eyf|DM<(7CptEq3?0@r>Xf?8Q5A@1Mz}*B5xaKs62i~PO{%STE&R&jI -`upaym&|7n2U3BqS~Z&Ruy3LSJ}%|s#P2qjAnNP@f03IOYTNFU*(`k)ulHzqDR$#bF23~X8GjJ3sKbl -%n+v1-O#o^SN2P)SYUExJqKIuYnkULHY~3$W9#>}xMV34}UyfWn{>1XnS1dnUOweg=u -&0?;&ukXDNiNQ>x*QRXb1iy`pe}2`)+Dri3s&gVPFgZr*pni=Z)gLsNEA3--n7{znBK#Yw{-G^cXn&x -4|Iixc)`ZX8aCl>?!mfXft{#l-~U9)y?4(2)gv5Z9bFrWtpNb`b!?(8MiiTIm(3Hyc1#ZsJ)4)ig7=NAXHLYZY33{pB&8s -r1i;8+UXAYv%!G~9Wc%E^Z)C1^EsOxI-~rx`OuCAYmZDQb!e&onY7BYNO98@2?Zd3QuicrJ;GIV+mid -dfi=A$)OFPkibA8O%^?f*ZH!id8?IO)79oAw`XPORXj@#+v*Yr{$W6k)#c;hiT%`^HRof*mcS!ezxfG -62eQHqG~hoa$t>_$jna?t4RD5i+On7_n`3)EyR+M5mqtg}$e)c-loh)xEilAIA#c -j347t7`ay9E~MLX#9smFUaA|NaUukZ1WpZv|Y%gPPZf0p`b#h^JX>V>WaCyxe{cq#8^>_ajtPY2hQAgM7ieW -X7ZMnvG3z|!UA!`-^Wu5d2i+3vsuWNhOM$t& -%*s<13z5Oz~=64hA>HinEH#mB@>%xZ8~fM=VcPe8AX=Vp}Pyisww^Y)*jKLS$S;uxOKHYh3ja|FT4>V -lI-3r)(>#B}+7rBX-Ysu;>DQ0EE@8$n6SIy-7s61_*#vyAlsJk5BU -5h@FagHDYJLz~naLBX%wn{J!AZ_q!5)UY3Ybl8xB=bqTOFoKlogEOOWcuOj|1=d?^&$RUu;m?yc3l!Y -91pT7Zd{8X&7^rEO<^YbD}c{&;l`_5TcBCC%`$}$yF?Ohjvu*#&e%Ril6oL+vq*}oiA=g#5H9k0&e3G -jCBj+IbzyPW50EqM$Wjo|xwH5gncTTSN`iHIG05{ufe*)w*t1W3yyPX|AXJcSKL2w{M~gAr4e91aFQU -0%F7dmFz#xtUy?yqmzf0I?If2$)z{LK)8#*KhFLU@*D(7~}ez`0VY)<@MwgH*UC8AOnCMEO}Ofc0FV7 -K_BnoK*frMub2vT6*M-HJR0aF$3(3b_lKLw^>MHUY5*S4^8x9)DfwJ1#GF>VJ->W?a(*1#WyNih=~Xv -7Rq+-3BvMXmZqD9MjsqnsuHR2T3R$g_Y{n+}M#v&3+xNf%X~zN2H+lof>+0+(HjH|6c0RGo;*TfSv=r -=1I?G+qAJK5Z6ci}o<;ThO_1WnpzPvu2Tm!X4b)@MSnO{h^{f^k%?{J>;6^|Z#JUKr*jn6MnPUFjq^I -vO#E(jku0vrr7Qbkx^t7RC+=xO2@Gy;Tnaru5SX77^SEoUGBaw-McjxKvt$k`AEQnl1w&d1YE7$DmB>n=^9_R|c&Tw}!J2+Qo}pliJlnBS@&zz1E5NdWABr| -e2plrk{(YdSPlbX2z*ivm7#PzcAARB!e$2)eogfN;l@*2+T3RE*(apucRs~@SFbeB8#J->Tj->@xv>C -WpB>*8UFqna3py*>G3OE9kQN#it#1)szq*QEItl1VK3~T|pqR?MxyNVv4UI1bsmL&aKvw0XTP{dWJBb -0qC69HSht~&H68MYZ0sWKB)2z(f^S3|=_(9YQN7%>IgkeG;pW{RF{)bP_VRO4;7>OH`^X^mr{B5>u)= -%0niL;N;kEiX7^KpewYC=wGJBJ?5_Dn1C&9~zyS4d{=%1P_LDz0)9cMyN#Mp?f9)$UyJsyF(y4WblU) -go}+5grs{R3NMqP4}#&%4Lv>IH=VgnIKfz>ez^9%4oXjjR64iQLm^nNK6(@4Q02(Xm`FVsLkxjF;y42ekvepIo*=8c&2p*kW6pS@c -F0XF2s~=V4A34<5FZ81^dC97mIzej3x(FBk0fD^TXP@Sv}R0n}iJ4t;$3%0pL03i&Zl8#a8Fb_}_*#+pzpWJx`C0cMAlnrd4w=F&X|O(x-_f -;Ize&?OykV@No34K1e($x$rdejFIiDKT(OS&Oyl2?CF+BIYS%FEw?wKWLIXL*_L_Kka%cq>{T`in}FO -75F((NKx&Y_JX0?r4SQKS+(`v@fcu#7hI=tV6{q@Ht;*qC+f$DFhri9F_KE|T5d!~YRwHKR?8mAC3V< -^!|8XkCRL@fot;6Xcp!Jv3k?x$SO_6}r5e83wt=fxq|v=R^!pl$WdbUR6igw~V22Ey4@8a}DJ7O?)DN -hEc|3NH7__j~JV4keSGlt%_{u=Ym}mjWH3>h^;8F0FLwvRVmKrJT$Q8Lr9F|Oj)f5ix$OB4*K56U=5s -ToWfH*Z@A_d_7AK}ka;1H_z5IWNIjFH%W6MsiaQxo17u%oUin^wp&+3>hlcQJ2fBt&w+@aXUwnw={E3fUed3M1{cJ@GzBm!;e{5xYbAeOeb#M6y_IbfO_TL=x%}L+-(D*LP9S0oAef_fBQAN_G@6eC8$KVj``=-k_(^&8c? -aNx2r#b$soo8|*4Da~l2aObv;w5%4y+xrv9GaP%xZmGsSUJ~x_X?@t{LuDH;5+p~HPjYZ$^(m%=T->8HW;G5E%pi;@1g*j0rX7&%dVu@027%k -)iBj%q^3b`Pglf+P9FKxVKaE6E4t5425m|;yw}rf?OD^Qob5)l -xh!fsrct{$S{JoUGQCaO8;+tftq1dmUJXJkm&4zv1+Axu -orv4~I(IQ_8@a?M+;U>oIw+z>({mVi&bdF(CvI=STr@1#m7gtZmdR&OB89Mv_MsNUo#S+@ZDV@f}lyR -H5%N=oAkW+7YEpI835ucq~l%53`G_!XovP_}}_rhJpHvuxJuEwpZSyD2yKqNE#9B!5p|kELr;|84{co -MQ8ZYTr455g^0{H6OsREkqDYulz>_jk?6te6SUbPjBs3aXGE)i_K>XeUpdx*Vf9dwqcS6dw;8K^Ecf& -V_ss_z@lDejcRV1G6lj{-ALc-p1jVP#p9df*2*p+9RWA;B%k%-xq89Ex|i}?4Q+@R*<-qWE&SlcjL6q -~(0SX+>j*hKD{O?-6M{6Se&(ETt6N!3RiW_yChhF;8d-rpIf&7Vu-FYJlR`1F=Pa -FTqgFB%N#&NOO#Gu?2S(hA*EGjm77FOa0oaelDAX~k511&W~WIrSK!)q$P?m!sZ{5!BcbXyj5>e|J!( -)ZaeGh*x*?Fy?87dcwut({UXazoluEZ+l00e5-5DbUJ`-C2tn_&2mpu4&DGblO2X?djpUFq#QOt6>g? -1A$@RI1r#HA{Z5ZvP+ILW4jBa4*ZhG%U?B9T#JUIQ%?=UBo`#HRt-0U4`4p -ygUOoGK1{CCvuxDM$DBYqhwYNx-Q)zfv#737^kxoDKSLS9%<_lQu8LnT=+~Z0vk8r@KrBgY7XtF%5I68bf!$>&6*S_f`_3ZYsT^ZntRyydsc4L|>kZvd~RoJ&@$ -ikE7T@VWku)Buwm9-w@?|>n_?WRXy$5!%ckQ@p2(@@Q^#U(KE21s2xj{pyCh~S5#W+vb$G^dNQ@=P<% -1-T|HgVs95BNDScJs=R!DMnQA!9bm3?g$Ys#pd)roB=A+V(`oZ8hl0%4s0Psub!E4(+z?RU8R+mwT6shwm=>*Vuke -2eg}m$Jop^qiz03w!_y&@TiZ7X#g>fBReJj6h5|x55AF4!(i|qP)h>@6aWAK2mt$eR#W78W^4=)007! -C000{R003}la4%nJZggdGZeeUMWq4y{aCB*JZgVbhdDU85bKAI*e)q4yG7pieB%Yl)mj`Wmt2~YqyJm -9P#Fj}t*Tn<+z3BpEwf@GRv=R@wi8jQQpws5uD4}Yto+F9| -Ne9_Kfk;<|Mlv_yNP&{CG|x7mKps2ky(=YM0_pq<-|@evofCFt0L7^T;8qbl`^`i64kE#29v97(a_}K -luG@xQKmNWMyIM{__O_af-k0o929oD>@znz5%@5{wKVHITlmTIOFW-+uX(+!fKb4Fyiv7GWi9>aU!+k -z9uLd|r}Pg$m|Et!pMGT@iQ%kL8&%XNCnrfRjS-)+@}jDAHEQ)awoF5Nv??tilz+!6bu-UdrA;O2g{9 -$%btK-YLRB*FD2S|Z#^7d#BpshWNHJ|HHjZF&NEC+fe<9lxhX{Yrg?jH4b%-qg{VX%`kcYJ@giK&|h6 -qRRFRsttoL!$qLRTXC^y|Cn)rYqqBhe~Gg! -|JAj|6W&(5+D&wTB-VlNwj-0!cxLyn=F@Az9ok3#@o$|<5m*L_yW*Z=p! -fR^uvdsnBPX$O7*V8o@Z;%Pa{hCRW8MbKHN?;|n8t&!POWMTno~uyF9$$>x>#3a@7?xFu($9Vs$ukTV -8h9c(8OAs-Rk&{XU(TV{!D~2M};#L$HgzOu-~muo#mC1>DDc-(mlB;T%F^VMqFviX|1Pll+HU5)}+UDWKz-0~m1VbZ5@qDU^QR;^0ds0tn&AqcDs`Xf#{AM`d -HN=+j$Z1uAt|}p3~_ScQc~T5NOfM>gAl5I(A6EFRDqYzz>~}C>rX_~4YNBsCac%VRVf&uO -Kb_fvsp5)Q4=?t?Ki~rDWY~<<~7z_XslQlx3r-nG`hDKd19MASG?|0PWX0VWHN>z=&BNvRdBzu%RM!zcHKn9T>KK~pwa{ -anP8<0>ntgtJoq(vJMw+ttHfJS&LM&O<|Q}&7^`w+fW18t -rtWWYjQGBvm#VT&nADF+S^x3{m7D`~6TT -VY?#IFmd|Rf5CL|hX3xoBGUAV{_db*_lo}>YwNYzlsP58)15F7eQp69peQ`KZw`-75K4c7*8mTG{I}| -9LN!)r4*utE+5ixFZak`O#WV>7GYKRy3AR4oTULK*7G#M23sE8 -sqJYlH;YsX*r|$jm8z<9NfJ(y8^^Jk>*YM5USQ!nmO*mFsF2cp&MKSFcXB&(2@{pEb6_>aPsXFO&QbozQ -u;Uv|h9WzJgM75br=uC!(}wN}RRI!o>)N+#0SOq@}Z5iV-&n8yWeJ?0C>u-F2UFPB*Y4?zlD275|0*5 -8Uzt{1r807$>8rX^caa=~SNQDz0DXSW$+vBBDAhE%DNS_Q@+y|~`U@ -8@5-ZkQtTP(_l_Yse;5NNW^{VsVy(n>!FNuEeq4%T_4^3bm>>a9~qsi4{^DNR5n+I$y?>7W7OzcKsD3 -pe=3WUWrmut^>>m=0G6ZwkxRaEP#7pKtXqzYeLR4T7*#pHOhv-!D1bh->a3X_#h#3+lf76 -)C%<)uAf9f2I9eIDOqTdkEHX?a&u>3k-3u?|Q18um$(R6SWf&k?wqd0p?aVgnm-;RoW?ay>qxGlsWe==QIT+& -q*l1VPlCNY!hi7?1g)Qqw&*^HrtkwrA4BD`bS|OY;5k)8Z7m)#)tX{g}>`7sF{vayYJfP8rs~y#*Rw3 -MN%}I14;4dtxX(1jXB^O&1{wd4dz!l4Z@3nqMR(Hl7j@-I<*BU!~?!&gm@l`TuYLItdezhk2e7wnNT}7C$*)3 -Cu!=Cw?FYQjPAC&sfO)rL$5;JRqCEYp4^n2g<>(fTGC`_({|fXUsL8D$SV|%ESRGSO;9#|qJFc{kZ?p -(YwD<2G+;aN#kR$rJs%|z(hBjoHK8`t_bc9&t^t+iH26?~yXOqDH|yGo-bd&W+}~sihF*D=Q0r&xbpR -*qrP4ndQwOR#+)E8sgk=U#Map`eJ^0_T -{q6HC_j&{dQ}RVfyvWD*zA{RYIp=5Zl+!zYQZ7K(xnDG!`LYm4PuDyhF_>RrnLo;o(aq8S}%S>PMc@9666OwsN*7nUgIbRDoN*B4T=fVkNw47cO27l%3r3$Dl4qZ3tM|I}*Ly6T9W5SJ__(5jbawvh*}ID`; ->}0#;p4SD%okyq_Rpn4#$ -w}V1L9VCdwM+$)t867IA?{Y&GLr9H4BpXu##6lg=?SF)nd_>vEt+O=F^xmRCX%oMcXkoYdL8UXk1o(| -l8@T<5Z#OrbhfVk{;%j%&&2jm-C4PoroiK8e`3eEEUT@gM-~t3N8RKe3SH=$FdA%63R^LHnMb`-Tq(={QHn+EJiR#frEY&o}o(aqVJ(X+(lQ8tM -f!}AfL?WVl)m7VE5(0n!5eo7T4V@mNQF@&)(E`#8q;JAlw9%^#zO@4l$dZ^u*3@+(fgMKmYY_r~r~w9 -sA!P=Y=1HjGun~^I!gY?06?A9NCzETK8ftC;I*#1HL#JJv$P_juU>qg{OV7MntL -wdsaTG*yXBfo#F@`N$g_eQYPa3oHF&1YhUcYh|q=neMuib*2~$ZOmxr1OPS8GW!mS`=$EKS-?QumUsHJHVN<(I9- -96BfB6RO^w521kd#WMAfbC4l4$$N4=-O>>i)!xFPg=iXegPV?;(97|G4|uYc&|V$WTHPg@kXg}VfG?4 -5y_uX>tEDwT#k)~H>W$c{@&ePqB7r}r=Tw7` -(E01=4Jl|H3#}1x?x)(x4WZP>QyJipm2BIY{?jPgO}F7+ZMR<0lqTS4syqM?V#|!f6QC#&)Ij#xA*eO -xVMGaa?Yp0^rioD>F&KQ+lvqFD~rM0g`Z=)y}nGcbc3rueJ6IvZjRkjn|~RROFecopMsNKs%rYP=`^a -ULoN^9F&eBB`_?nhBS#xHs?P_O;#ji+e0mJ2K1C=015ir?1QY-O00;p4c~(;=0Hdeq0000~0RR9M000 -1RX>c!JX>N37a&BR4FKuCIZZ2?nJ&?g_!!Qhn?|urA(+Zt^8Egw|$DPJ@*{zh~CQ2f3Y#}KddHcC3tq -@2^;@8JNNSVP_raS`8T*Tm$)b{YrMkUAOoa=FbIZ}RzGHQF@94?0kH8~#P4Zcdo9X!4RWosSOXqx6{B -88ePs3^bK!%zfD>Y*!HOG402h)uz!X!XeoYLpV35d;Sm%v~khP8MJf -%P)h>@6aWAK2mt$eR#Q9GlIB1O001u>000^Q003}la4%nJZggdGZeeUMaCvZYZ)#;@bS`jt)mh(<+qe -;a_g}#(54HhYNP!j&4ETyWT#7Cbw814n9~KLPmMEK9nN&$?H$t%gduN8EMEXav=*!`Z0Er~daAr93%{ -PoZb=o+l?W{5S#46pkqH9#n-aq)gwQE($a|k_R@%xP;T7+PCfBf*1t`kRxEi)cazEq0~VCxYbCnOi#ufbCrf72{SVo7PL~DQ5O8=0+reU<)XVYjF@*n;?o -s3+tBt1b(ArPr{F3WU>Lh%pP^$))+L}wG{_m4FF^{><78GVj5nXX9?dq^Eei@EHVo+@bRFM#cY+Wj-J -Lt4*9;it#Y!JiiB~9i1e@o$-)~*Fbp!feG!?yBj@&*$V{jwX|y8rOBaphPL4w^=@EEQ)*I+OTZE@Iu3 -WAz_A>&Z@=2hMBn_C++D+-Vj73C$L&i>(4M-B9Nm|U9MV_n6QH1j9a(T?jkO6SkO1rZ?5P0KTT0bR-; -dtN|sGpyBQ+$j0`@(81ENSCiC%8e+_n0yt2X};e2zzc=ai&5Ei3!H$u|Vda1s?O{nL{7wRb3WI@S?Sj;>pUiGeb+4!co)rbTtlg}vjmE;C@e1z!YvB=w)Wo -&FCtniHn)Tc)ac_ILV*TYgnq^{uegP%o_g!3Ktr*FrT-D360k)kfVvkbsdD^w4xX6EyOM;(Osu4EQgc-2kr;f0}fVIZ`kuuDB8peNsIaLBx_jxBs -rKER3xPdOO53F7ErPuwliP~4w|>S6in@Q3u -iqrQ!Yhz#dh8=$OA_YSL!pcJYiK?^nrw+f;d#vc&FnK6dh(4Fyv9`=h0bDjSd>mHh0`xz%BUdZX{h6QqplZhZ$rtraGpNnF()eE4EvMkrvzFKu-%T5zME<2kyQ%~2+R?u9uPv% -IFP7uhS$>1G=c0*tXzB*RqwaPA_>eBvrM%3Cbfn=!7LLXcf^W;9$&6<-v`5vFC8H5Uy -~4Of{HadEa6h?FTwbsx7|PPqFL1fpm9)p74vZhqA&-qUsMQG?hT*dT0wnWB_s2L-Y=l1qkT=aLiEQyx -Puj;k~lG2?mV=L@20OM(hIjiB>>xMs}q`EZ+rC#J;ry6&+?NWWm(Kif{fl2MF~|WcB8Z-MrGEXGj`SL -FFHI+%Vwi+-mjW7c8S|pD08_guBc$N4yJ5A2&#bQOh2IMouj>qSlnSByqF*zp97l;fX!;qThx}$?Gs?G|FN+^Xlf(I>S1UAJ?P)TRH4cIuKnfMl+jk -_>Xp;%Rc}7d7_&77qJ=EK!s@fjr@mKI1k8wzJTIo5~WTG~n -G9_vjBPJ>#>Z$t&nPmV-nN^uB*(aToK)mq>n-nnw9j-n?X9n7rUlww@E)^CFRs-!!^h7oT9>sgXfp95*bX`4Z8XXBEODC~Q*hd$2)Hz3>uO3(MQm2N;YVS!Rt;s -jj$Nx(_sZIL$eAdz@Sdx`uVIWjaWPqO7E1Qe#_)K3Y3Mn=>40jq1-&}<6n%!lQ4UQ><#nEZ}He#`BT* -!C>6Rhri>|2t&tn5(H0Nb@Q~lI84wP)h>@6aWAK2mt$eR#T2=^A6+)008+I001Na003}la4%nJZggdG -ZeeUMb7gF1UvG7EWMOn=WM5-wWn*hDaCwzjZI9cy5&rI9!75m+TzG{dIJAX(E}AAA+~IQ9AiHgGy#_k -2M6H>XM3to0YXtf4eTEceQS#ok{$N`qXE?9V%t$t!(w3Fn3M(72lKy$m&Ayg*;qjAEZTMfS`+M2mhey -@fj%zbgDwB2G?!%)wnpLG$!|bsG6&sdcwZ{#6BMZCoyPfQ^{86-}(jYG$I9-uF3T>on1ChIjapV8w!| -s%WY^~5OuQS<};wdXsU5mmh9XPy`?ZfM^_&lALK;#uYj>PZ%>RY#Xj<^w)!;m}>+zXqRqT+pRbJ0FZt -=dMk_AIF?MQt)8NHi#wcUn{?FuDoL@3AVhXbWM^acPA;DE$C7W@@+hvb*ss=ZJbMadRbW0bg0s1S(#B -;swObZPVqny(6c0JMH&=&N=nd1Nt8waizKt|R;3!(tYmt{yuU0qL@7})t=KA$_`I}d_*ZJG;Z`qC -|7e8KIG*=hp?Zr3Si|@A=H~&gjs})5Y+^`Fwm%*^_+*+FFEpJ4guW<~fW;xm1SVS{P>^9Q}aojRv^_p -G%nSQq`h7VTryQ38beDObnQQ?Dh?KX)H>q8b~X3t-~{3;zu*4bV>mGWK~I}m7Ld)+!ZNK(|?81h>6nk -;rh^7vbwjIfckd7i@C6^zPZRx-*-$RAWYoTm>R%bZSImoh)$*oHFbBSifC<;*#!JGlu5h}UX7^Mc*#B -eM#o^`GBdZNdh~5QKvN`K6#~-F%M_l43tB>2oB?k#f -R43Z>jEEcN91KNwNpGvGKPGEJlJU@zU92lqBoNHVZs|xBOC_EP(OH)M?dDo*1zrEa>s}21zY|CIZ@s+ -f1-pLgYFS8IADQVpU*K3+ME|uw~hCc{KNCxY2OA-MLouv`ru -Cg5G*Uf=54e0`kQ_#oqtk=IL(GThnM7V@4JiL~%B;<0svSfG2`#qj|L%ID^zj@ysEl+1= -`%o&_2p4o8?>nrr~0$u^l9(*fb4e4ljzVPF-?UAS$VrC;SEdt%tYk5G*Y -^+y0;HCG>W!UoQ813X8V<$zN9w!`Ia7nIMbZljZsn6Ho8^XUL((dvVB4Ha?-NuHFg>93T$jJ1s54Pju -z7rk$4A#?9JoOZ2Y+;uC0c?Uv6pra_ooVer&R4ZCrRRv!$2$HAPEPF0~ddANEt$O2QdjmPS|eecC#R?9t@1loO~Ga#phx=^tIbbN4uc`gev5AoJ2 -BLcFnAU1V1o%781aYr3*?tQQzu~|4ug2Ix|;V{HI6sADOCr#2NC0qCiJ&L>P;QjJeK^bTi0Cm|95ZVc -9xv#0Tz>$O#zC_PmDW7)>L-To4ii%l>|I{ULw-3Sg4I`St_B95|`U7w@5{5<4}lz}KeGva_!C#ojta< -^S(D=MwL^p=`z6=0)F|j9eAwwB3v%3~>J71plJGGy;@R2;$~B9UI8q;O=Z(nLyJNslj8>RA444T6IWI -Snz_q5tvsRiLjKey9g))yQ2-;JCiS3<7t5)Z@L3Rqv)f1iv6<_G2)y!os}5=>0G^8^lKB2KTYNNxM9o -M^b5T-)JY@5T}@J!w4M1dvG_RoM?OL7P~8 -B$&%Jfvx}7>Gk^uP`car|TcfvZDjR$+;M*OJ;IZAG%l%UT5?bD@2HtRIz7GQo6ax?)>ClnM;)Y}HZ!+lMSW5>e;O^s2n9Krh -J4lNpV`xD8c*Mbp`3018M;92M^q{&t7Lm+ndw5)wSyDB^Hj^X|%oIbGEOXlw}_$&lN -8k(dM}4l$%eZ=S}cy)8;>2q-kd}StL^?F^`ji|89w&*{i;GRy2|# -_=MSyk~@>l7+hYAGY7l63UdU6xeUh5lFf5Tuz3~|mL#y*W)iRTJeG;xsX|)umHq4pvicZkVZni;+^5npMd4>c3D+0|XQR000O8`*~JVvbSILlnej>*DnA79smFUaA|NaUuk -Z1WpZv|Y%h0cWo2w%Vs&Y3WMy(LaCzMtYmeJD^1FWpqacWcYIRM~$DIKeNYdmGplymI?R{7*1g*uj)s --bxly>75{qHw3d{dIWz1{;j1Dn_+XE-yQHyK6I+kU&}V(5#Z?b!|dU5`~=R?Uvx?>VmpyXo5ld(()as -Oxw9m$B;kfj5K5R#6nKR@I?v`+?Q%ZU;d6XDhO<820*S&-FL4ABU=55z^t<;XZ2Sd2>wJOW35iu6fGd -47``$zOBTNvbWt(wM|i{?8DgAd?itIRhQ*=yeZorHr(D8NJNHP2#t4JG;LFDi@N%i=S^_{jNZ^4?*(8 -!g-#lFNTsVeV -*SQ^D1_iC{kPi*`0e*i2;@g7DniakT8++>n&>7`Jo5R=~z}?oYgs-a=ik|muqup8t=JG9##X0$qJWp1 -uobPsk7a1i04YB+|AVGlxse{d;kZr!%Gg4NY6XGOy_u53lX>#pdF0|Ue#EX2^lQT2jn> -{YhQARe_BpJmVVX7qm#de=8ZMeLpcXlntnEEY+k*%1471Y0HDjTP`O>lq_VX|my58TOjc%Te&w+uQt_ -jwdhV`K;Oeaiy!Ngx*PdwRk`f)BTyGlwEUjFKFx_i9&|pOmkk{ApM|Z4aRF&BKN@0V;~-)lypwG%7ke -+k78g2Xy}3Wygo7uE)2Mm>Fc5v+}%n$3%e0aIASux_>o4Fk-B&jB#8I7RY&3eiAY&Fay;sy?s-ujfo+ -p-WKlYSMM^02c{9m8^_u)SDj%~d1JwfCmz$-mX$ShLoMDLaLq95vkJ)QVS@xA+T?@iX<#&+g7JP^xwR -bSV#NkjhC2O1ds4&EqrVSCBWR~1^AU;Jq_FMAW>(l(4GfuqCN5NHOo=|GVrZ}6kp%{=P7Iaa2tmeoBK{AG!s-sDSy5d5q667>Uv0PRSp;Ap1 ->YOqG57WRICxPJ0wt1=&1g4S(&lCZZN^*omX+rRMDf}qNqe`q#DfnjHQKNWj1QPjR0JR0hq*Ru(r?ma -k$^L&%K;Eg7=XD)#4wNa$DVsRsiUSv_u=~y&fK$iBH@J5?t7aSca$g)pKERS>i9RKX2U3WWe=01@^5j -U^XO2X@z}?*7N*WfWkCSHS_oh{3rt(I^~FUl$C@%$fr)7HWgCHh -W>$37P3xvo1b33m&e_XL&m;or4yyX(JmUBnzHFZ1|(v$z#{c9O1e;)VHT7B%)o)1Ii$IoO;Zj9^S%ho -EYO)>gEN5&8S+f{g_*G)AxjcKzkI3#$g) -q(%5x@M#n!6l-g@S}v%D@xra30ilP8T(2MgJfY4KX^a28u$Y$ -d+#31H@V#g=&FOISUL;+GMd#0#F&ENHTE`D@mPV)#1xf*=f1mFG=h3L6dYO=Af5t<_g+c3P -h)mx{uhJIk_-1R!jyoLG8RCC9aK$V2MrW$G(m8faU}CYijkcV}m`Yo*p^op+%43DOHUm9TPC#~VNFjw -%Vxe?|xB*azp#D-=5+m*;NJ+MX|SDvR(mo!^#$iF269|Bw5L|B6>2)$(pW;@hezH_9|g!1|)Z%Gt@EQ -C@DTwba84z)Y|(I={41BAQfnYP&!T-t>$%Oy=~XkMhfN9IKPV0sRZ;V -2?_O2^H0-B=>7v_PJ6%l<2jnStIv6&aof$jOq+&Qj}11ad70bsCH{T)u?d3TWVvU=8$WVR7N3=?y7< -74A5K*t-LEgKq*ZBss#Nx%N-hR{JR0cdwrXNG4X~(v;7J&_bYKTCWe}Tpgxb6YSR{nbieBnuhxX20ms -%pHASZ7C@e{r5d4c7kY!}Huzc#t-20WsG+I91!gzbyZzW8!8N%-{Vv}IZ$&Cg3&oPwpU>7IWqs~s>LC -+9p*$baJbWNbjq7i>gqd+g|uHidYC~l-&$Kt>4MIkeHQ~(xBO_ -vyZKpip$~9?%1wD%_-<@a37EMsq;1u4I|;{3k0W@W6I+5)w!N28&*{5|qBSk|P-`buAjbkRj$V`QNdl -2%Ren|d61C1|ZnNBf?qP0b*5*m!?4uZ&WG}VNaq{2T4j?^o-s}X0zkGQ)SM}G}e1wXB=EjZQd5Cel<- -Al~ctTbWOTkqXs_kNwN^!wskFAjOr3w1l|eMB9c+kG -^`O0exUDlDNRx&XgGFoxD~k~*KDZ3vXAWC%YAvyBwHwVgf7KVQe)52xE0SDV1ahpk3 -s0L?9(5I*3^W+|d28+zv&J5L8=yGJq?x&g4o?mjNid7QaAG=mes;F8O{x>}I6(Zj6hI;b`1uIm9+kgv -2Ju(DM!Z8&45Hu#HLstrHb{XtK6P6Bp%`75qzL#_M8*F;DV8i%Hd_py&525q5@ALjOBK3ynX? -(07QWVA;PoL}se%Nhio$*Jje*#cT0|XQR000O8`*~JV&UflGXaE2Jga7~l9RL6TaA|NaUukZ1WpZv|Y -%gPMX)j-2X>MtBUtcb8c_oZ74g(c!JX>N37a&BR4FJo+JFJX0bZ)0z5aBO9CX>V>WaCx0rO^@ -3)5WV|Xu*zX2#H%(rx^;n|U9>^d?jlLigJDQqnYP);qDoS`_P_58KO|C;)1X{d4onw2wx?aK3)VbwDVh^&^kT7q(GX^qX5{uq@`q^ -HYC+%uNbedgFXTahcCr^Tz?_IZL01TvK~(qXEJEyIR^@mesN@CtSu{7=OESXuq(fAWRN=T1oviR!sX7 -*c`aQ2%ZZv>E^6>Vdc=PAS`{Jkj-yh!HeY{IZBQ!(>oNeyBvPR=0neJp`UaMyz0nBxZAl%LC|iwv3hRFE31~BE7ofAw%M`sos>e(iDC@nh -zp$cOw6Qq?*Vaiu7;pYqe!u<++o0q&356AV~`vvDjIrXt3I?hO3N)sVmz3Yc>cyGK;8N{xBzG5rvl4{ -`I((SHHt(_9>LvD6Fa>dJ{rb~xH7>o1g=xivWnB1R8)amH%C^tH=$MtK#+@+U)f*{Cxhb2$e{~_#~Fg ->Rd#h-JQ-4p3b^YMkk}4}l|fV;#k0wwu5r`7E|}`-U4bf!L3C}Lbup8}sMPA2>tmYSCfO((9X<`&M20 -80X|jyR`u&56ZG_64IWZ!TEMVUi#!0hiZCzn}J2z@1{n3KZ<=B3F5W&2njc7Q4YaE@dL40u?A^?WuNc -w~61x`u+*qQTB%^?+{sW0n~vSZmq8$d9#X?Oy4HI>2xnGy5!<;cR5&lGrkUf|yP$Rs0FqNt0X-jyZU! -!K}X^@(U7(g9h=xbDf7;~Qi4nPvF25;+nVPu&i4V+iOW`PQSGEFA@HAcYozM=`hLmJM_3strmobv5=B -=5tJ4^3A$03-S+tK0Lg?`|zRo_3qunJOr?|faowc45%n-(Hsi+r^rh?0NEh58JP#i--EPm8Mv1^g-av -dPU;NuIt?;tvf=%50U?1_s`0b>|4*>l!&ATc*SeWJX^dSt?0C~NJ6q&d6GEm>NfNS&` -wI8e%TEHq!HwJY`1G{k4dgdBFGV1G(RLJL8mF52c&G>v+?4dUnH4{f78&wB4sL-JGs0+!44ZEK;D9W8 -}b}nD^P_;{e~4O9r1oOJTHFUTdii%M>$}Mgd^=Sx(2|q5ll!VR?4lKdh3J;~2>*EhJ|KsInXxS(b252 -8zn^2NeFXESLBn#NjUi)Zqo4gtw6VN7~|;$Mtx4SWJ3wn1M?9Lt2hC#FP;R0Lm@NcQyv_#e^3s+m1QG -b_QO0HC9=`A+wsS-2zB~5v|(fBsc4ufi@L+KJw}aMdUvnH;j8wGpvI8UTw(`IiP*Bdc8Hp!tHG`Wre| -@m#}1=#1I-T?Uz_|SgqoU25SXVbh`r4Ta33Bm<>_hu!<=~_f!@|)qE8bV;As$yvr2g|4I1xga!4>^?#CnuOmJqdrn_QLU3VU=W -%?#f`J_AIHDQhl#Rppv&lRL!*v&5mD|D1W&=ARN&Yu*v|nH7KTCH -=T^bW`%v7xrAZTcl8_S@wW@p{YKqS-v<8z2MYCyaDCQ5O8q%M$Xn -AKakhB!gMup>->6qof)h(ad+xiBIbsW6V_kG36*FxZ@QtOFv?BHYKbhC`NHbkF}uFupbYUF?)kNk4vBn)U80 -jZism=>o_Tvt?QPVvammNgoydroFyw{!^f>8*Oh3tpUYJ0Et#u!#Iq5UO*LMhSPA*C62!@?thyQgXM& -&6OE83{4$gJId57YLKZ3WDcT&iKWQ)E=o4RUCAB6)iUzkE?1QIO0MvvuL;Lz<23@6aWAK2mt$eR#SE#e -+IY-003?t001EX003}la4%nJZggdGZeeUMV{B_f5Liacd -WCXAUfdet%$mBinou2e2f6h)(v+Lnc`*t*V>swgt!7qVirQTb-@87D=)R_$t0R;AEJw%;wf*|)Ei7Kv -PmQ)Epgs@g!VR2olwJYsmR@9%H#r}p*k{`&L$_Tdx1e3;){@vHeg@9aJ-ep=lAvgj^-p5HGY-rrm= -KJ&%xl7GB?SX@m|^oO{GeU3``Krz*vfxp7;LKQpqbOYT -p}NqSpl5>IR+D_-hiGi5pXK2!gZR;JU~P@&|hYGZ4OZ`cOLEA)Q3FXg@5;8f^66UJ})r9Vz}lNBqGaa -zVIhcCs3qSi0y-=+AhbXAK?=Fgs{u5);8!|ObinjiK@BhKMT&e@DTMor{IUq&OcBo5K6%`~vqja?gao -{O#Id@>r1Y&!E1Hx3-q$%T!X+CYa32C+dBd5+f926mTF=ifDK$e$t31zREM0pz2|kj5-nD%XrdLsfA; -HZohoU*KV5O2Oks0spU#Ax|BWq*Tsm3kF?9(|CUHx6GCJ&mM@%e$Pk;Iw4MXfTZ~g5i`OE@ -&XWT-z7MWQb`#SCIQe4i~(XT#6$CKaKuO!TGQ6F&a)7I?Oq&A-P(L|c^CjfAljc3+QeGuX{qhI}HWWP -i!hh?SbJS`H=`%vByGyihf9$I=B))#F}c^n*}FEA^)@j&3rhn`{Psi?D*6k|<=VO7DZCuN#%wn3pSTU -0FC1>SfX#%>)SC!$o9%?M<8Cy$Bxa-)=y^$c)*4UHY-4@7s|35RK)+oY?&dk9=TNn|4=LXmmlTL`RsH -izOFZ^c*H_>Nn`{ov|AHgp|S-$`8wJMIZ=#}qF;c3!NZHxGcupD8-P-DQFc9LW-yh5|#N4*B3&?TINJ83xd40`1EGbTW!)Y8bjI&g!LB=R|l_#_O@wjMW_Qx@d~w}zfQQA0bQY>p*i2|#7N6y7dhn5>BYHd3^wzCMNo@M3QZ4m!kC+At?O*o|9c1DJ#^+N` -@qlWnF$fp3IoTYVa9pD*He+Dj(B{8ZmSIXsDg?08j)n$E{LsYiZXWUhqqj-5+4*mIq29!fpD17BruB!D>i`XpHJW*=-Y* -^W%=tun?=2^6T-CY0Jw5VLuZIYn&&b145T1?r#sQoZyWk}T#x`!5TEd@FnX}HX-;f_0(*McHCKZkzj4Ut)o4g3oHoGuo~JtE& -TX;bAiV5$hIJCStoZ9wd-YT42$zi=x>YKS;#+15cLd??Im0)Qw!?c#!eS4zI -XcEMfG1%g8t7E6>diR1_Lr-W+TE#8(Tt|nxU@7|>bg6*2lP30`Fl#ka7I0GSv!1!x3D3=;q~zFdF>Xs -`%M1Swtv;o-5c?5(jxlKI5GYdYMi?L4(oEpF89;`W1!ZJq_);kU6nTXYDtqOm9Q;FJB%lEWp;v=snAV -nN}s}(FDpbRpHm2#IEY+oBm$U+RJ&kzs0b@C&w*pbQG`NRPxuu9re}q<--qQYtY -mF=PR}%+3bD9@_+WuPjbI1gidKH)w(VN374Lu5W7XXcJp>Z*)z*%UfA0ZsmI8|L@!vSz0IWL ->o$z(SHF@O9KQH000080Q-4XQ{G90L}?xX0OxK103QGV0B~t=FJEbHbY*gGVQepBY-ulJZDen7bZKvH -b1ras-97zt+s1Og>#w-N(;@jD3EOegbU0C_isEN`n#3AO$(xR<;RC@Vi5LX%08lcY>;Jv`bzdMT%XBj -BR5O-H;BN13cW=LLZyiO^NwZFBy-;Pam({TlJ@+Z2zRPqG^+&&5~BDcC9xjtGt{idA^I)Tj*Bnq$vwE*IBWG_epC3-DmAuB`JP69VAtyi^ -V{tNdf=rdZz$tt54>-z1O->RZ=&)iB*+V@>#8Pq3a~K@Y?$}0p53>eDEOe8tiufb~ES@}3h%J7N>q^Vb20+MQ2)EXo@(wT!>ut&n -V77#b!Q>D{YPoH@W|Frzw4+X;`I?}`SR*&_WI?I7i0C7#x{RnAy1eu7uc;Ht6!6Rt7R-R69a9b7qE -VLx2q5*^5(1nLYxC?lX(t&^8+l@08a9;y50h}op48>Z9KaJHYn@3O44`93gE=sHgih?_9%-iP4es?dR -8QxpsOK$oaR|fi>*z+^RifGD}Ox)`_<*D)=e{;Lo?I;`?Jg0^?zQxyz1+v-=|u*#*Vhx0Pnr5>#}#8y -iAjz!!K7aFK6dx=dWMRet2^^2G(w8O`XoF8us4%J`kI7q4OqQmh~oSXTZg6UFHVdU+C#MKYXBfRxI>; -)9GS&@#cZHZBpZ`s$IK=JtTdu>EQ)E>+4Iji`PlB)^@E|*Oza9el>f2cJ;a+pWJDaHL2+W`=c%Az}hD -#Cm@Yfon(!k@phSM#PX2IJ?*e8H{K`CWs0$}{fh@4De+jBcJH`{Kn1gL(}jv -2e(sCaRqOrqh-h(0EEsCJMy(b_1fdB>}#G39u>|pgb6m&>x5xAEAM+mN}kW=+bmzqSm6%6**8; ->)??{l9~;Sg!|!ilg}4t+_IJb&S%V^5-LJJq0lZX|t|tx5tct50NMcW0f>EL1jqHXg;3?U-si3Y5wu0 -5zrP@{t)W&|l&Wy3Q*MZ)SaXMgqsAgLM{{k}rZjx=@Vv`Q8n{dx$%WwcRa@@M{CwzTs!0_6T1|af~^u -xl*5{p*c1rC(Z5jrGoTMq-sV+8^UM;nXfvESLuPM6e)_;%FRnf_Le=T;J*Z^egvf6l^3|TRQ`FPX?IVhkN^nwck{s5(3PTfwsS8{(=@5P7bB -PLW4frWChtmFy6LlQPt&1i}((<4xv{sK6Cr@Y#KAozpgc)7FyP{OU$;xc}ynU2>G6kav;DW+#_AA%wT -WBs*$%W9&Z9%n<=q*07e`rPEff0Z3G2P|LbI)9e4xQK(5iwCgaJqVdj@@L`=Gyl|{ZoLiv`^5SB3C~F-+_hQe9jevf05PGV8~Aqj;3`EMiv6wl0Cii#9plc+sbmI5rf+z2dAsEIszp^9? -5!_C2C*<*dT0cht$Ix3%ykKM1Q9-_ -dHxTWSV0H<07jV=#c-2YA5&9+N&{t{j(ZIxB@g8{JBOW+h+z&=lOrUybQaVr*T?VyWxK8n-CnDk%y7d -+Z!e&y*lKAg+6gVCr>PkJPDqktaV<^rZ%*26UVU$EFfSBelTwe6vsS8m%TTTuKCTs?4Ln(Z8%4iLNAmw~hwb+SNvEiYFq4(aI)%B7}VfY&H;Xhe9)ni96XUVH&+42 -S?b0o>mKL6_Tt;))shp9Z=@=-_Ce0;eEAahKI)L2QJ!mUtk+t|irfW(VM*1~&^Cu4wmPF@QTzCzA_q1p_5ED;7p38rC -=QJ7=+9ZUi09jwWKJeB~_6?n-Q(^Lco1gBeH-)3OFKRb{at$y%zvo?ZU@=I2*q_44xa?InXczkGB3=K -SnOBY}N5Nfy>STnvx@#(LY;f#`i*YYP$n={|16(IJ`MrZk3^ld&MfwzZ{&ZOEVEquPr%mm -8Zl;qG$rABPc@)dR5~Z`-GBQNxh21EPP6Kp=%@`fayiB?Xc^+Wo3f*lWWK*W^G%p8$sQrb)a3HYlQ -pqs{&DFHcjM;tYMTi4ePuw^wiJlnsalfV$Z#*8)Yp}O>4A2R~$%IFk@CaVulb0Nf@4fAj&c1}ci8#27 -zwYtZ9DgOztq-4(b>lrVyPBnq2iKBhZz8g}1%>>Ym!0%19QKB%<%Z4ZK5NtUSPiogh1cNI{V1#O?|cM -aD4typlhIuE81^FnS=Wzc!F@4~oddS=9_-2EKB3)JXhvFJy!>gRixiv<(Cudm!D3_t-Lm->eYV(N{2S -7IcVO}_#;OEKQo~1zg*1&_)c@Q7hm~A1AGEW7 -2`^T0v)u^wsn4#1k9Z1BR~@X72mR`T -13l^ur4Q^$#F62^=o%AeL}73O3|S|&uF56@6M&=2d6pD%gnRMwLVf-0h1Dt~fN5%er?ZPUBMKyPa2jm -C8k7^h$&-wn8v{U;ag%IxNDz_IZJo?4sva$;N&G_BJ!&}ocR)9NLeY`pK_S{FahW_XaS)_WLk6)>g$C -7pxY`gqvMU0F3&O`fH^u6Tsbn*r1Znk_f*j4Eex7tCQxBdW?f6pfee2OZ{Cw;24XK9^HEKwqle{e7(b -;6HCSZ~mi%Fgp?;1v@#JK{Uj%{Yt<}(;&=@ByW$Jyf;2mV;5u#6Qlq+nvH65h%+D_Go#g<7|poP^yNX -)bAYWMVTmLXJu5bPX;o?CC8Sg}@bb(4&TURuHfLL7S3}m*CO;Iv)|_rmMfqwxwx64{S7j`jlpe34#M) -IJc*R_#@2`DMwSZ(~)iN$pM%a=8pM&e>4mF_52ETC}x&0wdDkik_o_M&@7;bGa_ -l7+-W?+KtWs6+0N_h>x-){%DgkV&%jt3ZWjk+4|^W`FWxVnL+1;ltZ)L}Gqa$>2=I4ez#64saiVW{1UiTo82>9PY8{Q30#@7#R3mm15`?J42NudG -MDL6bjW89p??7{gXP4bWO_u_=m;C2)WvDr3Ajtu1GH>v&IqJkFjpR&8M&g~M<4CFx(nv_g-Hymlb*vFq_N3t>Lld#a8;N0!3O2z3_ri}*lo~Fv!v(-DwWe-1oZMgZ;v3cDVDizw -n(j+exK%BSc?-d1i^_PGi+=Z*Yggoo5UL`dp_=RDsDibXnchpZDaCBEuRn}V|A8 -df=h!JAo(kpy7i8wd%3AFTIKs`fVWMuQ%g((fF>e`Il$U_mR8oVl~ -s}hwdtBy{to{m`Ps{v`(rq|cQ5}gJS|C=|JQiBGpz@B+A^)j(~sw0;u;eW45CEX8chz_U13%#->y5zo -gSD^GpbD|KJpAux`)Sr2IgKoKWyFW5bOt}yP^NL7=S|Vk!*OETgyoP)(rDqtbTA88@wnqfpJNd9GH6Re|IY9ZCs6*@-7mzONw>G*O`~%L%%|j(Yl57KN95 -T%K<^7Y@_O-2MEfFo5q}fEh{#HY4lRy@*hZJ%Wb3n7o2oPQ)r1=i1@H_{OvZdEYlfGp#z=73SAVmQ{s -x?FX#hKPf=t8rI2Ja=tEEne%_O{%ws*4yr4nPv0_Cf!yyaz~S>pn{2}gDKOo`&5B6BPN5!*vzXaB+q? -kc>n!tMDUSrkiZDNF|3h9R{XsqfT_y-P7eR1Xm-w++Un+I6k9r3i!eu0}*ItJwp4da9mxR$8cgO&ujB -m?nI^1n9~H{U<Gj1q>yLonNU&t`*U&8F4hbhozwD!0##0zPFDJa`fFUI+Uf;(Y|~^-P6HQ^k4Ml+MR3qc -S;QQ

dbPd3PYyX07AGvS`hkw%MWAP9FHbZJ8U?NrV}*BO=vurX56-OQbyUZwe&UJ@9U%M+MNNV3DBsaj`2hU1R<{GMf@y&HYN>c9(;ojoYn&m84F5`8d``ZoOj9SAeT*PyBfV>S3~hrd -(&>?e8x9tT-9h}9dp2WwbD&hbd~NxUt{-*+6|nTA-{u_Ci^Q0)fDR6wv)AD;XqSrwqY?X9xV48)s!#} -N^F29!fcU3$4Te~=X}l*8*v-@-3Pm|eA-{mb(~_gwQ+bnuanK-*;Hur -&}{O$&Y{2W9u)-_$KJUUelAx!-9BRUT^a696!Q&}byp${BXNfIogNc^Z2&v=qHK`-T7Jj3ymW4zF@oy -kF;;S2qRi@HoP@n1Yu=Qf~qfpg_+GzLxPaOXtKy^M~dydog+TXXEp=H{p1_;X?Jn&$zWO&OFdO2IBNz -I+D`jE*VcFk!sU3IhqGl&{oceh@g?AU0D*y{G}qz@X%NiUr!yz{FIq!9@n{8w;S;PDu<#Kd|6I#5h2m -t-v_Mdx5Ci=9tnbQ2Oq>jX6ki@#bRo^IOMOr?`piwQwG~*Q*j2owJN62Eq0scLBwNO3LDqJfH~~(s<- -zp#;7){@D6JzK@4LdoDCusqnDpM)RZHE*NYGAV&5E|69OeCKbmT-BK{Ciy^W8d6^W^w^JTj@{`xf3iz -2CayV*d?7$`BhD-4qo+#W}_es5II*t+-hy(RU?!+OGB#&bO^#(RCE%3esc -v!gYKG`*NLX)+&^**;-Ns$>Qr-PLt`S=uv*~C2#wL`X4{@`p!dVKsfzekDj$9l%T+zs;p_)Nf((Om+< -`UOYN9y>>!2e#Nl=ydHDz&QRg5Zyj9=LP0;!Ut{gt$9#MIcmh*IMSgcSSF{c5hJ&6jh*2>wag$z6$#X -dDufC5hHP9Mh%YH0?Ihy?GF98LaDw%8#^_xL3#O1@wC{Vk&q89CfWULC855Pjw(BWfVw1?f$JPOny#N -R(EXo;w{%Pw09tfig7Cx*AfdShBJ -`(vA|dS}QP_QRiJW8YwZA=d7p_jr%lJVG4iwouu7JH=^x_i&0SXP=O+9y)xf>VP~}0d`_n6d7W1_YE< -Y6{NkTpUfp13~_Z4V?%qnUM1-cHr6`1%K*yAX`0VlbjW&-TK6N28rSzh%Mf&5>#S;<)2G{}eo9HfPbGhIo)sAGNB -*)g#V#bj2qTqJwk;V*Rc3)9>@a2`j5x0FsPmwEU{Z%@{2fGGJic@c!-^a=2S~`u9wCTHe8M( -&SArdZm-Zvu9M>}VCwyaz6im<1GqU`slvR@6RGMWT}0@3{I$1&l3X#c3VsVrk#s&9(R4k4mAKq$iPiT -z1Ie5*MR*~YvQO|Q(sfoYGO2Y)3G; -X{mh0bhAx}CeA`30SIIlel6`bJ}Oe`V=Hu+tW(ek+tsMK==8MxtHKmyNQdF=B*Ho9Kv85Yj|gEB8(@t -Lg{tl&_wr? -S6~0@v{T73nWqa4`^Bb`2#sAd)8?))Zc9oS3d*TJZfP<-S_p!wfgd*wF)OSTRrH6CZW_Id6rP+B%VEY -GSJ??BGYWcim(fS)7!-r-(;-v$=5`b_7Y>=wxKXh2aWV#bMkY-D8rLtuM!fil_XA+_Bgb#47@XY5x^x -%U3`27HVu3T8A>Yqt3Tq`&eqDi{9Xj`Tp;r$j*+B{n5`@pRgq=9(B)wq$CtsLRX@?!hPH`7Xj0~^T@? -}{CBu&>8UbQE(G$*5fUAfIzy=3Qj+F@Mc)FrYi3ocV6PWJMVb!@l-U4Wjj23;@a9xvyGjAjShdr_1== -Uq-z$tpo4aC|^8RO?D7tOd=brdVBv(WK0XcPe92*rP#~jTTVn79`FFUNeS`moA7!K}!WPk3K96>SWNN -=mS-x9xz}prC6-B;A{jg^XeD#-9o-S&Plz-o-l``!^3wwgUj|@y4J2gtdbudnLD)xlF=S%QwvdH-XXSEv^6 -x|*)VRc*&3o^iGT~1e{j&FWx^F~K#Y4XG?>b%gs7#YJ!hy_Kzz!vD_ -nXFhv$tMOu-?r2WLx=5ZhzTeT_U?vE7&CTCwQ1QX3c|lk6@FV6F-mC#>QcJn0vUu1QBkg)jE^2)_q*{b)*8J(6w0%21DB9$O*FM| -@B^sY6tF>YlioIo^!_2T)4`1QY-O00;p4c~(;(W`@cs0RRB_0ssIc0001RX>c!JX>N37a&BR4FJo+JF -Jo_QZDDR?Ut@1>bY*ySE^v8;l0j~RFc3xeK82MPNGS)1l&VtIN?EjHJmr=3iI|7ts-CI61@Gg- -tk@Ic%N>(CYdEc`{B@JzJ>>y=~6FNFc28~+5e~DruP -;TXD+?_}z6J%loVt}VQtQa;e|i%DaD7}5m@bfL(4JKZxQQ;zUgU=hCz1bQbAt;4b!e{am@R|E5mNRIP -)h>@6aWAK2mt$eR#R$OWPFeW005{7000>P003}la4%nJZggdGZeeUMV{B?AlhDcG+6Lto-D&#@SwkX@G#3k8hO^8}WHy3sPrEWKcs1Ud~RHhuH^W+w_xGSO9OH;q8m#+MxP&ty}e>Ld2q8P(j=ZhLXIcMEl(q -mBiELhXvC{dhcOdlM*UU)283b*lWRhCBfRF>;3%dW&nD~t_iB1|ntPlAJ8twpVX81(du_{&GG!}&-kF -Ju+ek@N(ZTKUS3PPSBw!^Cd><|9hLmz*!e*Ny}m-o!*l_SxEjI(byq3EKcBbbxI}Q>E;9;m5-_*b~8u -s*|RLy2ny#{f0jX1Q;BMJZOS@&Uz1trXh!-W3R*!&M>_N2Y&+}>RPd}jc}te49Zf4x%tj6+; -<4er*D<+1(vDWs)x=`Xd!WlSu(J?jO*}IUyo-=U(sidcUc7+vxFyjj8+9Ue+A%|eTVR{e{4*f=SDCfs -nG{P46Eqhy!npU$spK!Zf=Vcj3SDCLP{+$h!(iNkLS)yl^XkLn5HkdF=M{K~YR&ZI)MG*ak8iHZyLcI -6fim3wy_Xc0ppw>(aWUQ%(IyWysSb3A&m~3^jJ~v+CjN6i6(^@) -vB^K(-+#W}i+>>ZZ@&Jyr1b?-N1le)LR>$qck6|_JE9t0$b%<6>iqhE42)ri8&g?sv(+CMV#pL|jpr-fDhxu2=bXyK -Hi%a4D1ZoD)Z3bY{Vf|7BKZne0DMO9KQH000080Q-4XQqOyBujqT?j1EGu{mr(NrNQSCN9;2?@BO>h>g -L9S71HFbFlCHzdy!A^3ALe2RN6ZqV!Q-2+CkHit`y+)LP$3T?%v&dIk@0pJlx2hdI$FmhBtU%e|O)ylff?m@f$dX+Mif9IrFM2 -9;t%GXgj>+(%@{aV_^?pEql71Lpt8l?u}GuE*Bd)EQd&U^xrN{l~=;>v&6A}rl%jQ3a%TIZkMOeL-B} -(t8zxaZ``RoqYZYbxr8WsXQglMu!iF2cF0@)5d#NBqt(!-3u8z#8nS?6h27D(ijKQXkc{$(9M?0e@Uc -@*W|_!q-~bPLKo>=wj(ZSIr!>Y)qvB#4fu#0#)4(aFLQ`ttad_v&+6$YV;5#_Z)nzZPTKgY=eubBikL -a&eZU$QD%PU(d!Iw6Jr6Um9RX^v#V(TjAVos~_uCS0iS^kd_UAp)l$QpYh&cdRqXxXJJLiw` -G)fY&@HpsaN4ii=2o*tTHh;6R0r3RhJk^vC^Cx$)o7?4K-IJoCYFf8K~PsKFufGkdx}n2L1J_XXkpbY -iNYC)X>_J2oZ%cx?e7c{`-QfmiX*jZ9=xa(={n&paj*}cU}WjI+3l=oQ4RL+( -?Y&Yv`?GhL67?Ux;u$))JfV83?4XJKdE&;WwZpZy=-+$6iGQ$4%8AZfc -YXz>}K=U8GqQ$CyEqZ?{XWT6WKyTu7|ya=3C(w#Q73Q5&=(khy1$DXE4ox#)MgXeT}CvM*!oV)--Mu) -tjMn!*9w9>?L}?vLKXLVz6T*K9VG^Xq*`IJ|lz2{b}_%*Vw90cuhi>Zt-h2`6vGZP)h>@6aWAK2mt$eR#U^Vgc8LJ001N -^000{R003}la4%nJZggdGZeeUMV{BOi0Bj-E$L`4Sx+QWLzg7U(G -lNwqF1C5+bwUm?h&XprF)jULGfmMI!Je`*sZ$hX?L{g#hz56#TN~~B#w0Gx}HgCzNcmZ=Z=U0Rk -c-LtaHj*gD1hDu33^-QV5-&xcBEc!iFFm37#QoL_#27=VV=WKOE-~xlbCLNCEtbg8s+Q~KRF9n!?2jp -bq=<#l}aiRSH$JcB_N?0dXWtmFWY;y99_FK6!rh4}5>Slb*$muKcQMAlVjn}c6r4y2C9q$LJ$R#7R;o -Fwg9{5rpv50wEC{*Q3ox&Gwud`SceWtA;kDZ7Z^ot35 -G812&%v^#ru}BGRwxMQCLurF-M>oo0Li-Xbxsn^aVazUkh}kEEr(WJ$?}>u0|ZUT1kxBYCPG38a7DNztx!5;1f;%i)IpK$6vO -ZL*MLkaWwe9QytNDSUr?*UF)1MC};-51JQkbAbOCY1vJ~;qN6=H;lE0p^iYHB^%y@rK~{y@dAZafVPey2Iw^`d1x -AqTYAwD0o$cNmM8%l@)hAT4$O-_Kqc(1y;-6GTCj>euj=9o!6C`nF{klf-=b -Hi-i*8-Y>RTHM@zeL!|_88Z&|R6_~rQ70}$uKI?znq48ghAZQg7mE~+iAN{-k>dojzjQU={C4}6ZRpS -_83OJI2%k{)8$h;D7DPbxAhl$skT4s;C!Gl_u=#ZdNfiDKI615jvu^gbHp6@O7U9euh -oVC1iYsrURUC16;teea|Wd=^Yiavq&G27M2-i($|d?wniaXqYz!xK#hV0nJW#YTij$pBS>lG`&JlhhZ -%r?mJGIj@Ha0{`sg5vB)9}fqG(&OFug7n9Y(81T$D!XZavcHRJfN}Z1$(%n8k8q+_Bt9J#N!%y2wF?# -*wz*nCU+N{Z@ywSuw@VU=$*fFP}doi|xu&X~;!au8|#UvBGuu#sj~29h3Jki8AV<+9h*x!_aewrY7_f -K|BilJJyLdVn+`~Rm>$uMvq91GdxM`nV=ZA@DT$jq+Rmztu)>jJ?k3S)>0UiM|%PIC4xXl4r5L-iN$y -4bQWjU$yZ*QMHh92-?kS+H!u5meY*lW^+-8Cm36cEenEx-$b7FS1j`YRf>5;amS4j*>`L{*2ZdTN7Zt -RwLxJNJwOc{q)5@X~J`RWw*y;tAott@=s@Jd0+rM_bfOu1fQKgm}`cnzXSLX?>8*EOkX9~Iql=BO7zK -)eD40hQScf6_@Y+4s)o3Mqj6*~!PgE578(yUk07KdKTk;zg4^Q~DzCQ(=;ai7^zDL^@S;v%u8OXpndh -+&uUX2+}DxYc1Qn6|nDsdlWovb&sS6L#n}ys-(5wb#5CEp~MvN+G9ldI{g+ZL84r$0yJ6CzL_=rk0vs -aVZ*XRjdg1?Z5&*Q-C@i8$V|9DQLodOIrlF0|WdMelW{~PeHdabyIt0vAfU6?t3~?TFXz{!gF%P4i=t -4!kXHA@ON6A>JL%qXSW?QpJ@sJJI!edR(4ylPR%F&CsONnD=Zml!U;MkDO9>iUIPx*M53w&Ks`072dp -+i$ANkiINZ-ryF6eJ08sqVZ<=BAPxLj^wsjdWDY;^MIR(D!Pp@@jdaYFiTDiFJ@pfQOoeL#uRAP`qwj -5eJ6@hZE1yDfF@mV9fsgOp5-t(Fnxp22iwvD)?O_EQJ0CmXV5YBv0`!{ER;24GMZuWIkd+>goj)eC2to}8jkTi -6Rn1IWL9JEYsq!;8d)Sr(#k?;8E=sMiPSHTAV#Vgzl+!^;YeZFB)raA5c8k_CM-E7LE@X_vU1$is*E8 -qsD8bHV$tFTwI1NsI%5_M9+RzOkDP_jt?7gNTt(VpU_F0<)4$JpvMB+LDPTMPE%A*gSi#01aLSm7lE2 -*D90k2zXrXIm^St0ZJxGPD)?r}BW=F%-KD?WcmIq_t!a&@6_Rmd68-ujS}Pnm9QO40wq{Sj}8YA`-Zk^Yym)w!Yc3vU?x8B3W3p#S!LUc#XL#ab%JQisvEC(O8QDg9G|41W9X*|0L7 -k_svj7hCz+=W!mL=Abb&V9Wj)Rvv8DmcaF>M}6TXP^(uUU1BEB`?mE7ICg;JcH*LEX7uZ3^ILZ}3F-i -kly!<--n@kCxn`Q`xZZ~+Vuf??S3D@G1XK5vCPZ;P@)4L;Y3!=sY3=P$4nRE*Ze*lgDYsrJ_xhwv?H7 -Usg%K_!M(B}={OFm|W=>7?O^4e@%CxxR}hV#6r6Wy3(L1L!kiEB>Fzi~lE&u=X20gK<1}0q&SCTDFBG -raSqH;g1O(aSa604|^4^Ui-~+bfL%im;w%=&L0B@KA6uS-+-_EPkqdQjfj&O3&Z~w?kwjp6l;SXgHlI -KH4W@2nd$Oi27)0MPx5?7tCzI8ipUivK}YytYY?w-pTy=zyanhk_*j|)cS~mXAbYC+pcb4sqP@nBkg3 -7!xLFg0rM#k6=Iy_>l_K8RP`3uO2_p21Y+zrhcJ2`U*lM>&>|P~uF=k`I2$0t;b79~tk5+ugD48oSm0CRS(4#7VO?~Tu!T95uroXtfZB6|$ -8|GCKzRzOVz8$p;RyxZ;6aB;sEQj<p8Qe6|dr`rz -P(Ie7JtpT(og?x(#m2%FIV=7GNN`85xfuEo08L$s$55^ZW=lPrIarW+)EA7P}s8^N#ObRRzjYQK0JO=*K6ekd}YS5%@qq*;av02PK<-2q4+sYCQyxV% -8--+q>&kAS~+aAF;0c`oqKF~+y2*N1XIq~$ecD?4WK`-Qo~&3sKCyJ_5EGTmN!=ev$vg_2cvhgEJ1xKbzTbNZe3Nm7As -L%UgG))FN1w6(ek_Ym;K!#v(OMUYpg6ymaA3GZCwBH>izeB`*=OPyi|)tPqT#}5DF5^N>vTht)$5#(O -l^jw^Jj37Ne2Xo3jzaXIH1VI#;FAe8oqP*k6=2OwCy#=X0U4DHB>NZCK7LtXm1s#4e?oFj?hgQ;@9J8 -)L=lEiGj*78qSQ*6vO=5)ZkkEqm*}poAAEZp+TudsRWu5sc2JMwBAA(}?#pFS#+}S_=rIMRDdWIOktMaeJpDM+vd#Y%N%!swbA+Rcg)FOPH?ZNP073!SF4 -s32ulA6i>>MeQu0p5Vcu}z|0b@v|uL;F*Hr>@NOyzeu9usu6Yh$F{Im?!>Crdjn~pH{o066VAsmgqdjMzS}FE$B}iAb-An;a2~ -d+Hog7`gyY6;(yKl}}DUBT;BCeO*2m|s@w`ps-8@(aD3gNINt+o_@q6Ks&BO*B5V@eAC4qZj41GfO;L -LyX{cN#hO_FpS~XZn`+M)&3h!W1gOtby5_SGh#WpdHER!MdKBOv%WWB)v06$tkoSY-ffxgz1wc*=HrI -ai?CIIqAhXDv|m?@L~6pcel50iHpc#cXDK<5qm2-4;hW7Zva1q8>LmbU+sy%@M+tG|O>Henp81o?kFku{*JuDXsx;8dO_@FoDxRiH-EoFg>%<@JXB*zqtmdMfl?Yx%wVdP4dT(`yz7{ -R$`#QKSWnc(Lxi^>IHabP$`ONYUP%^FH9Riqs@#*a*n3%$XPJdB(1+1 -Itk9&9JE*r{23>xMv0G2hj6;I+8mKFJO4sH}gd=8`;#YWW! --CMIw7VD+Vm#_q4VhN^LEFNk&7giRcQf4Lo1+iM -gJOd7pdJ|ItKGA0!%DmKq7n>FdD{`$bVZDcQ9s -g?_V+cRLHa7FDyd`IF%3yU`i7LTOaF*1t*y=3$;kq4-pr^iIjJ}B`08Y}sW&GQSxlIdSFc}x+Y+H-2@ -N-Cb#T&mf~T&`ZEA7G(3&YNMja`IYLqp$a7Y%uha1N}&n@#$(XL>zO-DVMQI7*b8Zwb_7)@f?x|gakE -e2~Ch%&3J5V0;VP`lFO_rWPVaaq`>YFZAvTL=#a&V2Mz@bFoQz(+zT3&(TG>snNk9y5wdIAN&p6Jofc -CLppSCquDk~`efch_+t -@CnGchN`kzN6nOZuT!4dB0vcQi}DT~&x1S6M^$hg+87FygYq8l4P!tpQN`@*dAH<84sde&ZT4R2D7Bn -A(uD7T2aOkTZuovj%C);!W9zw=4Bo5|{1fCJj~AH1oN9Kr|$e$MAz3&(q@ZW3LIixhb_#egW%lc2wb} -?{9Hqoc!~r)6>)E?_Rz9j=JUc>f5cWS4pSn_n}yQ2w3-etN*eZ{Z18R%e*USz!!LEtaf++qo-`VGZe# -#zhN&@`roJWHu)pG4l>wHi$SdCqUh`n{`*~VH$}UJ8EyG6Q1gbjnVMP*ZuN*=+$&kIpM}QkBJZW^@BG -l6W&25*-(#*lXQ$ceaJ=_aNWddGrq^?J(lRFek`t4Bt%7i7i;)il+JJIVXsq}uY3B)6vR@+2)E2+)3$ -v?XuhpVMrr_Ap9!ZGQDm_qwl6JeeZKOa3d)T`7h`o7v`pOUdxxxsPTO>GFR#9ZcjwT0#oj9 -_007aX-69=QV^cIzB7E9M}A`xPCaXTg+)k-nvrKVPuJF`m2B*{)L38#>*5B4p1hrsbr-Cd2W>`c?|+J -E0k;}N@Diu^7>$6;vQR28X?Od4G`H2ljrMYV^ID^)b5=(qLJ1mTEB^O~tf`(r-`v9yC&Mz#lfW=DVCd -Hn@Of*S?-UcMN&3x>;K7mcm=IfwG&aXil8G-Tjs-fQzPl}f{rse8SX=SBleAB-)TQl@5~=}a*mUk2>B -cT^X!JN$T2Y=N_-dk_K;lzcTSkkGnE1agg*dO*(KsP(*Twi?F&htf`lqdG?^+cpy!X19r>jcS&Izo8 -#s(=Iv2=O~9S(C)oP8s@aO^r%d6()cR0`g6%Ku8}Cbu^PLTztO8E?R&4|{`DVtP_K`p{%7ijpy9^SAI -TW^Zov07i6_xXdLa9TXON_Q6Pe{3*uv*tKXG-bA~ct-5$n_UCi3HxV;5NciG)3Nm)(3xOyioK*1aJ&0 -_c{6s~s+IEEmDyy}*|8pSG@gl1=s=&};=@qp0*606h2CIS~iRgNTFF{`vO(+x9BmIY4x6RauQmMM{xx -E`FTe{NuyhYv}unKWkO$bg$%%KoljL*(;gpX$JC=r8aP6z5w -TA4nC)xcnMkv+l0I_dS|Ade0VK;>?KAia(sY7o>-qLUI@W5DyCJl#DKEKHVFpTg!dw?{NF+`7OFWib+ -Rs@ZHl8>FMp5hY|n)$wB}CAOHXWaA|NaUukZ1WpZv|Y%gPMX) -khRabII^ZEaz0WG--d)mv?k+c=W`?q9)Ee~3G<)w4P5F3y1Nb|&fFG%lI+CA}Lg27w?;w9Rc<(uz`de -1rM#S5+iMQlk1LGl#<&4YW;>#bU8uo+?sN6uqviE)$hx-GQ^$_3@Zh1>0tlv%JitP5%;s^U5Dk+q^cF>!3_wnMrLf7~(452E7jiQ&4kvSgx@><-pax4h8H;#D -rVTru9|@gj<%#X{37>-`?sGCyl+zR8nZ?ArRsc72t1bzMrs0<5(YS*f1ZO$Wb0mipn}va;uTWOr(C#r -nzj)oI1v3E8dKho%GJX61_A^i9bZs(T}vI2BdLTX+=bu&jaV=L6^EYitR2ErscrAB&oZ%bAWbZhWeD# -*ETRF*ii_hEdS^wHu9ktyWSl1q$pm~gRfJ}M|7AR*nR?T=P+OE$^ -Ufr2FTZj;#4r!0Zo7(4#1$uSZip0407%c1^wJp=M%8oou&ligY#WkkQNP8vlW1?CHeB1eEK^$ZB!2jQ -?u*i-$fbGB`8Lw)>r3rcI7F}Da4S0DT4|M2D!FMc$)ru}nm$-H3Z2j--$GYMhdX692gF(Z{q2IZE1S4 -WYd^5zBpR9xU0)jwtYPvrWdq)obrm*v)Um{1D_H8Mbc?rQ%N0N;WYQ)w+9wXULu#aE9ST12nA9>fehz -Uw~&gvZ_q6~>L+S*?7cHOk~ohTqW>nUO*RAu>w4dDByFSdP$K!O3MS=Y5oJ#@vQs#dH=EB5ECl>FRsg -dd;;gr>uWd0-rh=iD0wv%w>ulFwb7H3%)1@ZAp8Y)@wcHEJ_Q#FI(YgwmhUnGFc&*`ytG#yI162Vv0sd!w*$pa-Iu_Okt7V=V{DpU*1DebltO9~;!8AF4w|Bk -3)vptZI?0o?mSh}36&%fU;4Z$LCSWvXI_u50Ax;$knlL%&7nb)n$G#qrdfE`*f8+QlFL@N -a)$w<5nqOzx3E&x7c7EK;-_+UJobcpV*oHkR*^LabckE#Pg(8-X)7Zpe1)#F0Z3d#Ky2lIexwx -{MPc$V@>Al)47w1LhD5nQi5A(#7?kMyc#HOHZaWC|OQiftlhTwWy{ufX(A(M(3#FQx*Xaa(_o?xxP|p -IVSo)QX0P6Jyo>K6uN^?2wu{e_&p%%`!BTeuj -!^ls-72>6e&pNM!grx8=G+XankMC-yxi!Vq)6f?kBU!$+quLwXc2^z%gjm40u0GOKVm`Eb8qT;AVs@a -soN8IZ?&SOml}7z5F4&>X^B;D5noqW@K%yQM!iR>KjDC7#P5=+L;L8~md7h#Dkbq3Zvot@}npL -jw_4A^bSOid%{4LFbfRT*N5ZJ-B;XoHDUJPBbv7Xx(7t1>j8$qgu!tni>aoqf!f%avBt$5*8q7rZWLX -nLq%19!OVG%pAO=;eoL4M`$;mzp2CX8u(VALoRs{TanYsYUtepCRAlAjT|6c95Cjnzz{(d#~H#UaaZ=}d_9IWvZPE425U@ -Q|+z~uh(^78uoAKqQ2H<$0P-&}o6Z~lIL`3bXuY0en`@Ykz%Z_>*j{}xCnEq>pr9@@5_yV(|N!({`m5N;oDQ23!CI4E9uUgfw -FfGzqDuXmtv~@-G4mAXKP(r3#vinwaoJ`y1v}#K~RO~abT;IbssVR=x3G-mJ{{Sls*eXn~Gpvx|W~jh -{s4=!~#PC~J09+xBDTSI3xn;Omvj7NZV3Vj|V^)m8Ataf}!MK6jdJmAb*H(_ -_OUqJ7XJOP3^GM!;WOx4ktuJ*~;6mf91nbhQbDcy;T#3!V7r7cz~Kt?BAci$8l1r9xvvvFFYXPW9~bV -_v%4j#J34cI8C!;u^tQzZ=W(f}Ca&sRUb`SAG@qlJLfd4)#H-UGmAW|*c)HUh5Kqy?A+0K&@(;Iyc78 -H@UYR~M3sZxYF8=FKjR$B>w{c2lw -&@SW+r#umWf{$W`RPErtN%l$Jz+fn!-LYfmb$>zbP|hVO;^0T-sm6}Gx8w=`ci^QF501V9dH$HKmQ9P -|{OSW>N_1iZKGO!3AtdEa7%rS-GVjaykAFxWT~1^=*zUsR;ROx)&@UaC1sCn-vfMH0-Po$wEloi@{7< -O3oO&f-7|lzspu&{32R%G5$__*I83;WHJVD7y;4E88agLtMeRf2Yt6sH3RQe83$(tgN&(dz;V>Zlq8<@Lao=ZPZJ^*;ofCaY5$NDGDd(=(OQnr;0{HUf?Y%YkN2pfagI1IuixiYV##upM5YO4uzd^nHoTfgUAoeP+<~)2dKlAvmvB#zQhlK2_$92CTjRx(5 -rwls<2Io)5&S$*I~&fDHi1*5m7@l_5K#v16WZ<+n~DZR&2f0!`ZWwKj?>PycS#s=Wm?{*H2=_$q>I&(jf2E{7v1ZsE2Q=q>D3Tnzk-_I{A;MJ)Ce1faZaXCBQS@wkPiPH}0ujG2~yELGj&=vKIt!`ey56m^aZQa8N%m>J+=~aI@~AvnCYJtG7O=sxu9E0V(N^F^>a6nKEkVW6w4w=K -tITrpU`Yh(tU(Em^7YGOo$bnOAE34ehXVmWiC(>me>ZyWd6AGzza=(4MKB-iRIYIB(8uKBpY%L!t>Yj -ah!j4@g9lFn4&A3N+nhQYY3bj7CpYpX?vGVqS^;)J4r`z}Y9F8F3~@bizK^(6aXgXZc#a=%I(yhunQk -+?4!6tu&{53Va`Tg(y=m-I+PnX}EhRQDnd#68Lg2a4$#o{>r5&)U*eW*v#rOrUU -5mJ(BIhW6X8GYe%Inv<=?x%Sj$sO=wurkw-;;q1`ktuN|_N!|W=G$)oOR)d|LB6&(RPkaI0imMD44(> -2qPnZi?J#wtNmZR7;l2|fU56aiFJTvJQ$hu}h_@@KhKZN3?9m27t$Ruku_Rh%bg|f_6@47CKb3%QIik -!KrU!$?)1yJW-+->|Rk`q0JUy%wTl2MDcm#BOdJcj3LS**X@5 -ewt`{6Z=aT23*s-82UEE8n2xrzdbET#irqik1kNCpkfyt#sqGU%`8X$ZB;}acoUt1s*|OuOZ5=(y3ti -n9AGcQjO`C4GbS;paJU%Y2 -k0>FFH2NJgl5SW##}R&n%YV7o{soyD!ydrH71K_0STNSh90DLskvw87_!UMXxnBq -S5-Z{D|c8!UZN9quhtE&!EJnaI=aysq5ef1b)bshfAPVcJ>o~*L|=R>1Nw%i$KFegGqv}>*F8J-vZG1 -_%Q{x_3}2F|xF>!9DL)f4Glvf^mSE9f&?h9vG2f+=8a?J`;_{d1Kh9so>PRKb4Srs}EJRMF0oY?2mqa -Y`@3Y&LpHKA+KhsvfWqJnkexf0ZYmquEV71GO>D2C8t##xp3HleCf2G7>9z{}HZCPhxDdl|&Rk6 -fq4Q(w-AhP(BoH*|557G3i9U+#VO>nabPqZZ(z1?ZG -bFM)9K#^3v>f-9gBc)DrsZH-C`1!VOT7eHK(V<&y)bhxQ;mSO@d*!dmTpCrwI{cmZT(H^4^yY+>79|XZ;IZ=B>vCwDV -8VuWb}LVYLaVTiTBH8_PpmwSuRYHs-mQ}7H~ -B`|K;N9azA@Cq2^yP3yzFQOD$J&FmQCH$!TqC0**I+ZM0ZOTCl}pJQxg8L80{}OqEQ}-4QuW*~x(5FJ -n0|po|O?`It;!kaHy?&HZW04a1t97o}hY&Su$})=G~cxfYOf$<5k}n79`z76v+2=7bolzf@|}*DFd>% -Wqj!ebhH$+}f~!XSiJ~M$H*so93;+#nBb>3bm^_7g(BKe8B2Q_|a+EQk`EG>6+?b2k67;_4S{#Vaswv -&We(2o>C#U6Y`u?WkxMCGs6&~87@ZM7$SMfT1_A{aE0r>b>8yaI3^bg@o`F?Bu~HnAsjP4*)b_uL{q> -Oe>HTTdy5D+J%87>Vh?bT7@Mie_QXcId-N#U=_+{PAO?=NczqMzMhA=;-;wWvgk_NMiVDL9J$mr=HuY -LGXiogfsJ)7Df1gu0*=Xz-4atyv(}&4uOdj_<_ZWOhl8|9v#|_Wy+PS}t?L`nBV*7AZBWj^Fk;?W=?u -Lt|i2ThOx4gkqE6d1|g -jkU~qUnlq*@$tJs&vtPy>$NTxMIJn<0j|H-X5$4;<%&ex!O2;*YuVVN|u7k8)5`?>MBCBjHpUS@9sY{ -(D&V$U~&(K7m1b*yxJR! -`c2}E2m*w2L^rN)LjQ4^J>)F4f`1Wrauri?IPX-l3xAu*FVWWuww5_XJug)AHfv?G#l7%Qh#xRafBNU -oL@h6sMcpgg_hX8Ljt00QooVod-UQr* -eQdbSBjoJNuJyjH4oLIJxPB*{Jy@j1h&x-H?Pr13_D<`Y5z|8K9DnNme7(VT;^&te1x+enc-yDS+ySj -+eFj))4@eozWktG4*%;c6BWsaO&D!Ba(y_69RJZh_pjr7a!fyw1Q2z?Y(G#QgvNeM?BD8evY*icj4q0~{z -+pwriCM{<8`(6eOEoOg*q@H%gPO=u3W^LfESn41xkz{^Yd31lPHBJECKZx1xZ4KjGI!>En*kA*|1}@h -q(TB8$q#IGV5?11)TQ^;f`g8bTrCog^_`Z4c(er9|;DT2Ef%@a2YvYRoYjoaIk}QzKN_$7MB&N_|O3`!(U$+|Ux%s#&?#w^n_5EU)o1gEOai3$FeHq -X>{0!LbdSqvE-UXh6djqz2h`1}XH)>;=wvA*iCViQz%J4-7T#(OZW{s>AjvzmuU(OM788bCV5IvIIa* -9=3QP72_`41LQCPW6^m$)Ii3Diab|1eUNRVOZD~$svlZx-bAK@_w8<+>1ScYn`$iC7t%Y^PQ`Wt{uee3JTkOLw)xPM3mY2Ts9v34Nyx11QY-O00;p4c~(>XdH_p -U0001V0000X0001RX>c!JX>N37a&BR4FJo+JFLQKZbaiuIV{c?-b1rasJ&UmofG`Ze_FT~ups+GP8$< -;pC~3-=|G$6*Hp`aPQbN@*g$_`J<)t2scH*1-GZ9*mYV(2AoVfbRM)?f`T!O8zsV`QJ?77H)jX><@T+ -@d7A8~*OP)h>@6aWAK2mt$eR#W1KK|^)~005W{001HY003}la4%nJZggdGZeeUMV{dJ3VQyq|FJE72Z -fSI1UoLQYombzF<1`R{&tEYL545K?@LXw;?)DC`5^yVVa1X0h*G-(ps$&P+X)hrDcV_IQb=oWt`jTdT -e*VTDPtr6^9$9{1dk(o)jtM2y9+;HShz3P<%~WBN6zvjGH`+J|4=Hv@X>^S?Qu5pht!%FX#cE!-wvjx -TxUQk7z4oo@R`6crZUrA3@?$ayc9=5T3gx&#S(ZzY?U?1;9>w5)A6}EB|MQ?q4R=w}MH*?+6{NK;TFJ -K!bJYrR$*kRy^$Ki@cBV_0N%9qZs)U6?_@$r_3e7Dr*tIIJK$lQ)cI-fk($Qi{ZBQZ`(5-7)x4{5w_@ -LNMlGjm-!V_5(A}kRWxwcMr%YGTwM`#SUT={@6>ovuD$$X?w6$fn!Zb(%#hn(0;&O+EmvJOqr_`{Jaa -FuTN6+SqG)dH;+tYehwBwWy0TsEXvJoiOTF&5$}#g~=0EnC_J6s2YdT$JP11Le#LZeU>>o{LiCxLGu-4dlQAe0XPmSc_J8+>7=&?mLbl9A=cZ;dDG%nG71K4XRdOaOnWP`D~7(@m^jc%9c -zuPz(hRu{{MAnLUd7R>|H;P1F-rIAAe?}5*1d|QG@S -pz=K|@yqD>7cs^l2^v?- -Ql^1>YwSkQfn!m1D;Nw$qbjU1Q2Co*(9d3Qke_dlF2*`VBi+*-h1~Vn@1jYV0rqn?RSM;yafF|E0kqG8~aSHXmepGxcT4+zT2>DN!~G^lx)G&Um9)@QJj~kt-UUzdo|?ZyPj=DE88O^pUm!+a2t=!!g?QhH?Ip{RKnt -^nBfoVBYo53xW9kKp_mywnh#9J^h$>YekLi$P6{Gmm+x9htD9IdY{kS59E?9u-55yu8qM?mkoUSW`ZM -kiF6EKN0cs?8b6zGn_@4v0h$jIK9Vx2Puwhe%)xd57I+#5ScJuoDw7>}XBW#phA6MXY|h(spk$OVaA|NaUukZ1WpZv|Y%gPPZEaz0WO -FZLVPj}zE^v9xSzB-8HWq&OuOKuSL~3nix>%rqv7SYdPIn6wX)wv|KBR#nOSG+47PTZ5k2lDF?>UF0L -|vR@+5+7G6UXGaUp^jkihV7lWo}?tE19<&r-#@Z$@IDBP)w|ohqJWV!uzT+-bTnUr(_po$5vbQ_huR`S9` -Mk00mk`d=3pAFi*bIQgfH*SZju8mRdorI2%WRn+v?6t8iu6x*#Ak1f|fzzUbMSxKL4)vbj|Ql)#IB++ -a(%gR(Kc9CW~ZgqzCGg{6V`~*QjpWj!i=9y*~3)oJ#91S^B?6Bj!WEo0`XGD74tZmfP(yT04UdSc8-i -ZU#J8r0P4I~PAj4e@7ZG@%5Kjl8fps;i;8r|_Z>Jsw3q^wge(I>^khSh2I3z(;v0@)lgSB-fk--`#nW -FP61i312`@EKHjcNXYlWX0{VW#?>lxyDYG=38Fsbj+gZ+xaKAH4Aupda1B*#bh30Ws_l}I6c -$>Y5mkbf*NKY;VIfSEaZ$l=EFB75v704gk48l8YI2^D_=i~V2(SaxGNTpbW%UK}Ci$WmgD4MrSrkxba0c-_bb6 -yfZ@k7?t9Ipl@xwr8q45gOyj@z?9FEQ%mwfJg#+EZO7S4>g6`|#^9}7ovYgq0=n|ae4w_h#qI-AX5-J -Z!fO7LeQ%VX=oYrM9rZd7QSx7S;*Xk(a%?mSra%7lpemZSxYSpL8f1~)?BA%8^jX3s0F0uP0(f7rg^s -70bFtBLf_HoI!}$xiD!p~(+Q3uXo~Du$IR_x7AuQPg8@2a6Fa%|NQFn&68d@-&lV6Nyq}sjY0o-=($V -MtTntjP%bDU^qMxo&~D-A5IJ1_#2xuDQU+sA_*sqK4H>pB~JR`g$d?2JIeI&$lVI!oad0#39#}eAOGj -d&Qy>h{s|_mt;1fYe90as+p&#f~|Ld51GQz9L)iHA~Rx2MsUQk4?j8 ->jIL*d>ji#qHUPoTB!Fb|`LL)Py?X+%XNjM#hj)$fJtNz}JD4R0=RI}j{>iWcCwlk3p`8ThuGk#CDT= -)9W)TY+eE5$J0s(G~;YBO#iQ6i3e(JY=(10msZ{;|TO{g>v2P-C2jBe6%3zCP9udnvxDT6EKfSG2r_Qfu{6(I -Ny+~ZxfxgmMp39%AQ-rY7n|-a)D4NcjOYI+!xj!l$!!Wh6{Ot~pFJ=E%@ZzpaFN?Y=ITRe1N>e?dG8i~`gO|QRZqOW -bVYv)MSAD?@bX1gHEVh<1}G%+;d41u>kbWqpwT7N$?(+1%P=djUf}5coOQHfyPWV4(>{yAx> -cibFFkaGZ#b?Taoz69$un7cFk@iQenV4YaB`n%j;rtvAP~4htdXS^et675nR7zMiwftEM;f`!O$YCS5 -mlCs{l~U*KE!Q@D6sH$|C0MSovB){R@48Gis(OkKyRwt#Nd{yoP+(e?M||q37HL`UQkbo|1X -z2Z0J-NM-vTIM?#Zdpp%PC19Kor98=X{{fFq2BNi+wPN-KI@siA*2Wle -+(7ydoILxvfo*f@v->YF_V+zu+8#(G0%D}`Mp)?hwYKMitLjWUAI2{9v{`qUz;mqg1BKInjdZpEd4r$MD{dhZJJD^N=V1QY-O00;p4c~(;zHf)J80ssJ&1^@sb00 -01RX>c!JX>N37a&BR4FJo_QZDDR?b1!3PWn*hDaCx;5&Y5WV|X41t0TUbE*w_Y!(5yO6NGgkThV> -}VrPT79(S-|xs@QMPV-X+zJ&kg3a*yR*7}Yc}qn#+NtDWyZyDzVfTBpdD!iiDwRgV99Pm7)d{QfRGB6 -~tKTX$tIn*&(xS_UZG+5Vyk7=rLM0MHV4Ww~QZw(kutmphHMMy$oPd;f+LIVTNi=XjNm{h%9{attEG6 -r(EVCp@D#5IUP2e_al8*n`y63lAt)9^E<^nS#7|B{Vv1bI5R@1SM0-;8YvPM~Pv;|crJ%qQEOiaQC^y -^L=)xw?(4r(3d%o-aoUZxgBlfxKZkH+V~OiORU?7em7kioYf#LBIc5f&hp7sBw+$xCW>wABAYsncu*B -4I;qPN$QN;FSy0cK_PMGOzBA?MnoITYK9q -tawV&SJB?_I}7wP_}6oT;W>ww6R%FdElB0AgHWvUMTD5ba>jV1<>2*P`|pR>DIdrj8TP4C52xv$CiX$ -2|5|>U{3kYB(4lm_3rFwZqWo~}`m`@{fEvGP%{!Q}Ht*TSn`v5sw2g0A@H4)(w*ne;-pJugJO@x%yiL -2&c=c1k(Wo=ZSWZi>dPxJEEj5_F@&5o&O9KQH000080Q-4XQ|fU;s?`Gk0FDa)03-ka0B~t=FJEbHbY -*gGVQepBZ*6U1Ze(*WV{dJ6Y-Mz5Z*DGdd8Jn0Z{s!$e)nHNczLjQv2WR903Xn08Bnad1W9j)Lt7ZGE -vMQP+mqz>(jfo)D9Mh0=C*Zyu`H4w$&V6Ex-W7rK^%ld%~F!D@`Xo*mGXQKc_E943j&q&_lT857@0g2 -G~3mZ8!Dt_1O+YpkY9f+K0*MtZCl?d1UQN(!q^EgxPlQ$^9;~4mq!tETFBEyErOvd7^?vew~N -j7`gV&Bjuzl!W8P2pT)Ttn2@x-Ba2)q6IUdhT^(frUa((k9aA4x-&+zkV^Yb>aC&y#+dH4Op>14zGea -E`@X!V8C4<^m}AB@)5>+tH+?W*Q(8O&fZgC9St3`N -<=HrzJ#^{5$aX9lc)$KThEw_tibEx>A&A231T6?wl6`HF0#0?Y8O>C|U1;GBOw74+#<ak4 -BcW|0u86;=hSnxa#^-FdBk1=xWK!lCY>GKWV3r638Ud9&Tr-q+npnbTdCa}sj$S4r{xOtNQeQMbG#Mv -gZWq9h5i*`u9s1YxNQ-Us-YXJmfPQ*Hb4Uh4_JF>pFu+McJo-n6&-*xQNpc?)bIRcu&wA$uBnFR(|@@ -RxuB-IIF~MN(id_gKf)El@uZ%~B7`{qMQGj->Yb*3^bzZ_e!X|0brJLO-+gt(lFWy*afWmTPsB)AI@U -v1Jf0tL(xZ&&$I3VcN~#ZO%I{GkwmdP@VdDHQP)f;ye6O^UNJKh6jBUaL#Bh0$!{&HVi31K9NDld8)T -}6z>_BbXyHkZwS18!?D5tlN>~A9?+`wr9_K6tdb4y%)VyEtSGv{frliH0e2tH5HzjR^BY~QH1AZv3_I -wfp8ouVBqWpFQ=*=cuyN3GI$oy8kn3ZB*wG!;^n!X8bKGu&?tT@DRHrws)i@@^z`Uj=i=gAShs}vBd3 -K%|>utgW=_P+a0GSZZ{TEIp876HnH=EUF4qx&T_N912jI1+#NU%^vRR03q7y+>ocKsU|xg3E1!xY>I!3_-SNTUS}sky274$p3yb{E$eAcG51od5CR -sX2|)?2WLjbp=P`xCEM+aYBwp=PbSPa(XMOGHdKhoma_w4byH_tP@>oW$jX-LwdPoZ472jFI>zL>5KT -;O>ZV{-R!$UMUDQchR1M|uEZLMrvd#oeH|%i8s@(8jsGp};M3XhG0H&M@!*Y3dcXJn$#r?&_bqy!(CNlzioo|0_S6c5zpCL!ZQ`4~_j^hDH -jQ)~;GM_LTc3lMV&|0R*jFIkETjwk5=eZ>S2%*bb|SRT-q324tYMb^-qJQmHKATpX-AoW61(W` -AG*YEM;b@Fn$Zg-aI<1-5hs1ziCd|Ol?rUi%K$Z${)UTsOmDoWUvOsB1&Jcdtg1Z1C11^5BUI3+clhU -PKJ$V0)K7D5sQ&Hu!$#Z3^93W2#LxDEW&HbUXXAWG0TP(>u<2A=ZALQvSLEXjf8V5&`~;$WZEH -LXa)Brl#Z~8K7m>mQB+6nEX=gD#mHb>HpdSJ|E!7;K^UOM!yy4g7eNN@$W4L0a^Zn!lz6I;jb&{ -Z7+~4r&Hx5lgS<~hLUmt3Hix| -6SHqHQ=8)VR9$T@2r`ivDh782R6D6kE(1bHwuTO?9?Xyf3ei&tO`-N2(8&~QDCG~IG?`zW^DA?V)OhY -~@P>S%P^%r_>ejB&E$MWFcw)A~&*l4-d4sK~-@H#S+q!Ab+QDQA3rZb|T?h5n-CFDc>@FNCu -uLSz;0-|78W38>&Z|hi2a>a)~477pG0jD^+s$%Ia`W>_{V-eoHFnWYO^4cL+o21d$h^J -(&B&UuQl?(hT`#d+UG*nLL8j~j(=MIkeWgd|H)IEV7VD-303L@yC>!16esOu%oh|9^_Gp}1G{fiXo8L -ZO_bgVmbDC*!_2sf>>F(OIISKNqTTWZ~N9S4_l-ZCO2`q?5N7L5Ba@ASG+$OJdU6?`x)>e?t1bQi_?; -x)c*@DR;)nTP2?3-0bPtp+ywjGJ;sX~c>zHKBBBL+0#kO~cXVs;Tm%O2^Ki|R?$dyat?;xt^n*YqKVT -9>p6ZF59qJ|`b$z9_QEoLhxw&}4R@I;h>`T}Zz{$V0Bj&Q&4;CW4v*kbtVOhNS1#_(9mW&DV;oxSSqWT6sbe}7wgIiGQj-3l`6+ktM716ngwMJVDy1)?GaHJycP4eP{La>E%i)=_2as -M*zV39Ylxlm5;s^$JwNmwuC$x`E)NtVmqQS4Av4q~80+KKgMUh^*gc|0IngOcmXcN}`S-Ioqp+2873f -_>WDnGh@6b}RFJ*t0+0<~tZ#a)m>Oek|y+)mJ&HdwQ1wkMuZ%Q0$IaOAyvx4J{i!;dJP)}|t?>jG5sM ->R;Q^2TZVHmQiCF9U9rO?)a65J@#!}f+;H7Y}~hpw-+=7EwOFD$rTj%w%@n_-+KWT;WisX#?k^>pY2< -jhGZavV~DM77~dNrl_@&C3a9CL>Yq;9g<4^;V9=3f&EiZxlH|?dRd4=c1CL@VZdXrpCmX9qJO=RQL&@I>P~> -bbyc5(FwqR(z2#WrTTM3zk7pOuNUuIUC!RM7@4`;y;OW`i*??^H&V|t+UB%s-+A!(>i_TdX&L6oQ?dg`u4B$E+8?| -T_p=DZ6{dFRHz9IiXdu`CMwrvSL7RSM3KZ7$iOo;5nz{DtgP^1jr5}So_NO{U*kQR=j=+eA#_s?w1s> -ciNE6ncQIo-|3ZAmjQ2B-2Rd$Y&apxD?$kvDu#bT+$O={NUk%GKqO!4&9RW|g*pxSH9nU?wFxrNZejf -q9ahvbsy|Hs*nPffnF31jUoSYy4um<)z_T4TPz8aNjFTbDj0ySEOxfzb{0r{|``00|XQR000O8`*~JV -T3!3Cp$Gr~OVaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZMWny(_E^v9B8C`GOHu8Oc1*;;c1Z;% -lbhL -LbSm}daRZ6x*_NHe_bK(#F!kINIh=m%4r_8L3bQ8HLc{Mrdn07WnpL>57k -y42c~?s12t*rs!Icq}2-KeTT=8S5o)upi%9B5GrGb2acM>q|7Sx}vMhXFr(n>U(QVS}2ipoMZqpL(`x -B|ZOq8ZsK+EQ9^^clc>$KcvRwk_+*fOX+M&!J32147Hba%z~WxuN~R?9cL*Ys1W-;lgsV&&}P%#nsKt -Nfg;tq8^l$bz(2tJcZSA!%AjYKtQ&5_b8O?THcvcD1#A=&b`wGJkgZ^4!W+-qqIU7@jYmq(TYfqE3ys -VoCCzQ-AgI$SPKTtR;yLXE28)Ei9VtWr+Ba-+Nd?L8yob|jJ*9oKFf}8Rs?<=@?Op`ZyL*D3QO%T -%~&FDhRP~frQD85x&`aOC8p3lKINoK+3yH*5{?ifDD6N~XA%sx_Nnz{S%ClWb9co5(O{UAu8I5XS_6g -A%rZCUTc!05fV`hs5%?t%!eXTZW1Qb>-g{C`q(>BvObErr6{(m1%XHA(!1z+2*D8Av7;9XJ~$ls)g## -hn=jYc#9kIivyI^FmYybh<`#T;~z=$41LFQM{FpkRrMR0JDOVUhztNBT^APRIq|TN<~*R -Ly1%{ltF(wn9e+CIrNMpWVNEAQg~D2rwO~a!`F~serykMUw3{!2{eHX+Ah -@{7h~Y>onlE0AeIbTAO|oG*pnf1cN#L6gPu%8>!>SaRN6WD>FVU2N_<0>3u^)-APEc&OI>p+)Wf?HLR -^oqqU7I>oikU*m%a>jtwGGu#W6F8Ty{9f@ypIWPi(+mH-Eu(ctd6lP0(g%XBTWqGEqn-Kbv!r%qS(!N|d^kMQK_kaiMySkYB(Ab -t8AI0lTkVeReDU$eZoEEVdx%pBbalh}b&jB#Z3i7L{gVsA~0%{!1x9UK^!7IpaBV}-tlJADDYXqnOnp%3-j-f@rbmj9`DzzzvuU-RY80A%~{47TkqSk(YV6-TuX3j`X&MD%rYLb -zNQ-s-rbSzc+&wPv-Ay<|KZbryNxD+6I>NR(twYi`yh3+e3CicQkHj_trP!Fg?|8$V5wh9Wks6k|o^;uOw6m@_og-)Dr|kk@Ni6#Zy1DrC^QY_IFK#z8AqQxWw9!-D-lJcg -QVqD-^GlZGSOJ+iX6cM_77g8Y`$pTAZswSC1+3=+%{$3OY<{oZbFg5*M(h-Wq&7xx;#0v1=Lhwj>BwH -y_PsuZ%)~OR)~O#pHh$t;hz20E5v{-!`TpI???s~tR_3F5B@oR54HR$&VRNm$KMS4CrGRdQ9cK -X~d@CSuR*!%#}0>;bp%jnJxu!7sr1vv8Akv{>QsdRdMU}@P&Xrq25?`>`$>5vbT#U|GA$nAk!ZlTf+e -0FAaRqOlBi2JZ|Nkj9qR6arb!dE|XPpk%jh&kWNKW~~NIk9X4MCOI{0>^jGah-lo)t -^C-}Z6BOOP%gGm}HF^6ss1Y(Z9uVjk^M^&g4W=3?ZO2u1Rv2eAH)t{H8W?I%+z7szPu(`=Kmk6RU_Xd -9fp*k8Dr)g7KdcEwMLHVC_0l2ctSnOi4!wV#;0Q8rzZ~R*>6??}vy3W`cVq`kF$5`$PD(+{QP;@g6qJ -Zg8&DjMbWeNE?Vcvofz=w -wK0}dt%1ADyb-w{K3(~OZkqt&VLt1Q<2_G1cHYn2I8VVnvEQpmbT8RdP0q}y`;wT5x55}T~0x2tZRba -OT4_Xi^_N)VsL1useawrO(Pe75GN(U~tT;c@@}P)h>@6aWAK2mt$eR#UyLkZq9 -#001Zx001HY003}la4%nJZggdGZeeUMV{dJ3VQyq|FJy0bZftL1WG--drC3{U+cp$__pcy46q5m4+a8 -7iK@|*0H()?96ie5aAYf_f*kVJIDoHt2fBg>S7A@caW4Ed-kF8k86)Z@ICvp3v|nr(9all8L+&eBdc}6)NOPnlp^~#4*V1*b`98^cE~_I$JAwP -FBNbTB_ZS$!ZY(2R^r4!w#gq>nTg}%xz6e0Zg{dIaS6o+ZxnX!Dz_+zjt?k+~1!P!IS;zR1r)PmU>7$tg0J~}?%xLi(xA_5i!eGnZ)TpjBJa9)f04 -{ZO4-^WCM);?W7~?(HN -_sbuyoT{4sPYj`|DiPqA_i-~mylO@(3U)ea(N`LPl9$!9zq0G9^=)>pXd1*O)H%!>m9sC(d&I_;z-@T -#4D2MxkP`FJp|3E{b^(#;i_OmSXI%$pBWIF1zFvazm;?RO&{FyMiotAH_89Cf*zN+TSgC{E-%l7nB%p#{_h28&~4E-cLU~ -@dheCWgUSkDeD+cS+kMk?9frD&DV=NN_Dn#UY33+b>NA)Q$_FB=4?%a3dxEOKFgcs!X*Wew#Mrnqr?Dnh -_ZUw?tTIL09^3sjr#rP(R2DXP;#dxkNJzcEP11ALfWu7}E^fpo&eq$p7I_}(xH4Kf>-eR?=2blOmsv? -r!T5i}Ix{jeImZAExbiHRje@-1aN-8hSv`|8Q+i*F>sMQ}F+yw>2`7n_~ApCnCsY?ChK;|oM~(D?*dD -5SC^8XvwEQ}><_)v3K6^v`~I!A$}$6c~c4a7A{`HxuVavT3=x9?N|hqP!g96KTO!0Ncz2kI$JHk%1v~ -)?cjpWI=d}5u)Yy=SmctBKmwmLiJu1!MQXTQCStIYAt1VLmvM$HF&Em{=}=#Im~ef*&m_RKTHuCa4Uu -n-);MgsQu5{@RoHHt#xo>H{YmHHVo51J3q}_pa3^H!%MFq>zznbbIc!uJ-Zlz%fpAvW$%*o-8Wn^p`y -<<;0*9N9~a-_l1o{f+iB&aT75lN(6B_j4~>+@zpxoK$XbEi!mxtmwKU+#Fn#h8Ht#9D4!x6M -OD}wZv*P8!0{{Sr3jhEh0001RX>c!JX>N37a&BR4FJo_Q -ZDDR?b1!CcWo3G0E^v9RR!xuFHW0n*R}cgW$f&LKY(TwiHbv2*DX>j0n}tA2Bbi-_WJzk1ZIJ)oAt}m -|Y@ShQoWUj3f5hX01^@QQB!{ED#i?c&CTuI-6a#H!LV?LqBQ`2Poc%tC6mE%VGF#KW- -e)4*9d;7^kZkU?Dg&Y>i1uMx+5j|OwpEZ3ANw`?UWMMuNp+h@WbH1uRb0D1pSg_SuL2ghf*4T-wsX>c)Y}Tg@A@=RzI@N$1I6~zXoog2k9(nmFbx)vS2o8F#|;4EzlAT7^xBI9eM+4x#Lej -EQW$?1*~k2AauFBbGYR@tTlIpmm;K~5{d*D^xTiHp`eIdlcWO~SdZnZ0*?yv$B -|nSp+hc6a)3*228)#FF*vUAJjZ*m#Zql^ifi-DNfPHt5~^nF?U-PV|aff+Y6F!1=LGWVC=vyhCQ_!_h6Z-faK{d3{V1X{DpgmMz78XL8@-U^B>_U>ND&**%Xl$f)Kg -<=9Htdh_=nB7(Yor48g%}{cIkii?wCP__hvhGM7tj+oh`nb9j^RzbB{0ELwJ#JGMGI`KJg{GcKK{5IK -{-<*dz3O=#%{0MuQg|WWx6TFSXq0^w*Tm9$ayI4o|@;4A&ta(i76@6aWAK2mt$eR#U&o`{3gO005^30015U003}la4%n -JZggdGZeeUMV{dJ3VQyq|FKA(NXfAMheN|Cw+b|G*_pcC!f(?$?bC?UIWb2@`kaT_NicsvctyPvhNp4 -rh{`*d{9mi?Ayx7v+eZKp?yNhT$Z5(O1ZKT*oVmL}&*Fx3P(Z1TKGP)(Ya~(Gp$Y{9dvWL;;UONn#EZ -4%iXfSl5qf96VMsZ0CDd?VCV1;g5uF5IkayWhzVjXwA#h?=G6tdZFZ?_rQeZRci>~`-(_D)DkeQ|Ttm -y7$`?YFxPySmt2Vf5Yh_U3CiZ2p7M3R_GF26)MerdLlkBQHicV7hl*j|F?;z>s`mkk;U?!(GCd;w>bP*+ZQZO6i -cfsFgOr#;>twP|p~4XL=ZbO2Asml8pd~1zDwJbxv-yf`J78^VhU!gP)~yKyvU1>8*I2o!qRrtTk%Wv? -nHPAeYAok5Q>X(?cR&2b640kN~grpT9A!v}w6p7 -8}aTNKAifp^`B2Fcznd-z4Kr*gY7EAWjI!G3zDqu3OU~0-F4LO9K51Y -e7(P_1W#h^P-W16wq$0^}<+9)$Q0VkjSBB+9wGUBC6wmll{yOKLdg5!WRauKWz{2 -sDT(&!&hO9KQH000080Q-4XQ;EZrJn8`e0Bi&R03HAU0B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WXk~10E -^v8uQ^Ag#Fc7`-6(g!DAZ1a{QFkxVO0Cpe+FqgvIbi^|V7p_pSyc7kI|j@`l=a0}Z{G8p=bMzzFyEcsg=Faeo*x0!mFV*|iKrLq%srh4APrhcm!OL%M ->`Q{__tWTiA=PWY#jyuYA&VcK`bN9*X6!ow4ApS$T&fP0BeW< -&GIT1t4V=nJHUnspj1;M|W*a -2L|$c{U6hr;(&$@65TX02C$b#YNX8Jf3@KqJ~I?Tb^C86s`%~LgFvV6N#X~*HO -6I`oQ{AiZV-AgM8I*YLUYHh`gW4Y6uZKE!_m<6pcvtvKvzxN3d-q?QR|kY~Eww4y&F^tyutM -ouvqTK0}09h_3|%CVt9Z!`=zBySTkJ^g1^U`fmt+oQ#w8;Wrlc2bN@8SLhmN}|4>T<1QY-O00;p4c~( -;`3unU81pok=5&!@n0001RX>c!JX>N37a&BR4FJo_QZDDR?b1!LbWMz0RaCwzjZEM^(5dOZuLg!Mj!S -%IYmvf<{?GY$xNZP`&EJm@lySmttE6GWhkpJG%tF5&Yn*ESnNi(CFXI>TB^;{^_^8KE&{c0z8hj_1gE -F?kH9{#}XK%_lW#~$eX$p?1K(YYc5-?Su0U#L_$`r0eX8E$j*oxzWlW$bBRRCk0mTogU7Z?V3wxFbRW -jhFX@>~VekVK;N+eoQ4$2DgFmqHd|@s=J1pJLMCEg&;qxns*&$jb}P%4f*x2;6s0A51!(c3adc^dqbp -W{&aQq^(sf#-_OoIU0*M9TA{L#1dICM8G8W~RRcHyDM)TxtQCjFjxUKJziMJl^Aeo4pM>BdM;}3wpf` -gix!~$EA6S#4OWK?BC5cqq4eGTmsTCIidCG3j{N~eq)^nuV>1ANPw7Z|p<`Q!U;I$eHEnnN)3$hC)UH -k1z*d>Cnnk;(KNl{qriK_aeI*gI?&1_zpy(L~o)IArspn{wR93XXVcb1p{R=BUDvi9`%f+|w+LG^=Lt -#sA^3Z+k<0#x{7_R^|ixFc1StyZhL#ZuaUbKXm5(U+}P2tG-YnD28Yd%y(gxDcI*5rpqBZFQm{tEvSZ -8OIP=1b*{lkov3)0`=#0FZdq0l#}QmC*gg}*^nK=O;fp)O2KKn483`eE;u9WAZ$#!LQ_XhiFga3)EhE -e1?FhK7;j4_Qt6&?K`KbyfeIp>=rqe-Bn1NPr7VnBH@h)Prs0*a7oj|ekFe)UAq4 -%WCuuBsO(iP_H;>hKZW5F|*)?@04^4YuqFbQ{Q9v>VhQ^jS+VwAB;ldkTR)nBD;O*y&d!OYVGyJ`$)+ -9c2VzQ!>XO+abo4!5D!U{k?g+HG80gM|K@u?I0n#SHyrA#V!aK01()lQ2JVCNO1&s=%Q34`J5$3N%23 -Y5CTvwNt60}vn(dgqLmo -CV{roD9w{?LtJJSR$t90K{7GCOx4#OM2ua21AlBWEOTWOTBa3H%Knr$(4R5SyXi{nq6jq{uGg>(3;Wx6U8 -6xS^-hNn*vjwtYhLRRLf{JQ&?xu^Znuv8Wy~CwhL1bRNa=aR1;? -CuM)kB%YeWXqGCF-r^$bOKulLw2o^o$(cKXW3RE5FH(b|su^)81NhWy5Bkz2Oh|bdxv7Y_sK*VvWkm} -ZB8>%ZWF8YHAUD|xjh%9rNSVr7W6(|2WNvV1Na95S`v^2fQ@4-3CasK>(dD^F(X2J$G0+X@y;V~)fvuw#3)|5C| -3^B|@)x;`218cJ~kDf-(jZI;Yg-WY`lk%GwNVbh@s}2VRlIG&$B%i!l)8ye1r4Nr;Y(egXO&&Zk*N5= --98+Kd!#MZNb;%||pZoX6i6*PxP)h>@6aWAK2mt$eR#O~ch$~Z3ybsJFby+PX2nh5jbiU0amqSJCpLR -a0-EN4w1$!I^q<@&5e$?b*de`s>-N^C|m#R(4#-rtNN9RM)!U%NC|ulzhrw6{4N8H`}(TtE@bdjkm=% -NpEUHZ2P?_GVcVKw_j -}(znokHrWTBwpnw{+jP5Y*L5}4x8WbtyxwfGYNQbNPNs&u_D)N{dz6bXA&(w5j -V>y(rpsnwLDQy6yC6GB%-Tv$hj3=aL(6&)&X$^L7dw_WU{QSEwidlyi593aGQDshcT#S;4qvnPC%p6d -I(g^UN6o(Ia37Ho-mE@HSg!ZDzOT)lJdV6=EgW1y)sxWK}nttc5jJZBv)J|1b3PxqKMvElC~$OaQ3-L -%-B&RTg>s0ssP{n=*O}{b*|)+?A##`DcqO`q=8^nFyjyHR@-zZyLO$F!b<1=40>@5);)+!v={kk~ZVQl8w?eTb0MFC9Yqw -oH!jcjiSoIcX))2je-!EXdauC3~@ovBiZI_k66z)<$HdKI)w5yi9N&8k3s{VIc)#-IvFS2q3x?2>I>7 -pyjWlv3Vj-QJvYj)4dvcBcZ^M)@G30%%V9pqQBuDfz6he&}hEv`{QZE|!}(q^%Fzj1L&VmwXZ?a`92X -g4I=EokC2lQYLIub;YHK4G(`BaogFcsO)$SYVga0MpTciy9`P?)WjfhaiSU43rTLme9+(VNo(5eB~d6@~J7p`P{Jk&)Kx&A1M=wH>IN%ahh1!?J -2&NpM{yYrVUMh#cDYosF}%THWPSSOHaEy={hmgm4vz;cP|{P*V<$aQ}JFR-pD>2FOiJ@+shFkUUIV -~53=K@nMBOzxXIYc4K>$ND$75-)XBR=|toQZ#No^jEePS9L#33_h%VnzC$AXtDV%XPp#e0Uu$7r)*JCXDq -;@*x+Y@EcC$-Vjl^9s -Rqk(smHS=ir3br!q%65)B=e}9x35$m9f8OaySi3F+n9tC;s#_2j+cE<|!9R -iJLQ()({$y374#Z(Yqp5k`gZDF=b=&JETXJOc!C)4661dQ4Xv(vS-!f%;h{SC)=~2>ep0&5QbB_1nu! -Py+Lny}I=T7k9Ug0joixgj`=n#4>vK+^nM*<`?_(e_UH@It40h6FPK#|~L7*3<5r;+?S)SYYxLOz61* -o79gV8Y&k>j^Ta+_C>TA&;aynWC0>{OtVv#j|(M9{>Kknx+o!^l#F@OIUotxee8_&O4MBWC{{6h>7I= -#o62R=VupZp~0}#s>4-C*CLr5u+azaRaJkB?OK6)4DOff-+v9D=)?x0#1|f0`LoS!6#$~nP2;SFbHv6!+2+Kk3bYwPW-1T>Xl%9Y -|UgT5r^#jRM -24!+d-yiy#lzm9L%vQ2P0Sg5GbT@;J`R)fitv4T7JDlvANu*NFJaJyeak}Ivs5!i8!jOayKhhvx@U2P -?Os#U)M#>#r$&6j71zU+(cK0?b8bmti35JK;gQ+?Qsg|Avh7&(q}~zozM^a7(nl(0{oOcR^P5dcE+&+ -4;&I_|B!DKW|km;uCoRv0?!Nf`8Z$@thr=K*y_A%8c;K!z>EW-?F1%(v&9O>A0Z0I)`z*UnE^vlYb++ -=m3XL!CArJM_b5(?B*DC@$({-=GdknRV!AkB%T -n5S=sPxi9|SS>)8?*#nm?9`a7&KHrQPkxR^tNS04EKOBcAE(czA@9>?yP6pWEC>{ci&14s*OGRh&?JF87>VhBfWp_m>hF1qa{nzAPXaDco6WyLZqnBrl%J -UfQQnF)Q*wDQdK9Ic9FYPBx%paLC$1mjAxR$tApyVn6@0R)Pt!naG=B+s<@`>4D3l+?pIJ9U1Db4OFY -gk)$fL!ijQZ5IpTj*e3OFunuTWz04yCF>OKpnkw8k<9{>Ti*>lg`_5>Nls_!cZ&?t -FeS9FlTLw;CiCAiadCf57`5xx2vq`pWOS>p&V3dmAV%rx{JDB~=Dq}NT|ZCzS%1S^g}ZQOk%xF~7YjB -k^CkJ6bptvliOQwzGueY%CBQvYG+vyT)7VrGkCSW~8!bE@hJrVP`Ir&K|339iN!1%8XF?c=^+BiKsKL -_eg6di4uE2FPt6AP}wmJeyro8$B~6kJ7nx6l%>X&8Z1^#dVKo+ -6lMy*W>hxy4Cy@zu37eTml7DvYj0ld0UUF@V5vaMf&`8RT4&1T;z@Kkr0i&TYij=u7-pAOeMgxtOu%e -g)Pl#+drBic#WgRPgGqr%iQ7<`cYK -NYXl@!iPlG(ICf51qQ&W3$>7<~q08hvVWBmvst1~j7-^C)uez~VXO#4m<42854Y-=$r5D=gpAW5=?z< -=Xmqlt^Q6&w00kJxGhZhgI44PNQ-$;gR15-B2*-u7!YRcLHX^QMn$)>7Zdi9u3;yCwfP-QJPJ> -lYL!@E}_5on08{Q`#6?8Q0|?7YGbdDwOH4;u;iBa!V_Bp^f|hjb2%IByELj&^{N;4KBGzR54sL3%Z3_ -ag=)Cb^C)$q{M1yw3`#gHH+-{zV4JIdgR)FQGIxG?0xIav&OMIzUWaUBS&|}4`Q}y?pJ6F_MWWQ~i(6 -G;Vb`venumyBq;kqJ$$BdH3Dq;8OWyK1$%5XfB?HGrl_c+Q*hx|tShO={Ge-t!fNhA=u7lCf~KBrBx|_Y-}sJf#Np^v%euqDQIx!M_6x>~+ -#%q>upbEX6^Q5KhFb+Rs1dTtBsQ#|f3Y3z>LaVa0Rz-Yawm`qC5cXbEz`vgRgvR+5bh2v-Hn|Y=FTr~ -k3uz+vWj)2R+VUN+#JCVl2O{KussZRIV7|Y|FK4CFo!B^9iQA#pd=nT8sG)pRi+y^)8J~|!LHn;q*JB -_%!;xD#)0L~gD~@$Reyy51rCMX3Ir+fBZ1M<`toYn!ZqLrO$K(5p?m4ups9mW2yK--tB{zNHMm11zs~ -X<2%xsxCR@V{-e97}euyiEB^fU1OZk>M_@n}0JLVN87g>62XvEtR*!;$lpa@92+wxPT8@&Cse?iYZP> -p!0gM%U?uIh72!6ODGBPfdtSMm9aF24mfR}@ax=Fmf>p>zgzyHjd`xO -8%&eDjr|;khkzNd^#YcUtnETeM^+{M<<*(y4fn45a~?P)4&Sud40+Z@kGX{;R`5DB=pIA4m;0fP!%e -dRm7#^DuBPd`#r;+d5dWgCjvdmL88JhM)&a4V*HuT*F{q*|>DU^me8IsBH5FG*uTVPAiq?c)`}he&83 -BMmh9m%^?T)@k#>-Mf_Gk(_fhR;Lcwqa9=rn>{Z4l2h!&LdCk~C+GKn_kmEwmm;*QEz+e< -uu#D^$m&wt0QcmERKF=GTW19&P#FI#}Eh-BKiVEhZ?dMys&-D}_qxe{ZzATy;l{`kG@ZyX)2eMiLdHOyy6pox!8#B+V) -$Nkc@DAzPX*Xs^JBjrBMf3_Mf#iC`ZpJ3ccX>Khj9Qf2yQ8|#}{ALJbFcox?Qs^v<5{(W-%W6!SB}LP|^y9Wvo%`O8o_AoBBrjlzt}DbU|%~`nZ&G-%j --X(_oT!DJaz|f5Z{^tsz5#a-jXbD;QP2RrXEP!@iLO-fJWunnsvyaa-UMmv!7Ux;3|#-L{deHILnT-= -ICL6C3jc+IMq%!3WG~4Yp@az4QZCVVd@cK{C()g2iI@a>pZjp{nN!A;1R6A7K4|I)3bA -Z4d}Oxa#bCaZR_zzas2R$!VxUJ~-@D4f5d%hTDDs{p9_qstD4rkEsS1SAcBYD>s4u)}Thh8%%P#WjLu -RQ5+H9Tc-50mkD=_|X*^eG2)pFd=gZUBWKZN%C-b%M9h5JEhEvK7ESTcQRr==#Phk@y6lZ$Is}N;m5* -yAeIb!+y#7PWa7Waj(;-tz`W+aC%3247Y16=+Cun;{;F>b}q>qEBrC>R|wJZk -u%i5QpZBIP*nt{Y~8k7ae8uc4Vt4{U>N3PyW3_b(D;O!ij#`pddF)*n4yqsziIp3Ou1|NMXJtWpAPSQ -@|{RBa>uMzU2G+IJ7w_(zz2l&)4-1j`b*xdwq-$yP}z^358!9|wmbqA9NmM;@O+;Styw=Gp7o>HGvStY7+&*FqcPO|MC9TYt(b0=?I0_#$7i@R6)Xy4ps$xFNI) -#dlYxS_SEeq^^>pnSpvLPkP6pRwZ?_E|^gbjnV!&sNIn%XBP}X_B5u4E-Jk3Ul`+GZBipy_T?zprD*P -_n-M63+{Q3w96-f-1<<#I~<{~=lMhaCXb51hi|k*0e#a;)dhjpY=7S7V@3AoUEO_Qz?5UQW%OA?4nta -?f9W&hMI~?XD;8z2c>AA1Bk(=kNH1q@c4V}fp48s4U}HeyJdWW1(WuUI+Ou(G -XkJlYICLmd5H=^_H`II*v{8&4o%uv3P~bOKR=hH;(nsSEiYV|)AF>x52dY?&Gw>rFO~u`eV$grZLd5s -~G#P!6nfj+s$}3jfzXqLfyDy{QfFTz~0bsl=B2M?RLDrHsN$H>5T7qE}aHA26bRd@>Foa_V8vy -BSbT>l`K;Txc0v^;0B`Q0Rc%ibu(zN6HWxDlMi-YV=%_6F#?Ag}`6go4@B&k8Ea#eD_n~okKafMV}r$ -+(uDUgXC8klxzM=Ye8pr@OHYy>CR-rE~>uomgDi|x}Dp6}Rg1W76$rkeJ4OxE=d)UbWL5*Dcmn! -thI?Dlxg2ku|3w0-ATYmyi*@coWW{{OLXqRHjwdL1pfsa^|sT)kND3mr+S8*MOci@=aVr>dmi~tklYY -85n*c!CgnMN7tjVVlLx3OXpN+2)!qPvbG=rnT)27^Rsj212FE#TN_kQ}i}I0|pCoYSVGWgj0JRev==&IOw5?agwb$w<?p2mO0TF4%Q?cgmrouksb--^?;)>H9WJrHxUL1B9<02{^(w -G6-Klr>`q|H~&QiKs@8Tu+lLxam&oexe)D5X@SqOsT?q%F^xwUjCNFM1!0Y8L$iW&)>?y2fb0{0f#Wu -U5zblYQr%=^Ngq4p=mc@IISFySJB>JxCjy;x8~)A5Pkz(m($0CQ+Q_D)nE -+o1-d#}tcR4LDlkXjOL=d22q4pTh21wnrobtngn~nL}Y@C}3VuU9YJJEsGCyeK2~)_7Ga>PMN$ab -xJ0nQNIWP&z6b7TJ-O_O3f_Z^9}b_YRRfFT_-m6%Cb(C?M^3NY(J8!cu0bP@G!DbUdKQXo`p?FROl{yNom6@U)*MXq`izQT)k3#ZI|rxdtSEm{0T-}oOezW}26hlx7Br8 -m5{u!P1S0iV!cD#8FBeM!!qVYOP11foi%dJu9f+B3VUS -nv#&Jq_@Cc)p*KsTL> -o#jDtTJz-A`1d|a)br2W*`ttR=Q>i$V&@8Ukt#mp_IO2IXAfiF#NkmStx)Rf*4XK~Hw!BB2-&JWR?T# -hvzv_-+I(TT6p{3;!GO9KQH000080Q-4XQ{z~G$36@I0IM$m03HAU0B~t=FJEbHbY*gGVQepBZ*6U1Z -e(*WY-w|JE^v9}8eMPOIP%@Ug3zLL-Rfo$3z+A9D%58f*(x$RVubYb9y%O!vUFeM^T-nopkb -qt>$Kw0nE-qfae;-3(cUz{JvA~A@OCeej!{EqCzHRH8Z9$$E?G10PFYMwno)#i2clfVEey3M|QHwF;B -kO5JdukDWrjq>~^eFgdKfff-}P{aD$R$K!a -DFNDDfp_oqN(@O^3<&Mm(cWUnzN-1A?5_9Gb2Q44%dJJ|>1L>D4;FeWc+bjq=({0$Q;iFbLO~(5#Xr) -nJPviw`-5P#TCMml^pf)xfzJu?=Nme@o-hC3nNU|(1hzGml9agrZ#e4>uq^x6&8|(SYRmMA_(_uF6~A -GmKQK~xAf(-V0=*DQwk^6RX?KL(^@5TeXLTj>l_T-tz<|43Ue{#9NY{2sS{11VfpOvEN3;#tx!il24Cvc)8cBI$fK*)^%?(tOojc!Gb!}b7|WVB%-_b)KAQmZ>dVEHePp8p1$mygi-87o!@y6@DWu;SEF= -u&9a{HZ9e8A%kDiW;V3}~@N@*Z_A!JJ^T6_pjJ6DHp`tzacY#DT* -%gyI{x-3Ku+wLM5U0TwKqggom+yC;_>!d^K-726S7Ri)WTAqwm=a`uA!+BR%7LtM@wLL;2aM}*WtDE7 -|UWIb6qvz9g4p(>~piR|$rJ#an&0M00QW$_EGB@4F>tnP|L$=*_TqW7p|U(Eh6XMgDW7Eky->|5&JhW -#lIz`nJ<@58>OPQbb_9LfAi{MU2->lR9gZ_Rvot~%-UPCFrF%6_0V`cr>^q;#N??CW+zYd98z&YDy$Y -1&@7E=lH;G<1D`HV+GV2%bGfR%e4`Vs>xY7fEE}>FR#l)b~VZR(BmQ@PX{`*fWS+>hQCZIOL%#Q_W#co*Lc35N)liy@ce!jP7tYfwUT5+WPDG5cbN$DhtYlW9U(K{R)IoaMGS`H3Vq~ktGc7m}#8UDgnCdp!^rYlZWR~UoS#}j&NoBcU4Spy2N4S0vUbLm-;L(Q|L?Ra$4*6X-VVE@bQFH_vqVL=YQ;NGYwzgnOVEPbW@8OnZU$agW!ML&0p&F@H#j%r4WuS@ -BjL4~O1fCV2HkVeQMun2(C|U=MMpjA{+o7C&~$?1X5WH;LNBE9y%|5`QED~x`sFwQD8lAH->Sn>!?U| -hVZJ@_{{Xy0BLZIE-j?^Yxn`A4QOJxg{fS;4bA>OeppP%2Eqv%j^OM$*efWzsP1bXa=dczHNP_i)XhJ -<$DFoz0!4=`ZAt#gD6_fvGe4uTKp-C4Eh>AV@htc>F+RH0tH_+aX*1~rdDR->~rhB_+3;6SZOF=m(%O -4q~$y+l|8apCIXzVa>WQGY~gZ=bz!y8uRi6>6ag|`Q`y)Iq3WFyAhlH@g17zIeJPrbQH7Q3DaFEYFd5 -`ar0f9Y(bmo^#ZX3}8&H>KloVH4LmNhZB}0?b{8WdoIcnne0|C>p9P0L7zK={Xs4rOq%xHdziOsVH3* -E&VBqoELbi;V7{vbM~#4)-y;fctb#3nZvmOKnQG?=LpkOGKq@zg8i~L23i -k6~~SX*+Y4X>cuGp#vcrkXt^Gl9j`&gnExE&RkQ0rQ^hB?tGEh@i28ah> -K8m7FBzs;}IxE+CrXWT0!ed#${s+G`!iLV#&mMJ5qPCH@MOyou3E`vrs*z^%?B>l!-8)puf+9NatZwFd&>r5Re8c_}5!^YLG>p%NJ+7+pg#W4UwFl=uasfMlw{=C`YS``$SKnYinR28irDOkH -^z>A*WE33fSIz-Gy;v^d4=`AGPz-uS5-nZPTh2d%GUr=GFOXIgE6toIh%AH`9R8`9hP*xA0y}Ip$0e% -y+u+Kg(qvgicXe3G|$|p>lVrqC-aABgdy-28h-A@z&_N2)%L|ixH1!F4-@9y)81&s4h0 -rf>Zb1MgFqg>9V*EB`ZuU4~Vb_2Fbbr)&mt$KP#zH|_CgTRj&$SV~T5w*-K$GM_5Xmg`1ijk`vA5E3v -ei{lYs`m*+=&HlVO#a8j6a<*d2V-E*#>_IIJWDZ)JmlDHqK?e?0M5d=k}r?KgA8je|IAu((sc!lST1NlJuF(a3Fx15%gB;B&aUUf7rYCF;5D0H;Q7(n -FIa|8>cyVq=3}}i}1|gEYA`UG-3wXL$2lYKXEZM2Q&S0Bd{xLAKqAO$-RCcddjQnNn^TdWN=Pyp|WF8 -JYvP!w6_XELMe?;G&DB^B_@6Esac~eG=m1}$GLZykupw^tp$&ce!VYotkdkVj)HU0nrN(~#z{t -1snQzX3c}3xNQS%vEnh#aNkWqRQ_Gta;%aJQ$Q=Z*vxNMO+!@ZY%;N<;39_nkq>h^On<=FUIvK+DuuGSf50yD-~<-~FB3C5#^;6R^` -OKQOJZmmq~N-gVm51887)phN8##|a&`xuPnBLKx)8?GK0sbgu8f`zRF1PaIEXg)>7AS3DHYxs1)7))r -v^x~BWZ{2Uz)s}yg)&0yt0Ge2V>}j;F}TuYtUbtF{!ZUE7gK3bPP~!9dK%I8-#)#IQ?_M2)+i076A1O -8p(4j_vflVTw7mLPj?Z*qcJIm&_A)l8T(9#B)|-^;00Z<(o`+kzZRu`J0fz%g5}zwB=hYdT6KYQ)W~b?X2v#B500F$&u~n#7c+8_$kk=j)xm4f+b)7HR=yG -j>mF;ZSlb{_X=aXa}f*4~+)O(0&kmpCp&tE21Wact)E`LReuxQb`il8|8k2r2A#zNfm&GCV;D}wHZ6G -vFaw$|_6A>0^ip+z${h>HOJ#TJ2LH(Uf(+&+AsZ`nvO^DP@~H?M|cCrz30Urc!JX>N37a&BR4FJo_QZDDR?b1!pcVRB<=E^v9RSX*z~HWYsMuOL(ukXBph -u+SFN7kF>PoJeWa~G4oDgk>x9SnPzuPtP7czboH5{L+Krp3aGp|tCcGIlDXZs1T -Hj_AQgQ|KT#lB75hCEYcJx&{(Oo>vM*hZIa8>`F<$ToE}r66n(9h{fHkY5cX^dvyuN`L%(J&wx0i3mW -PW>cayp+631JT_RtCX$YEPvsqX5mIsp>S!47s04fhf&XpT7}MF^=XzpSGB5p$u-N0N|%)$jrb;n)^tSR83N#;bjUR -ntDGuDjJ2CW?%DcxuH%{}v10HMbkC+lX<4tR3JyXZ!bOKbH8r4Kz}9j!8lkI9l9|uT6b|Lwti@S|lCv -xUGHcA7QMuZ=V~*YTcnEdQyeNt+4*?g;M!S+bknnFXb02dj>=hd`|iBQWU^$25ll-!O;*V~B-ydNoJGGv86m(=iVcKY@1P!C -+klfZlRL2!MV`Btq@^kugUlQc1vl`yd!6BpU3tGZ)C<+hDK6KzPOCfJZ -HqR^Hua8FVWp0z7X99?k09psNY;We^VSV0KrH>fj!NYW^&WtkO#0cxlhO+`RJ(_54OFqxQOOglA5`7-XFkORGV>;crL9)9)V+sZ`uHbCPK2#6N$ -eHoG+o6OA>`McZaIq=kdN_uZry49aKRRQcoeZ -D2jyM#fd?11i+e$cAtWTAh1W}2lf*=_}2sPp^oFq-gHkXMm2pw$# -X?w%LH>>ATBJOeRzFC>{q2Z>w}xFHzO_OfTo_dK?;1L^Vc`0^P4HYqjw-~B3aI0(EXXX^Nyb8wn6_0S -2j;fCKHhHF^T#a>K|+R@P6O(;aK)O3>}F)9zzuSdCiDxHJx6iwZ@qX+N#K_y|6>vwEkbk0>mxdz-fuY -aqlV0PHMP;JS^NcE$jWR5s3Z}`F)ZZ36FeFANm+r3`R9)B5w7JguHM|VMPKG%+$CkMPssvCcu#N&k&z -ZpX$*pgOyOm3-V+47WIF&J-i`D4azNYNWThy#piZk?A$2^ixoZ^Zb?w<{E2ngCIiPhAwP9%S|H>V`Tk -`;SDq34PoL>KwmjB>fT3i=b*;HSHd?w2CmnMobN@9=@gw%FIIy9A_)XN~=!1yhFeCW;j>LWcUXUM>B> -ku0A`VoydahxrPiPZ8;=+-{k9O^tw9XM&8yd$nee9_{9wD%gPT{fP@7)=Kf`igSXTRAgo1)}d+zsMZa -Gc{$h55u$yE~jHKRI_XIq-icQ)A#|+HUe#0X*%Pz?3!%`bjeS4^T@31QY-O00;p4c~(>4LP_-V2><|q -9{>Oz0001RX>c!JX>N37a&BR4FJo_QZDDR?b1!pfZ+9+mdA(U}kK4Er{_bBvxHv>gWg_WEw+e9G+ilZ -JH^FZ1?t`(AX`8myl|_Y;yzw3S-#arTMM{=^DNuBAh$V{i!g+aSIF}zPr5m=ckCWUy7HYT4%bk0zM6) -mQ)nu#HfvKvggsvsCAAc4_D{J$uIaNUBzPxXw7LC#g`;ga7!tTXEZa%kFArtnoYI0SIB4M;JG4xel@$ -4wejncdl>t}&l@V3c|+A&>=QkD5y6#1_*D}iH3nwKCFxbk(8^OdM&wpNFOC^wG!#=Kr4sTH4trNC --C;+;p0QX9)G&IxqW;bqddq)Y(yg*#a}`24{&0guUb51!gjLBHgYT4qRC`=%(W^HvTVkf(P8-^uh)*< -i_ROFskW)wW->w7NX=5`BM!Vj&{~v{J+dsGOeX6>)HQqDs~3CZ$o|Ij34_1sbV|RtgXQH`>4QLBnOK1 -&rb@|_W(TFg&n1|pDCCA2m&lq=i1zayHj!E^2XmF|;qGPz2Cs@-lnpzGykL7NHlVKI&pXld(AK~VPU3 -W7?7K><9a|_0j{1rH#Y%j~IOlF#zAHhkOv^8A4!;mCM_}?12WR^=*%VHa|**kU*rd6qH^j6zcfMt%YRtag5lukz7okfC=V(g#HfNs-32DS2AddM)GT~0eFvc-9@wevFxW`P(rq!j%+3v% -aR_YUk2+H<$(6R=j*RWnoq1XG&MR$hj5D`&4RKL=2sLYZ^1JSgYkbg3ANovi+=`CSLsaL9&-Lif6UYh -^@VeF!VBbVvfC!_s))ZGKE<%qVLGjI&Of!G@4`-MWSDf#`1O|H!%@NhJ@>yRx+CzCMA8G4`P-lt)aKL -hi)+lo7Lt{+*i|hT9Vas;7hy^=z4v0^T|VzA-EzxD8XE(gtF9^VJkzOeF{ -K+6bgYT8qS(+Y6!Q8M6xD1yqd4Y+X)yG{vv*@H0rVw`5Ad}eiqZTXp`0{^2Wt)l@~tw`E)2avM@jAvNjfY&N7^5( -=|a*$r14ns*k@W*hq7RjTt>H-aN4s8UZNsXBC4y3C73Y#q90K -MuR@E>yIgm@KfS(TS7@4*_j%4bKvK&Sg`KrGOTFjcWPgt6Iiv;^g2>D3|^$8h&&_Zzbe$Z36I=}?6fmRiI*%CE}Ew4 -t>^cAxa(9@2+;w~v)nQp=ZLU>@)%3bJ|Bio61rBn7Bis9XzATxyy7pWbh2T%yOZ~!Eo^|XrOe-lIU50 -PRwusFM6MEFlk45|45hD2fl1X}ePQv&)3g{a}Xf(7k%WC^RDcS!qdUYU?cLdh}4JMJ(s?@wu@n23fgR --BLNVgF>qDFTkjY71GvnX~VpD`of}{}<$FWVysabC;pnjlbbb7sAoH)f%i%rkkJLZThi{(d{*}snc(h -$YHKASWK~1nLc5DPk*}q@&4m4_tW0?(2Jc(_>sEKsNeg6aY9`OQ8B;(_S?CeH{3fKbHnIr;zlC!$#R{ -`E^Lfjy!|K#GE7o1<67xWbj4&{qtUZnU_7jhJ)Ii~MKCowX#m{}7ow@@27)$NeqGC|VRxX-y=v~z{kK -{xJ=$C1m8e_)1C&36{I87`R90u86I{5#`XzP)XsMduF~>XL(9LHMsnrVrdu$A<>7I#02GSm`tEfM(;j --93@kF!67p!SooBxY7M-c6QVp`KbL3ygk7!{%9;QY^$f~+?8K!XQfHg+pcVF%*ARGOM*;uW5=4qwA+N --V&^_x5&ARQ7%J2&DC88I8R_b1?1!=Ev_a4c+cn>&UDYn=7EmW{TBeTi ->%4ybt*l0IYrOyOox6&S)MTJV?0w!BWaAyb`byB17gHCm#4zDv>S1x=$)1MB{!yMrLKV)`;Nt_ch|Udhz|5+4 -SV7Ek_Q2K3Ob@Zmy1i7|CAz+-P-5ilD~^2=9V5Z*(t^CsGKhn*k2sK86zSTyYyeZS+B%VN_XUG$`2)T -PFc;wAIltoyTFEyf5PpWB7FzsVB(-pCU;Dd;qmi^{BDXrGV -=y{N-F_SbXCNh--gIA~*;_mfEMaFWJ-TT6XjZX1oDK-w>}5dlcUYd``WTIw#tbOFr^UU7I|Bhuf&*&U}_#ur;l1oJ2+i|6*Ffo69|ICgvhg2q0F42B)Lr -+g;v*aR0H~bAq?Aj%ohi*JPxL4YuYre+6L%x}6UZtN{4^v?Ts@JwtJm*M$U58G)UaHjJ;S4Ky&gRP)h>@6aWA -K2mt$eR#Pt{mhhPb0071f001KZ003}la4%nJZggdGZeeUMV{dJ3VQyq|FLiEdZgX^DY-}!Yd7W3=j@v -d6efL)k1OzJsGLdH^%F8wnLDQnZHU)~sLgq>%n}|fJBxSD~usx;=YsNN(8qB#hm38;xjKc1Bq!%^IG6;m#gwt*Fc(opY(A$ay9H#!T`W= -c+$uLOJM2D4n%HJ8Ppec!#lrlRq$Ta5=aMkv`U{vuw4OyI(`%tV>#R%YB}S}FUQNn^Qf4OuO4 -%;Dkl{r!iBhcy*^$+5BIa1{P1$wXCgxA;M8rPtIb@Il_Ap3y=pG}b7sc}0Qc3JyT9|wcRV$0R%}B1ZRjWdjR9u&9Mt>JURJfSU#!(A3qyxv;5$z|x7H6 -IXS+?D5HhIO3fzPrJvM*ZPeP!RJwat~TFLlLAoJi~b?FlD7RYWnWo{)a|D5$ -x@0@Elo=R7lj-%rH%4ky=jiCk1yvQ<;qqH)JS)Wq|pf$bFW_CLda4dK7yEvg;t;rf8Kp>~ED^|e*>qM -ww!v%t+4D3u0kfu+#3Z#baJ*|9FLLeD7{%~M+DNc+&(u;3d0jr?};CQ9d@IkWq~Y>?Jta5;zv?d=mKK`#peh5UaeW_X^wTYY-e@$=x#X@0Uwt=mQGEmIO}H`^%Dc$jfm|1zi$~kLoAW!q{@d%_GITF=r -IY9MQs*;rqzh14Ys!wRJxK$3cJ@6szMGMIX}x)))u -@E+d&py>rsg>(xd3O -VaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZfXk}$=E^v9RSZ#0HHW2>qU%|O3Dg(CCeHb5k9gvYcCBgbr=MB?U1cT6>g0g06NrY4l2ygsPyzAiDyWXEeyEZ0(%o;f&lHnY)*pdyB8j>rAv*0Qn`w1Ki*%x1HSQ>{Z(d1y8cg2m -;G;Lm(McmLnoJw1j{Rc+Z0y^bo7z3dbjm{b!RTQ#&GY7i9zozI*Ek7Y=A$1$rGO;0yqr8Lwm9Zs&-pf -oWU{IuRtVJw4Oqq&U}f`*e#%ec)Ux3iXO^GFqtx>I%$cA)kogiKC8poWdKWH1!{fi?uN;+-^SK2pm{U -y`jct-idtaBCPkv_>vV%9*?XalxqpZ7%#-2IfMj2Gg}hO0KVp*ZD90BwmhZe}yjg3$IA0!JqHG#^m=N -97rJW&0w5io)BLzkkj!vLD6JJ`jEhb)@;Y>j&jcxjcwiLK6Kjh7rh?|dD$wtA-ADW+>ti}=MmjP#nue -e70L7b1Y)HU5XJ%H@D$*71-Q?y(gX>AW9*Yo@$Un;tngeBV)N`N%$pFFR`x~D+#8~XHHK7O2L4O~UoX -h&iu@)8T+V_}LhO2uh14?Y{D8=M4rkxh<9vCvvT`0+VX_~Qut1t(?sIiaZ7?!-R2B0n;F@;a6jJ9^r) -=Fx$XnoTk?Q3%@ww{b0V4#1cJ*f9g9|;GA|A~ExvEW!wr=SS ->7{2)jnIC2b6;Ep9yUhV10Qe*edKQFVhmMfu)`)0dv1>q5bK{pP#Y!^T8GV!HW(fL7xkSni+b};auFX -Je-TwsyY|KHh3RHyxDcx?#=dm_n?vS-a1=fTZeE{UEWh=v1gZ(m5?jEw%B??&BWIsj_Q31zX_oP8GE#+GXDX{zUD4PJWhKs<)6@7x#s44v -THW0MgJ02$IT;l3kmcVwjCig5yao=rnVqkvgrpr=t-3Ytjv0F9+H!&VeO12|NCpOgX{%M0LXT21^o-a -~bA0Z0pnpzuB4X7VGJq>mIPEUiOL%Ywi}Rhd8=~d&7rx7Fd#VYuE7=UQauugcvCUX#!*e#5Ma46SfEq -V(EzywTwF)Y!)U5H)a-t2{Y>tV88iUCocI$9mfq}l;i_dB17NepvZJU%0?^)RSsj-6j#qSoaVxz!46G -MAMBd56SG|s+5=>_R1tXl#w@(Apgp67Ov6RB)I=kJQB%EH;x-k2>q2>yx -|F#$4}#WpMhypS={UK9~Xr+qiTcnjZ4y?q`XNJ*hgWqO3;zFcT? -qagiDu?fs3iwII>V`e5!4`;3C0#*!6AkOCK925%ztew2vSD5pc-%f>g5I`!^ha%i6GYS -y8i`GO9KQH000080Q-4XQ`2<~E;|7L09pe804D$d0B~t=FJEbHbY*gGVQepCX>)XPX<~JBX>V?GFJE7 -2ZfSI1UoLQYl~T=W!!QiK`zb_sSPNYqpohU=JM=W@20I2PQCd;QZe?eoZ$BkzHjfxe4LQj2_oGj#*)? -#YQ)XPX<~JBX>V -?GFJfVHWiD`eg_PZH+b|4;@BI`+Ze|0CA0R-7rTYPTv0@l@Z_pNqHPP1X+ppiBE5p@=fQ)xO3cRIqcv}FpQC*mE&}e9Vn7+2W@4swg-y?Wg$Q -*l|W1XC)+OPjU7mk2hl6~*^Zt!M;CG|A~Cv5+(>;rkp?yBg5Qmz`>LVgYG*DruAw4*kv(a{;4 -$e?vDXW+`z-lP2oP)h>@6aWAK2mt$eR#T7SymDOu00932001Ze003}la4%nJZggdGZeeUMWNCABa%p0 -9bZKvHb1!Lbb97;BY-MCFaCv=_O=`n15QX6n6U$E2N&qQyv{2k%0Hr>(h-!S2-YRA# -06+4;8u?Tvm#)GbRr1!GwQ&3ZF?7nk~?t{O(0a$EY}iQ=-lAPL`p2Hlu?R$I8u^}Yq_Q0L^Ayj9v*KX -6hry^Ls0ndqhz~o3>|wp^W4g08!`u~RgcLMSLMlq-HFJaVy{Wbb^=|9ygN&KsPmOckBo4-u&h_9Kxqu -N3~25O?k*4tT)OhVQfXGrEVGC5qz>1QX%FqFjgmS#GyjMKw9c8ib%vPNzJT-WwE6>3O9KQH000080Q- -4XQx<_rxOWBs0Nxb<03-ka0B~t=FJEbHbY*gGVQepCX>)XPX<~JBX>V?GFLPvRb963ndDU1^i`+I4e& -1gq91338xXnY-vT%jANz2jn8k&?ogy3lH*#_jx(9=vF7L?@dX -8ojis6iRnu*J~jgmF>6!xBG`3zTt&_9;{fh03Qoz&32EtQJPm3r-v`PPIgP%L{q}YtmVZuKR~{xubng -S%0B(sJY3pbgXSgI+Rrg4xnF}XK9 -`{yoNkquU4y~;%M--?7JeukuC*syhm9MlsHm+08;ivXxo+<_HWIw!AYTWlBTK_OfI8h` -U(>P%7p_GQ40zXcg0v?Z6m106NDtlp6-? -=y$r!Yg5qmUZqB|xf@;5JkH4RBWTQZzGX!Vt^&!U?nb>&BLNkmjmeYfkx7Q)!uCY(e`B?{l6LSVx -x?YdCZCD|K>>WwHMKZEsHc7W}dTyAlJE5e__$}K>S(&ZI?7afpHFm;qYuEGQD!th -Y@YH${?E<6gvmbcInUfz_`pm+m#UTk9wjR%WZdE?J!Fe;caEv&o*{_Kztz&H3@i(+Ch@C*|O?dmLAVL -ZrPB3Rsm!PGLoo>=_p2*ouX&$SI7MA5@wLMgPBY@uDHslKJ$Rfg4P&%VK8o0vUvBLlA7uo&I9Vt>d`1 -<-QAn?`D)ro^;jz`IJI@f{Q#31GKY4JL*$d5oQfh~cXn=*0d+NIk@%lXx#0#DXVJ%!&~Q5k-|S!noxa -3bf8|r0|rHwAk}QQYL1}feQ7vI%Vv+G>uR*j36*xDameSca3rCWKV>XTN=wvIc=jR8r%Ex>es8^|GLU -hz8;f&QF%2+^~pA=!T?6)D4jzB;2NkHsS65nhh-(Z)5I<{4B6|EFpAR1*F49lN?cO=;O2@qLZ9-2aw? -r>+GUXH8>^l+Q!N2X}C_^2t0`5SRR3_yUD|pfb)V;-czZ4vHZtq|?D+QA1#2+2v9A9F -3tW&ENWs*s%e7$-;iDGb%E2VIf%Km~}C8Bp`6UK69Lej-(NH`=O=GL2Xzfu~I93O7PFiX+4$DA}i1o0 -#xEnj%W(m>?=`Fd2CoMl67oAC_$X^ -7=*xRblBise2|^vvRmF!)yE#uC;4rN$-KHKW#e{jMy#(?mTYErTy)|$>(9et=RI>C$}8AxR4o*w(>zx -2jj5evZUpBk*Ddx`R3<q&kXQs@(XCnkeE@Lrc4kGoFt~lwZ>3I%v|HNFIqjQh}#rON4U&*G%O14Ai -2e!+W*$u841HE>vo>&R_P}~?>f$Z82x!pou|V=OwLr#|BxOZ`H-Q72L0JRb?BJ9!OPi4yk{;n13RXgo -;t*U0D=lz=p#E@*1Z}hI@6~WpvdB71*5p3dzQIYMp78qh)#931}}kb6BrOP8lo!UKiBDp(1i;AK*Moi -j0rYVpicz6KO;5yIo(@%f}-I(0(1LQ`}ws@iJ;>Q%YE!|Pyn&08xngt}E?;g7F?+LQb>E{JQQ|w -~$TGp^swlreg1);g0^!UV~vf6bw#eTnKcO|Hz6Xv(M`)p|{1p0B#sWoDYux9pKj5k1Zu{`Tcfm}=<#L -EMW7sRU_H)cY+Pwu>w+>?xprjT`u_dTq!Ap~^g#JBTVh+^q?Z@6aWAK2mt$eR#N~_vuN4@003JA001Na003}la4%nJZggdGZeeUMWNCABa%p09bZKvHb1!#j -Wo2wGaCvo8!H(20488X&tU0w($p?%C5(w=HAp}T!tlY+(iAa-@xVx0E$4R%{mYtc?)bV@qdw%JIA7C` -2YM#hxAJmB5FT5CcQ4<>*!3?7h3>`Y}Oo!I|rN-evMbU3i*Kv -!%I8A?EhM=ZTpJCMvaUspe#UspJa^vwRNsO7)x=EOvB@q$S%bTar2x$8+`y&MNxDXV} -#tiXDe?htInD3bUJ;fF;FDO5fCyp?5S0e8-HF+1nnt;^9+ti1G*UmI%2@o2pAW5OsjyFgeFkOc819%y -GX0`&V^G`pY^P!93+*IY+u2<4{zLACVyf2;!#K7E5T-MwmnrAF}>IbvH&j~gAcRJtsxLk!HMFHYkz~F -kztCW)^ME_2ePw74)WoF+J}uJ(C38x%2V0)zD7H>c&KGvtMUuy|)>fWAxQKdX3H~rWOAHP)h>@6aWAK2mt$eR#QViOU@tw0015U00 -18V003}la4%nJZggdGZeeUMX>Md?crRaHX>MtBUtcb8c~eqS^2|#~tx(9!D@iR%OfJdH&r?XwPf6ucQ -c~gq08mQ<1QY-O00;p4c~(>2N#Fa(6#xK!L;wIF0001RX>c!JX>N37a&BR4FKKRMWq2=RZ)|L3V{~tF -E^v9}TitWxwswE_UxA@#?@EnKC)>I0j9T4h>+EiJW|Qp1&dr1Ckzo>&7*m8okd}3~d;j)(J^&CPDSO* -I^r3EVR-%A|gL8i86JU}gCs!LKwz8b>5arz^6tF^+>;cD60DlU6v|qbe(4WX=! -v@XUa^b=}7_5h5yJS3tbuaXw_CgM3??S(Wp9lQ0n!%sMhYQ_CM6heQN8nEbh{stWEz}t$ -uHnY24GtZJDa3Rw{i{s;#P;EBJh}s`XarT|)$f9r{;U!c6|HIqZN1_sir&z~6-|OL|}bvjO>bCx_nSou4~85rrDN%hON$o --K!Z4by6^H&d&h*QNo`!GCT}PEK;Q63eY@KOop -Tiuqm45a$;1unt{6p5LL|*VN0_0E@qi4lXc?86*^{2yQAHBY9XDN#I&0PE2iNVKt;2k{H8T;+IaQoeIgVby(>u(6V90o^DmM(fPE8 -v8#^*jP2mpG5t7--iX20*%8kJr#w{FCB!Tt#rJPsf9EiODzf%GBjfqs>FesF*B4N7YLXAKpqNj#9WRW -Ife8H}jXNg;!KT#N3?$ZUW&#$d4>Pf<<=VQ_Fyl$#i`kV$gW~hbx1I;r0(bE#*)+`#;er2}pRR2*0E|J#36Z*X$dF-jpaoRq1-I4>Q?ch{a8r$X2~5h$Ekw@K>2@+1!u(`53e(bX$I6ziUr}Vl| -Yn>cW@*v9MUg7FCt|R9iK -vzXg@N|OD#yw5JsGK2?<%2GAk?Va`YMEmODn%)4*cE0BsiQ>D9YJn{yG&Myg*yeF9#VsCQK{Txb^zO= -aX@7g!4a=g@j7c-0Kh?jhX+M(tsvPASU!*i5u|{SU)j`2tiXH#1%~PUb1I(owp$4jB?I0ov9|13RpM$ -R+-r==m?OoOg`0{qt_fy1WHXeUVd4_Fz@rYz<8fjCwGij5D9zPfyPhQPRMVis2!k(7GM=!}>f;uNzTG -o}%la?Y`!7C4;h(_2Ym&JLT>2~;aw>iW?d$4gBlfZ)S9u4Cb`JVvyMRn5la%m4E-2B-_2!=&NEl0X4j -G}!WDDBH?P$o@)~bR62%!NrBIfw8_huuT9)V^LxD!zN0G`F4{)D+IxF_%zWeJuF|Dk#iOCHEM0fVdl{ -Ea%ie4t&x+R=2I$Uy{Z#NWU9S^&aZNN9Tm0N8t!Kne!m8gX|3fj5?0u5tgSYK>?kcEAF91f0ZZ^VJ~~ -*i397z&;>yfss3q$O@Lz{XMh>m)pw6Gr-2;YuW&)6n##m&xqcP>TM98W28sIG{_tzTDGi)?41){UjqV -4yIs)m9tjXnu`T`vk+GJmRgs~uqEL}ZjDvejZD}8b;EgmOv>u6UBday+9oPf~%=~h{wFaN;@qir|z^jKapUbXbzH1sH~1!j@tBVJFW4N2 -h=Omw){_o}FBvF3!+3)w)E>@=6OV>ob3RJ_eo&Y_hQ1r~H9^5yY!I4UoqD33QXj`ERg#NMCY8G0c)M8 -nZ2DT{Z~y;!Y~Q_c)Z^kxNSf5dyKB5(yA1Ov4gH{8E(|JNFoYOso276?k!^c(6o}wY?82_oynlv5IPn -Ehk9iw=os*Jlo)AiNFw;-EqqVt%T&Np+{`(9037qMUy_4P>>!t&f}9X!%$VGF^;kWwvKdS|Lw{GTZTS -SN`B{JDDH2M_VK{(z^xGC4Xp}FXXm)nASBsG6wu;K9w#}*Jl5hw9C8!6#bP;I7n}+@N!W?kG0B^*+=8 -VOFkhI`&`L~dw4mgluKnKB|Zp@q&$ei>gI?2DT;@yMl -?A{5SDSM^1uCWp0@quA}>&DoaZd~5nmgG8aZ(Z+^(Kngi-licFou02YUVuY&47w(?B@nJ3v4WTjA!d@ -K=jZriSgVrU7Bq(oVtkN9P^Tf*4aU8S0LmT%9{0?=gLeioqrwMEEXtNx7)VzdO)#|9kS(GUip4`YTwDF#0yqJ~g{Cwazfw#HGiKPWbGo^1v1+ARc^d9ADk -SOmNnBgW_!aFYp-rF6&Y$O<=zzXYLhcs8%DS}*EintH=4IM>$s=vgik#t{ -%|k_>P1C3f>P%?f!t==lb2f{mAf5e(UVSy6SvB=ixQ_07$99Mu_Ai$_$ZXgBp%jGAgH9`#j6!!0wMPF -B3>v^nqjLoPDhg@{Y?Yg(bOJBOWTxq!j=BuJra0FIw*tWr^VVk^ha3vs7Hx`e1>>@3IC@v2=w8Z$BYV -MEZXi6FuPJ_Aad%NB`;pNG6%C$e5TvRdcHH~AgS9DAgu3P8Iit^S^@M}?J@&TpIaY`zuOK1Znw=qT*^ -|o5!Gbtu647*aiH>&3T}AG0-~hvlUsZE|@8tTr6XVUyUqE4dyRlYyWH8=Ou ->)hvdTS6X<*G#dkx1R5 -7;aH+{BZ;usFk-8GN;?lL(qqB{Vk)ktXmfDWGf#HMEeDzZ|b$TuO&t9%yGA5$c}Q067EDf&~XVrO;%N -p7Af2%1_5WdvVq|PUY3rhQR5j~ADrG15sg;9j{evlCzjhITMPFRQ*&Gcs=pnFG@+f_R>-0m-(QTl7_A -@azLxAYIQV$zP^dDe=1v@94E9Zsmh}m}jKz0J7NXbO@FHN*vUoZd+SIZ#SQPjsBe~+#ed|VI1!Q&*l! -MS}76jbL*>X_w4hA2&0EEHniFVse`22>NVl04T4HX%7Q*5kxCOPoFnkdJoOfCQ|q97D^0VuA;aMb~XW%im28Th3E@A|7Jnt={uocCdFyL~W14q^<(>-_Ep2vOT -Za|zk5vie(Qj7vE;Mux`#4t`e#Nwarw{@$d`}D17Eanwf*PLgQi?~(`1l^covTNrVeE)6p+)^y;=w374rL=Gqp_t^Ix6 -q*wo-RdDgN1uf8jL|4`4eEx6Ncn;@XH84S!gB;CflBT0K@jqxXBc(R -@A`>x@i5rcV))2Mrrvd(o#!k-NOxn$Yrng?Vu*dVNc87_`Ufq -%Zvsp-YpA`W!cE{CMwcX<&c(YmTWsR-9o>VUQtQObC6Z;p&nuov25uQvv#cA(WNykY1y!5_;McH#mUG -aS?t|SPKJa~#~gD?%g7#l3}^K))Jav1_OIJs%Qd#3BcUhi-zQ`SwqDQ4VT{1eZ)Gd(?*dF -lk|6t!G-<*i-3ra4KY;V>Vcn)Lw<0WaGLJHV?s`t8FQe#PLE-Qy1HS2IObh3RBe53UebQgCN9v0epe; -JWSslT)%?doF}32kH~_08r056I^hQ=XYq|DR){-+bSgN^#M)klNoALfzEaz5}&ZdBnMzC-4 -3CVnBzFRhz*H?Kl?kKUHxa>3qYNMd4l_Pz#Jtdj4xh4&)*#xMr@XdjS4fic0OKe`=E$X*zKKLhW^g@^ -qZ^wQpL@DP@6nON(FUG=XX9=kdg@5}Bgx`sXcHM*=YE|c;1LIx5V&r3Lp!@!7S_m;{ZBI90g84&SJI} -|70ON0Ruk3}H0*Af -?XwW}px^2FUc!HONz|FReqZS=_*%&dFlZ|Cz3yRId=TRONy#`_5UQ_v{eQB+G -C(C+MtdO}cG54=zT}MrAo_fQFSL8r3seXRA;~{#B&R#KgMTX5e3wPJty*UTC3VRo}0aum`ymgDI4W1O -x)mxi}%X^&W?%?`Y!2k=DQdGmyIJyRcj6p3%ZvyhQe)I}hArId2v%mLRZC_G6kKE@Df9zi8nujlP9W8 -{ZiizF=TSQd)139PkH*IYrj<(KQ@*CkMNVkowG+($2&6l6&_ASC-7Pwx3u~}AnNtp2!MA!hrO(qdOk| -zs)&kHKiGJ}Z{1qyqG!5ive@{pxpZN(0Ai*=>#P0+KmN}q9|&HIa7{(i%41wOxX9xza#1?_l6-PsQw2 -wU-`{^^bQ%E>f^5LL@%Hz7=q+!(hNitR3;jW-#`w=mTiv<{N@Czv(S01N8_2O|&K~S}n75C3-CjO9>;2g6VG(|LqFRX;w=RS4LNo{OFScXvkvnghQT$h~ -u>)XCuR$0u6Oy>qP{7tyYUz8UuxZ>cL1y&hw)BXNeA-Qy!-VBAVwT#gHD$5EKacb<0I?WG?>j>HF$D> -@2#m2}W7buX&2UAe=V!RxD?AMJ@$n(ma~8O;m_KCPI*9gWC447;unF>fkt@#b& -AA-k8uzkBnazr5?UD1a!b<$A#OD)}eziQoVKoD6@(w+bkB_AY`Al|CDq=yX62+dtvOU_R+j-2s$)9L> -A=;+=fLm$qrZ^s{%{6Enp~%%B$|TTF1152`l&_S{N*3Zegb9#6r&Lsa;*|dat2~=nO`Jfs)7R -343cOdKO(U49PhKdLy?LPoeO9KQH000080Q-4XQ=hR{A73H>0Qijn03`qb0B~t=FJEbHbY*gGVQepHZ -e(S6FK}UFYhh<)UuJ1;WMy(LaCz-LYm?hXa^Lq?%u(kOc;@0HJ6FjurW09~?o`pqDoZ}6YEOZ%9@jm`o-=HixdNr4p^&mYrOQt`W~~o~H2mb#1 -lm&YOK#HFddNJTA9No}E>@ebaW(x?XlovsLCn)yXz^ux+lc01fkJE6>(#vlHFT9+A^eAGgh~vVI}EnR -o&K%FC^siKmr0GspMbz*)6*5%T`HID*mO=Z+%k0V#*dMmdkfbNz}9W -W@iF^zLToc13%cdvRvIfsa02Qw{kV32H#h>>X!%X`>g36H-}okc~ZZH$*z9hNV$DnZ6(f`-~3do!yX5 -b{C0LWP2ohGnlFJnB`{8F5PMqHZyuV5Gd;I0JX9$lyJjV~DhKMXsuk=614U1xM7$rGmm>hyO}i_*V%g -MP+iWc}($k0haHKb)=i@)pRLQbkZVI(*_7>R}^y~#b8tsWRf%z2sR<4!ntRFvzr?QpxQjSroE88pC75 -kfR<2dOhJ$z2jM*3dkYMMT@w5i6e{3Tk2ALTEH3PcUao>T|gM|lC_!Sn-Uo`SHD?QruoupCGZY3cCup -C3JXT0HvCmye#kc=E%u;hvjPZCtlM(_h1_wa_xnk!kA=aUJfmtJKo8zdTgiRZ+`s+boBB^kmg?BbRTA -mg%PSY7O^4)Cj@r|Kg|T&wu#w<)iN%L0*h>Z=D>2-;1s(_GPy@J1YuUy`m5caWM(@!ek~UC_C)d`cbO -Gw!_EIX)f|HQmUQ2Iy*bt(ozBtyR2D3BC;r^XJ_9bg+S86dlKarB-byp=_@VbF96#a9zL&Lo!RkT0Io -3K|ByG(&uixX=L=PLc3>+2Yz9U2R&p&0COGcnnCdJj@y(p}Hp@6IY=M&NW}ZUe6}PLd>ZX-3%(=LPgA -p*NBNXJLIRBO&&8ZU}-uUm25)R;6z_x?qxTQ1ss)AEN@JJWzPF!;)2ST8itc+lZ1dzN;9X-hk?yJ -5B_M?gO0c^mYy+w%kQCdLX_&?{HS4S>tq2(*UuffccTl5EU&hg#Ou;e{b6JT~*Wnmu+Lw1KotkoBP8J -5$0zz*+w^^v3fJG!_rJdwH~8ReYf)nlm|5K+MBrrR;j`xWad#AE>Zvx5Ap}qzaIA>0 -ViDD)o|ebv1Xk;st64u0f#V&I9e%HS*Vk&tvy)=y0-H1U7#K4Dqa~wb4UIIR5EmkYPvs(WhQGWwg#=d -0K(?(zb%-WJOR4e4odiW1_sImD10~yIPX5LxBD;>_WStLyplPa`D1C2ufJ9F4&W#UfjK`>axALE1>IL -)g0QTq~9+acnM5Ybv+c~MFB+kkrA?_l!A`Y@#znQAqEQuk9M*>)vImQ-Dtjt|Df+`##{egxg$40*93r -AzOg|Q0GXjdeJ!_J`WG3Tnhq#E_+zA=koi^aHL@q1++E7m3J48^VZBVOwZR+!$`Icx5Xo(Mg|^ZZ#Ku -aLS7ile6pD~Pzx>Nnt=9?Tg%QsmJr{qt_jRZPfQnq68}>clYpt*B*p~0}N4Ob`4u)7NkPt0O8X$+3Ih -mFcpbV;T2B0evI)D6XN&;yw+g;UZ|I{UME35A;f-5!;d)S<=8-jeb>P3Kwrpgc{NbV|J5KB{)i|<(~s -{j%VM#pQO+}EIvq_IUd48;L8Pq$sfCpZeqyaWzu#rejl|OUad#JN)OYm7uA -_hEj&guIV|0a}h1#DITAoON!V0_iG#>yARtVsSKmD4K??*cNLOB5ScRA} -hH(dcWzEewaZio_W{W*JqYdlMS}asV@8eUsh*PK`%y_xA-UeSntcUql>r$v$g`TZB;pc+{U;6#5~o#^ -f7dY;Ea%JBfOdnwaUCuP1tBTto&i7w`F(4@Ob*aRu317}*=*gm2ImF&d}+b_KdDVG*Nq;1mS}!`1QLmg(6Tev`kFg;%3># -dx4M;P+#H9C%47u$6hv{v&ATc+?3B9(K{R@f{EmB8bj=$pTEJ+H)kSA|}Wg*yUdg6Crt=O7G5ltXK59 -D}N?MZVd<_Ey23RGec=)g$L0Td^-X_`*Q2c04eXNUqFDNh2y)ixyH$>5}!<~R-eK0$+E5z`(S{F#uPZ -`S?UX0X7dUV6T|$uOL(J=Ey-7UT;N=jYlPghv}p{n`gZ+F~X}su8US0tk -|1%Blus!6Wy@$QHywR#Vux<0EVByRr&P;%50Bb4mK!U+0ym -eAP-gl`fH!p;qNe`sV$RKS(3)FNWnKl%-B8ej+g*Va+Jj0QABSGk-=qbtYjOPO?T1Xzc5GW9psx#RN$ -lA~M?*vAr|-TDJpbbpNbptK7+Z{)aIsI`W4CBxA;IpLP!sC#+8<<(6VymaRssYVN*V891Oa2Bg?dx%_ -aOhtQk&*uJc&bjgb)1H6UsASGvo_p;{oh;WU&J+iRRGxPLk^lo>Mwt4JRJDlr{cr3F;}AqL$ZZ%!I1LL(>7#ChL< -mCi$SVxI^vJvvC2u^-qKR#nEQC{`4rors_5XOIB<$hn>(ms~i5+#IRO$PBWJ1m=-@^&+AHQ>F9!X-Z( -)QjXh~T*3BH^lo*v9cO4ntWYLqPAxMNVKDsX3T3f?#G!px*bR5z4LKoAX(kfbbX@@01>5+32wMEbo{j -76ckzP}?enzOl+pUB{6xEbgQXn3iGFjK*q}h#sO^148$f-FK?6FHJYvZ#(XB~MZ_3>&5NKR*rIy4k;P -}TMZlmk08Xi~K%AcjYwB888qjQv+qRjpq3X&6t{;zn$41UqhBV}*0 -s?@SS!gastA8XhkDQaKl)+Kil -UWB05N>-VdZe(bB6r2DR5_ftUrylyz$cPh4hwWq)@vlx)`FZ;Be7fso23ekHMtH$nHu^WxV_Q4?h3S| -5gj%U(?&G-bzNBb%I^B@damX7P98F&(>*Bnm}pMGg=|~~-fO|-#L@%<=X$`Ipdh;7^^bE0 -28B{uAn`~!!(!H(h3p$Y!~s%_2$8Vn;Qm!9jBqZBI2mEdV?K&WmhO3sN1R#kpyccD| -D>kOXPNMbY!HYd`^tzj%M!)lDS&S>^SC&XFZa2Zmpo8d -MXE4A(0gMI_}0WH%+g18j?%%?HtWZ?Dm_bLeC=QZ{`pc0liD^$|>SD^r0O2t)fz%wsQZ-8aPB%Rd2R-?PdqtpX{!d+e@8=-_4#lrfuT!+C -SBE%LErfz%Tr4W=!F{~b>IQj#9K$s0^-t -MT^WKt2=|B<&%@1@X>oxlPFJ<72!-wkx}d63Jqd{(4RVwt7f}6lG%JwbtXOBYegzE1U5YrB^*!vx6{W9 -{?rGOlN>#aDT*y#Au4^3O4E`r!jWzVM%^|y8DH0#zReZ4tMVz58iS-@NyY6WDI4Iivl6MW`=(qpui50 -*X|aTx0ZGtU&!wD6zRbbyxkkX(muuh8D>`9$1qfkd{5bsI)~n;@On<_7>V<1eBx(;-`CDG4amJ-hykK -xeb=~)eGW!FV-W`uAT;lMDX>UeI{gtB;h(egTo(4X*umq;iAC^N*f1$87Rje!$H0Nnvob$1AvT=5Rv+ -;KOHytPiL3ufv3a`SJF1(;lSL_p34Q(2v)ka2Vlf0Idxt6JDqMMUyw%G4NN6u#qOA>Kza_G&Jw)jQUZR_|tJ -rm{ZlNZE^XHf;9~Zoi{QOzL!>#kf2zz!WpH4)hJGsJsAWGLV#lcSrn0VQNwgs(Oug6ztLN9YBDhEIsR -sw@tW}#x`1P@dfN1GQ4eLj_cm?qSNqg=nck&eoI*m229SnKtB!Oj`$fk-%!y+l#N1CqYnp15EMwdlyn -6niqwSQC|bp?U-UxOD-7IlGDdgU(;a%g#WpiijT9;+Q^0!H#dCcL5{-aV`!`obD|H^m@TGGHMY;P?32 -<~LS7+j1mp3>qKou##M4|nUg$Mu?=4P`(y4;lYRcbcu946KC8u+Pe_5jO-u>#al7QU^}OVnos2agOPO -ia*Vps#4I>ls1^Ru`)lM$l0@j(P*DO#4Yt>~}WA(JtpOf)Y8c@u#m*3~CcSfx`GdK(F%*6=@inlwR+a -CDIrESxL}7DORVs372u)ABAC}WoiNs&zwJ#oc3Ey*8a!419g7-Pe&g`E2u27C=B`_+MpeUL -c9lnvq#p4HU+Zh_j -ttO9lcgYL3j^`mqGA=BKArbJ2AlrA`@zMwC{nvB`OHBz%xjLp!xM-~V)buM2>2@wO#w*IOsLUN4$GIdG@ie$Dd9e{Lt%Qq;Ibys3Z43RUx*mu|(Oxg4 -s) -6_nzVc6&n7e=u3;M}ERbV!cr+p&ZOTjFU6Xw2@yD|Ug;UiL5W^K5c%fTtd{1!OTvbcfbdY9G{d@quto -_ZbIKPyTau=-C3iV7b?-GKr$tDU-9lH#i_d*P*@02+#CEip?%1UxxhB?~|nM~df8lQY4AbF<%EHCJLp3Z|FwYr8=ryVtCV-{Jm -e?MP3e4?*3{1r1K9<^=LCelKQZgGX8fw}csLpzk}_OS5ojqLvCeYB!LEK7kb@nWnd2Zj&A -i5r-~m^pau|9)tcQQ_OcihQvc#Mm96;y9x2f*AFw9d_5KKMgj)O#3f(-^omvB;Z^Ycei --Jy_+0>jkG3hI2JeuW^kEn4p&@I=c!lb&8!~*}cuz|%H+A*PLFz2khQf*PJHap@XQ*JC`XPIKLj2 -%0M?$m>NFZDfzNK$aJUy&JS2t{(!Fg0+&^G(zsv%(wK`#;#luql1jP3exVLz{9DzC2J|MOQX;T$XztZ -m2^f<@SYN)JY=UHVJOK6o93(SiRdXKz_xoF2dyI5~!aCsjXk9aHZ?Sbb6ZS7$sT=7{U6j6V14t8Fgos -*NVYaN_s+EObw}#0N|j_GWyk#f3XceH*18`5jn1#*~z47l&y#`jh;m9Kdo>@C=^tBYsm)$hiaV(49a&go-7R9|l(e_Gks@ngTqGEd8K1u1s10yQPI!~5j{gnJBP7?|XYNj)U!tC -0fZ0%wCGPqFWM5bqfQz4{KUZv*OlUV=v5|KW8iq6oku=sqQ}B?fRL(dC5Xo@i2w=< -j|dNw?J&;h(c%qAr5JQWIsVG%&x|i;h2`%1-LGbZH(}VlJUxvsXtuL3PV49IAgx1sHx*GHN)&BK$LTlBk9LckCETNa -^vzEV8dYYsm=bQ6Zm~6N+4O0rW;IPRP)3VvPPA^S&w}xB4o1v(`3`NzI(mwNR{lsV`b$#C|611CNzCd -JR?psQhwU?SOsBu3jC;Gv;4k@o@l93P&mM7rGKXGuNUSuB=n`leeC_I$*5{5mKEenp6J#0h$}IgIx|i#gp#O7r=ATPAbm$X=Xf#qD*1O -wH(C7pBZ9h>8f5NIHFL3t8X2*-Xx*!Q7!lVe7qd! -NF2P|oE?`z-bpi^Fi;&>iPG=CYK>G)C6k|%3Nr{QXtHU$ln1u(_PnOo^=x}KxI>T<6Rh#vzPu(=X-HI -4Vi5+o0+goaDuRin2lSn(Eaqi98M2}9=JdJMnNwLXKserPA&V@VQ)rxG19(G-UdCv3PFEI_33ON#n{Y -;j`pDNNTp6&(NFIyw2F<|)r7Ms?o2B%$V%Yv3{5FwxNoeqXSA5j7?S{eL>(TJw0mDFC_ZkG(&9Bx&n)u{^hjpr -C%ck3brp{$3(qMI!CLlS?{FE0~pL+E1(PILAffywY1VmAU6cLm@?c|(GkW;u -bpxbJ3?N14diG6fN1Ntfn)H|fRL -M(Njzp_$-5@bIiq8L=Nb%$J+8s&hiV(&u7`E|VvZz-0aFD2CjakRPwXKw~<{5-lnl3iZyKgv^YPaw06 -AvL8Ie@~5;-K9&>?!aoKT2%mm7$(YO?VKFeh}W(8w%Fr%a~6i&y{m_$-c|y^eeajOLYKJWgQHZld1sd@L8VJcx_}U=?9D{-5(>5Bc(CbM -uhn-U0q>hYt&mxnPuS@94)fv5>F5cOzmHi0yKV+2$ho$@2 -$Hyco_Ro@(Mx6GACxls`#ElBOn19BKEqpZZo8Mv^)bm^nRuE{B{Lt{o{(T*Zzdw#5f73~e+q -}b_BN?I%Tc`FPU>4&~GYku%fK(JQnrpIm=;SYXf3BSD&-en5yl0E}#|Co-x9505 -a&50!_{uo6~Q1c<(^fNa-K~CD6?&udbn?vbPkdwP#sKhm_Nk)uj_6b@m)5I4{QJnW4_BL5sq}bAS)9B -`Z^z9D5f|iov2_ige+yS)mx@g~1^vxqce>yZYA?@mdn@`S-E-Ef&h*WxM>C=zF8^gv}H?arTiS2nGxR -0XCO3)}GH|R5!kW2F6fXrP-@CQE@Jn+q2T8~H3m0!Hqt4Gd&r8pR=5XtkdIk%CofqQ|Di9H4$__AX7w9$c&egr@!Tvf&#-j{*-i@*fQ+_kX?JP_aLF5i -CnjmGg?`!P2$F)b%p@kZ{Z!n-Hof2%h4!N(JNc^ommaAqo+IF(Y_x?_Is*owdQwrV6x`&y#kpKd1+G% -gQdY6rkvuCSN}Z%@P;Kehvy5D*M0mZF97T86O;Z$?WqW=S5P{>3aj{d-3|_rG$7(#GtJ9hhl!vz+;YZ)4$+jQa<5Z?KedVFG0uTC-dpT9d!OrZ!UjtEog{ -vSVsg&THCnRcGBh!CGi|WX$n(_)6(&CKe^5s`pSw%4;p3GD~Kx?z$3d_Cv@)4$%@n2Q@j5KDTJU=HR# -Nr5hTE7xU^BeF-6A)zmprojIxIN(*G`$$JCsgX2WL4f#Fk=X3fCQl2ImyT(u77^f)i$~~?!_Dwje*c(oNbGI%bpZWUbL1^gmBmagOa=+RJrfkMuzQWFeJ -yUi4aJwbX2M8SiTL`qv*sc!JX>N37a&BR4FKKRMWq2=hZ*_8GWpgfYdF@zjZ`(Ey{;pp^co-@dvbN9OT&(C8%z)PwS+*~M -AO?mnP2AA{TjTc`a{V7>goN!iR -eD0Rd7JH#P49-iJsJRsiKeOT#(_B~~Rc7BTq2cN`nyzRemoVUQyT&~sd~S?$7j%k6^3!XzouJx`Kx5L -~E3kvBFNAg^6?J!s|9aiA3*F%B$)R?r3L_$l3TpsBro?TDf;t*{J`@vl_kr_w_b)=J4`5CoY_jUku*p -$j(SwLQ{T&NmDL5E?EwV>Wr$gx*PWJot=8qf2fNx-iV{I0G}emPgDZ -Gzb)8C$TQSKt^_UZYevELlyc#3^f4vR#D+Xo>0?+fG|#L&K3z*xJ6vVdZ+2{_{2Y85*_U+si}f>bvy#WDpRC;-JMvnrp#`VoXSuPg-56YE=8p_f6c) -s(qVVE91|RWP1ryEY4$D16CrBa3|G%X(i2Zw-uhf9PCy$~C2`Y*M_a=^l%bZacx<4XRJS6oDh<-1SA1AFt8RWt^t;GI#~iCzr(pcVFe=d4NKXdmkVh1%pA&60s=vIg5A-& -klyvVksk9L}LF3$u#EVWlfdWD)`t;DB4mdLpTo+6ZlI?C|R>T!LQJ_NIqG=nB*+R59sMzcTn>V4Tvgh -CZ|loY6`zf-bGp=U~hr7__3}(-LvZH$#s@gI3}haBxz$PuH*Oth#b9`CeK6#M1p@$$F*wpRE_N;I`hQ -Pnox6|Egu>YeJ2nJMeER!XbmYmGe*6;r*pF$NYcH_W7Yc=QhhuM6ZP4&%zE3YeR|~>~NZj7kkqWP!~h -CLX~1CPep-S?ybm39BwTvii9nDT9zF6;MlTSkuott4DdnQ!>&jG^MChw#l-&#Nn7dLoG{l1A@jtY;9L -YH?BLMW&f(*&`}ed+J^~Z@T?xzyr1_MNvh_0A&e@VwNBeq~tfC95>n0wcyg;lR9o{|z)5UGd -)Sd%_B{oBO8eQ~lAg5ADxEvCo+`AYyC~~ecc0kWEjSFo;n(HfY=O3g0(?e`ij}?j*1SO;u`QJ5`IL=( -Lbu=M0`HZ;t=PxO>4-^W1)!Bovm5kzb=#0J1KG0YaVx6zue5ng8jNV#UnrBUv<1MzhnQaWvqz+9B@B$hEAXUk$?G*^`cyUgz -%Aw5OeE{4{j8bb9JM&t1-Cb~nQxf2y^QOT+I|Zm@N~q;}7Y(jy|}O!PuU$An(2TATZ4_Ls=p*G)!R!7 -uKH;Q_dD_);_gLwABO2e0kr-l($?sU%g~kl;ig2Nr(=P)h>@6aWAK2mt$eR#U~1Yoq%R003+_001KZ0 -03}la4%nJZggdGZeeUMY;R*>bZKvHb1z?CX>MtBUtcb8dCfa(bKAzX-}NiDa54pZNzjs9$JT^y6!1H5-a$AwkqN*jgr2&ELV -x#;q9w<6@+mLYhfx_W>cA{Kus2MDGkhvx9^WW9EJZp`thPKE@-N2Z&}2!b&@Rsb+h%;@#(v>PZyh)Pv -t6?lL#?0d(4oWZr*km=S3MMGQBgq@-j_<4q=&2Wgb@Z`kH}R7fyi~1f(y*1}TGar0lKz9RWYzT^xN3- -yQ-5!gt3X`!qI}MYxQ9%JOg-r||Ec%oX5YO{#nn!X0Mu1CPZj%+y0T%cG^d&+=OyRdFH1RW!Mc=2G!c -sQ`Bz@9*?>cET`9k}wn_aYeIU9T|`JczEJCPW6e+Q3UCZbs+9euac+$v8xRcTsaM4T-7GZTsGFS;M7J -eEzD@`-e%C=90sm;c6LxNWDZx?3K9UU6B_fwFaAy6oufyM;Do^v!qQ?tmi5$5tZj0F9p5i1d=lPm|N1GB?u -20H}jLQ3)Vq9|7CU^pC?a}})C#R5p37RG6F@u<{OcAr11mEvyoCDSOdzHXBTEz5+Svqp(~z*jHVTmp3m0V($&Vs3dCW0S -E0aptV>;s$2GY)vc>j4AXN7pC;080Pl;LH~bkzvl{(n2=Z%jfw7k_0E;v$989(lxJ)&HI#1W&`V85eW -SOG3RvQRgwB5Al3;xtYW?A}aQ-!AG@74(iNnVDN2-f(^=t0H*mZQ@wmA#EzMGm%A?IT>{eMBxBKq*Vu -SW6I_KjI60Ut~+!7vr)ZdTG?O#$A#+Do{%)vNTzX`)DnW#Ux6F0%Zg>E+ptsB=n&JL&tMg%s)yIHSuA -aZ@GGlH*D;-lX%;wC`MC}Z?Ah6)-22UFyC_sioCY;)H2fhb^M*maU7-Edv$R59nFhqIt9+$XZRuJU`k -7v3jR2ndw?BADurBn9TX=@Ga-^}5+z-0ew);9Bui89Z*Is)5O;2F2r(X#57I;dH#b_+_2xh0w&&0H{< -=G0k2FB@I?(hQ;FXZl0i6Lrc@Zm!UPRzDp$S|AgWBn+LeXj^X$I^IkqnSBnn%EVkhr2uAqGv>f9r`Uk -VpJq2__#D7y?Ga`~+fcm5F;PCb@*gpq4Ak3mC0>7#TlIO;6($T;VzEN~5a<6oo0 -E+rZ}@RM2g4nF7cm_h>P!elu;z@!1}u!iBgi~L^CEDL5aTp6S#%qWpie$6abW&zhaBu6tm^L~an4O5x -`#2XWYh2M~i-Y=RLH4B2U9tYC};a6IMCBWOm@OXZ|S?)hT$hK#t-jbRywkZX26+{_i;=9x19|j0r8w; -%f@3VUev0h&=I2(h3(KnzgU^h2u0m7>R2#oHKC|X_-IBc6|mj=SOd_NI2ZfD?sMQ)BE -dZxxn*-h%5)#)m_P+K@FP+TKR|f?J~O=n%1?0A_tFA9g?v~6_z^`Qy^S8t -I{|EM6vLJkFHn6Yj|~~eRG~Wi4ZP|uYeg=Lu(-OF>udI7a?PI1W?I5VC%wvvkz4|Y!)P4Ol4z~~PF|= -3&fTwU&}fo;8esuVraFUY5rjX6cTw(pC-1&L`gj33l1AE`)1AGTS-_U{hW0!Y77Ex%;5)Lh{LjNPh+Ebp;b8( -=&+WrVT10ryV}ZKa9LQO@#046>)dWpJcI(PHi3$ifTzNA&CJK={waKqNA)Zq&POTnSe`!MdShgGEzIj -=cOV0h6!GOTjt(?u%63H;4YxM^PndAVEWb2fCU8d0$xO2_Ws6}_-O6peb1xp&hWAJvkZF2_I#a23sAdlexnwh)IOZg;^I7e$y$cK -VC^_QwZbwJnx4+$9_#}NF$Rv5_`!2$%x^LdsP+veGWZ(7k1KvFbY6(c?$Rsp$2`#@KgXd2Om4PWN&Am -ZE_L_}MICIYF3uXi4mA&@;U_z65RTkc+;h}RdK{oy%%eQhI5RRN7@jzUefE|QZ!mI1NK5-hoP!Tt>9a -8;Pu12PDcVcqq(_p05kA~RlXL^QHPc&iZ+tWpHH$cU&{Sz3+z$b6`#81<1`jJ0KpdhxBLYH$r^GO43e -H~z1?5B<{No6%a*k8A6rD0_D5_|+%cMxw!@f26DF<#_+OxA -Eibh3OckkdNQ2Egmpx(aavaY3PVfhi$-tO)*tljaz*JK#(_iL8Sd2Xn)?2q^ZZTJ8S+^RH+T-)cDF-_ -Q15yxe;gSU~DFUWJH5r~v0UN#YU1n=EtQV`R%aW2AiHM+U69!n+d&$^HnuKnQk|MY8Wz=oA#EZsO(vZ -%`7(L6$=>pJAL657&k`z|#f0OtW#g;X`lm`Z^{TV`7`M3r#{8JpV`F{O5{^>NJ()WU&h;%U^Kw1KBFg -+%s-ve-`C=cz@VH*Is4Rh{$Z+@=oU>$YY&JxTGW5#!xn)ZI_qq8xVB<5Keta=;YN;jmi4*a#H3QR?F` -8>!Asy>WK&4zyvZaxU??T7aX0JtR>Yni9GNgMwuPgj!r+LDXUh7DvRzIS<5oax2s#DRBm0U-)<_Azdp$~NGW+))L~9C;M1n341RBmTP@Vp?!o>w3YHJ~4tJTU9NON53bL243z$9E6a_={=Us7L%|Luj_Tg1W)U?IS=+Mr%<*JFiJ<2PvI_cL7myU?`dNtqe)a2T{> -%8sbe5+TbD^{_|X)%o0QZM25{;Hp&~V+jU0&DxD6nRtTmoJP!o+Kg1X&!xf$Qu_tY=N)jJ*w2FJX12z_5UV_R%r%6nd<#pa?J6JlN_qi+ZYaGyr{(5{dnzIRxK5?IKe#Xa -*^qZ>N6fYItx!us!_f3tg8ZqZBwR?|~KLT;5{|xCqJhHS`ReFhW>xcz!v+d*xY@fElQzi~6O5U3+s=(Z2E4fL$b -2Q?SNYE41p{D;o%zS3>$6(_6MkfWHE?bV^`CR*qolK@}fNkC_-=W@{i-h^pdcoPg0PcoJhyG-zn*@~l -iL=(@Ra*Z_#LuH&%_so#YdTw}VvWkCH+$IVUk01Y2geU}vRlAccyKztzu1jNgr?*O}i2C}jdI(r8c`< -zBB$Ts7XC?p@)`RrbSh?x6gSmHif;c`tX<v}@3>g*#4D<`mapwm$-YuHzD=9-?_sHIQuK5)rBkcVqdu$! -*R&j{1-SR=Q#fEw`F%Lhg7*K<0iB^fkpqD3v|J@IM}K#_+~5{>dzA^tzgcn~HPItnyNA_MKh%kUA*u) -*jlbJPjRk1UYhT^%UhmYu}}apj_FP}*m&j--$>agc%(>=2v|EIwC=>cnlI*0U -m}C+L$#qaksfXs}5KsiEil)hmp)B-LWdrdZMGji|IKXI*Pp@HA$F`X3?TkF)&A7$JEc!>GNcfl=*Gs2 -q&!A8%m##Pr*wH(P?wI(yq(?CtyqP)h>@6aWAK2mt$eR#Wqj+MRm{008e6001Qb003}la4%nJZggdGZ -eeUMY;R*>bZKvHb1z?HX>)XSbZKmJE^v9JSZi(>V!%Fl@P?tyyA{h?VCm2fK_Ji) -ZL^U@jil^&LHpbH9Fn^E^0wIWA(qH<4$t*Dhl--;Ugg|MRT;M52rbxNuu`dKwo;nqs#?jlNtmpRl -?ZC0y<37bbzG?`qnPa6TsvQ&FntwF!tipnyxQB7H}rC?fY)lL+QV^M7dtChsQte7Rn#x`1{5VJkhhdH -EcncG`clRms${K88kCKI`>mA24oy~ZK!t1|7AIhe^xt1T-e^s}-wX}&F3*nJC~cm)j%d)MxVwG%GCoV -L~m2@h}odiVFc>~DAfdiY6>_7!J#v{_ip^7Dto3h(Y#$XeK}KG=<_vQjQRxjoe6=Role-b&a#L?rBEa -9YAXHFYTx_Rk9M3Cp>$6VF`BI)xTiXwaowRR}sZ-w4E|HPyYHBk%035z0XQ`Td7?A3r}_Cf`YI8(xZP -*RtrQs$fCZRE5x4zaLagm)wY<1GfCD(%Dv4_`eg{z^2KBGC^pzXj38u<-J>rv#jD{cPSl2L7WIhG5l`M -rhmNI3xe{?Z_B&A8v)7sAAlSn6}4M%AR#;cdFHg{ ->2GRYp%_hJf3zW;w2ZgC6jX$&O1T>@3dBWECq;osD;@8@(Lx4*Q3hIrU2Z@syGtwP}TvX7Tl;R9SK9J -Z47KJ(sQgh3vhtk^mdLoeTAfZuBqDM(VokJ_l{?fC;{pTntub-Y6P>2i3(;C;0UtRq)~DzDPd9i6{sF -7laj)&gel~4?aix6rZZ(aX7|oj%dhhey&jJt`YF^(OX|Eqzz|S0!&)4LEQc;R{Ak7ElWv3;&kd5tI#% -h!(TS4~M(`M4#OQ?hyqqic;p3+}l&`RsJVH6iU>(5&+W_dPp&40-Td`V+94D^4GDJ*!jo$y#B}Wj%UKkK>g#bpbSq9a(VBV -&s9O_0V{E8o~$#E)6{K;53kplbrs#r0!Cc{-A=dj0?oQ8bsv4z+_Y?SLw##RDr|tb=HJ=Q}P-zAQy>ZI -}r+*4Ab|n?Vv{aXakVK5lcT=`b{ODm~FiK+iSMim%;(g&=DQ- -1gIhZz@nUp(qx80{iQ7NTimI5@&d#4?MJ#xX_dFik7^CX)hXH0s@auit{|Q*YbqkK5#2R%V2~@JF!Y1 -IxibOKFDX6D?^8bY`>BDhAC=J9lL~Vu4xv&CGo{jK%ahS0&+(f>RQO?f#`46kkR&AT;sg3F@L?h}6_e0+q;{0ddF6nEqx7!R^qRhG0hW++CJEw#1~Pvqwx|!T3%2F -?#|O3|@ZT%_+)leDNcwY$IN-KRqQ1}W)P?`q}ccO4HpCCH`u -n1^WUR?;fetr_AqB~w|Shu3461L1BTIUcWN|0HaZ$x77a`7*L>IKbEu}HWDX+6UkGDTJ_~n++47Atttg1p#Dfo8PPF^}c>IYf9MdDqk9BVWYSG>)e~y=wJ(^baA|NaUukZ1WpZv|Y%gqYV_|e@Z*FrhUvqhLV{dL|X=g5Qd9_(xZ`(K)efO^* -)Q8x!in5&r_Q60F$aK@4>|~mtX*aV73YnJZn2jt7q7=s{`rr3llAXH|Uc*;c8NL6a)7GIQ)ypZ`#wiGMzPOav$W9&YEGlYt+L< -*lb2e$i;%UMUz@Pj3=`hJf4?F4HHlBJev#12=FBF7Dt+u3V>_AVU1#3Kj3p5OtV5h2o|l -Eyn!CHm>s26Ef(_Wh&fZ^cv!4i1s4H(6(#J`QfL7@e_Gbm7P-`GXxLb);OEYG+QraAsE -jx)s_uTz<|hje!Nr}@VYJNxDCq`Y%Fp%->`Srn`Ws>=;8G58A<*raL%$Z3`*F=g7g5Ih2472awRPMqA -g%m|FlPp*;*VG1zUiXY|b+ni;TzF_0`SAZ_X^qXq2A)>+It4?5E4~)bj8KF4v+Ndn9ta^OW(s9#7!-^ -!nrZ`}2<<&;Rb^(zELeh+;8sntB`@Bq9Lvhp1r*2NlI2PAP~&um(%@Qnf{%wpt8{=o~qou;ah6xl+Y+ -#PAbE(YvL9R%H6K3hKkGv)NFzkn@_?8?ei(OS0AtCdbrr-Zs`d9SA~u#mk&s%2KdT*if@2*OooH5}D+ -MgEL^n5=i0PId~z;somoT&G)AV@PCF&1m*z%iLZw=3-Dx -M9)A9+ar&@E#on=38;-&nXTy(gik~i)JlE`3xUHFax)3)MT&iYSFYc<@m#h2F+Su!pd{8y*fp -NxSO546dj3Jaj5)_CxfNi1^DreUy+tW`~2~KX0MDT5C22X*bm-Et -}bFasYuif{5RXV+)W&2_UX+uyJH<8}(wYY7Qj_LE{nr*`jwKR{)AC)n1T54X1&TM!_vnkC-xr7qN}H& -B!OPXIMjbLv}!J2`cTYj8jiWNB_O6#+~{T5B6eR+(AANP8MU6>=5n0%&YI74&X{9pu8wSv8HKO=6=cQm$+C!6B(l_h^krl-V -bI~)m+2#~Z7>q?b*F!;z4a|x?BC6Gq -c+Vuld^GRqIT5J-#YY{3m1pMAi0H7Qiyhqnj`+REq4^LKEn&(S&#Mq}>SMg>U?r5!!*u+dh7b}48dUc -yZ!|Bt>SdZ+NF#)hg#=3`oGm~iDprp)zkjq+RjiRpA45_s^Ci>(dK<_wtUn)%YW+FsnRd%TIcX+V-3R -=p!EM&7`<28+n-YRC20JWQ%M(u7w>EiNOOT=uo=`RGc#XV6T-5l*X2~X6~WxpX@Xmxg{YE#wfQRc$Th -WaKmY2YkbBf?0CPZfSzqz8Ue^*~*&eVqpj5pxo3+ak{q^PS}kQujs5DT&^;jzuYx -nE*efW?Ck1h!N9V{cLSYwdX%mwKV+lrzV1aIq9#b{A2fu&Jxu$e{$MJoF9_dzkM17)Fq~Wa44^(F{oS -6PvK+l1Ry6bJ*y?^7m>?z$ed>WcOD_AXCLH`xAU=6Lmkn7nc6VI4GK{fFXpnkhJfpiCt{)Ny_%>nJvS -P?Vl>qxq&cuhAu1GTtw7zpk1X;ldrJSnD&gBe~y<_t?<|H7-Ud -mO5aWRJ^V)$_KpQE;$5LSb}IjM>0iH?S_09vE -F)WIJm2r8Z{72g8*`U`a%ro~Nwj-KG(6PcUUpxGo&;VFjNoXsWr62$JQYJ6OW@y!(~&;JF^0h4g$H`v -b%Ovjz;1~6)K-O{2_mK^U#2gXCsT^P*#tB7y+=RHb)S77FlM|HdTcvfC!_xWP)h>@6aWAK2mt$eR#UU -|S?w1C001in0018V003}la4%nJZggdGZeeUMY;R*>bZKvHb1!0Hb7d}Yd3{vdjuSZ$eebU*^*qoGGtR -E|VWp8kj39^+f}qSGSRvGMx~mft$8Oo~OtOf7-{ZdIvSR3m#Fwg5r%qK_Z5rzW)vYOmwc01DKd^_DSA -h@fy+2u&*3@b_9)x|P%rZ%8XE)TSMij~fc1CK^2Bkd>cCu+I<@}>|Vyj$erDn4oU0iDnu#47I?26-kl -3dVD(`Z%7psbG>E}v>q6xELU7$pQWIX<`L=5?U(mcsw{i+1T@Ri -wg$>rN889nE3@W`f~BRl3#mI{t{Pdc ->8@-OjsN^r*F&p%Re7RU8*(jPy?kDsZ4EE^fJr$|T)u~5g_o+ja4Su~Dowfi4;RXWSV$}ZTl2VvKyXe5$^U-~%=rygGn+QhAQ29`GB{lYeG8IU9EKzL=9D7Z=J+3u4VP_g%PnOdwN}&&|u~4j-JuUgdQs~O4&Huu+w{R^j2> -NW*5|p?d^ucDhAhT%`}PHO@f^?U1Lrb%1nefZMacix$xb%9L0~VcBst-eS&%rKaO{t#c>p4vmv05!Qt -m2>}BkK&|%{-dLGv@MBxXZt<)_?b7fReQY#Ok$EAK^XgxI{=pi~yUA0&uzpw64h<4JsGq$B{H{-RjR)EJbJQ=3t;DPN@t^y*v -k!0PH}9vPXY+Syo+tkSP)h>@6aWAK2mt$eR#Oaf8puKd004Xj001HY003}la4%nJZggdGZeeUMZDn*} -WMOn+FJE72ZfSI1UoLQYt(3`*+b|4+@sVX^iAt3vyoX%|{99cMueRrSB0q<(Oy+Q6HHR)@n`cquS1TQ7`N3)js -39={shvkH2qMC$(;60s1Byq|bmZ9!u_n8LS{Tk6m)OvG%d-G@ehztTzdi6h=6(E0*crg<;<@&i(# -HOn42)G_j|NJh+~OY$S2FPTZpCR#k6+3c~%Xfn9kmsvYGMFHfzBMkCHSp&Oi!z-m5u!{=_#CQBg;sCG -ufMa6AOv0Xap4tV;5Jr~goTErH}ks;M@3NfUYv80h#3iI{~u4AZx>DwYu8!h=%rN)j5F3T=x({Ibo6} -(UPOf=fOa~Dm4jy%@r@h)OW{a%lFAu=MKeR+<8^@G%n;X7ALOTzPqZn7?VwAkI0|HIf0mHfxby)}P01 -;WByUSjcTglSb54AX@XViX<$U6a{l{{c`-0|XQR000O8`*~JVE`z@J2MhoJUn>9r9smFUaA|NaUukZ1 -WpZv|Y%gtPbYWy+bYU-IVRL0JaCy}lZFAeU`MZ7vPF=3lPNuW1+imV;n-?po*ZAceC%rZ2Q4k49s40R -2Ks##M{q{W%K#%}MI(FRjZo0%0N#J>Z2l=KHQWLhJJdZa_DY{}KZg2jeMB#qtVzuJM%3aE4(T}`b@|= -y9Qf!FcmavG_kD?KQ&+*yRd|&?Se3m@_X7>EsI&D$hf5GHAN<3$^KJu$u_Le$`O0f-n(n7Dp|^xoXKY6adAU)p1pPtr -lk4#n%xPj(mZd#oC>)CI6t!Nly2Bv+in0kFzGhFVk??%-`bl~QvdMz=LLukBouE%#&Q*Bz-OwHO6H-a -j*c&8&o7fV7q60&+2!HU;pJg+dU!Ikl07oJ-pIUj`4+jSsDqi5sGdn5YQt-sicLwi!>V+a^F>l#uM{C -)kjt~9vjcJ>ZWzhfk{29dk~Nbo9)`$Pt0d>ewS(!d6Tq`ha9g6b%3KWt0ZBA^fZvH0cEgA-L$f_FtoN -AuxfEKYA|HW#nO)^>k!M6KegIjGMn_<@k}oP`tkx4eH75J6UyWCz5qchzu&#E&c*XQ9b1zDg0&FEoz} -iP?PL(46po+~3Ew|?ac#Iqk6HC(HODRew_10X0kO}UO6w8+rviDa|h_nmwBPmx5%u*k3%G~$`%Z!F2K --Uiq9HIIJ!L6jI%1;633Bwn8R~(2VNhB*}k_@3?x{?K1jz8e@SkhY~@}G&lu3hnG64~oqq{b6|o{UEx -3IO?74C5goIg~AU7ZBe8o~stocs%|Nor&l*IG86E!WBTF8Ow}i$D?M;A(Dz#ZWN2_3ZY5CQp^*Q=UYM -v27mA{&A_$5XQo8)A`KC!24G4HvS2Efwy -6_P`wPLl2I@T!i&8P+X%V!*g6u14EdZm1CR&gZHZyr+r7u|yI)e)Y4+7D*j1>(yt_dwND_Ed^hrQTW>0|thWn5!^P -2dRzsN^BGbO}_|`x+Z0OCcMmp$T~6zpTY&`NAv$>LItrHp^`dGuC3069A{cKgzh=S}RRasjUFe90&yI -J_|q<^FnC~H3r&2OBklXej&OfWHjpJg}|J~z~ob*G<<TB=IPSl9R5)f}Ohpa{7f7+GD&JjucA;I3K`ZM6~dX40^!XPq -fy$R0sR}D+nFc0?V9zuw=Rq#l}8Ys$y#YcTa!+)gLB9DhsklH#eY_5BV@JmLh4q=su47%JD0#-^A~L$ -)8X~S9=Jp^-0fXU-J#mskC-KpF2wD^C?EXk0pr)Oea~i5&aV*`3p2v9o -Evh`jETqJS!7XTN9JricGa%_hlg9TyH2HY%gG@d#Z&}5r+s~g{8Zjhci5a#zkE=GVK3dp4<@KW -ww6U|tubTcR^sci7cCetd=6T&Npf*EEH`^|Zo2%h$wx;M*WbOrX?9I_@G=<)aP8(R@5CCC!CEAj0RQp -v7EkhVCAq;8|lFMD-gju55dmRmRiC%7bpR!Vu7hq%{?-!yftfWH(esCM4qF&SpGd9TpS&C``chE)w0B -EjK2oFaL|JDtv{~STZ|9pLRI=EuE(RC{(mNx5^uFCiom-PZ#SCU0TUW_nuGwp9g9hS4hQ91- -5EDD_Vd|4XBe2$O5z5r``v<72U7bNRxPaAlOXref#Q=BwR?B!nJ@5 -+102j$?;tAk1zAfiw?;5}TQZWaS$fUn*S2V^XJ(Xq1JN03cRNfD8)!V00KNh$0D5EypX`)K-6M;h`lX -Ij0CaqA6A^hLO(E(jr)vUOyWCX;)j(?-b&fhEthWFekg0P3c-jf?87o6-)Tb^fNrrig2k2wJPQ*Sw;kJnhxM8J{SzYuh8v!-E -V1(H$&EXqe`Zz^w-d)azlLPaEk3np@P8iB1ZOEXT#5o0SndJHA;hBgwu@RA`vZ2+wV{lV0xeaWRAQsB -fsUgGk+rQRThc1mQJO0w_i+dL_#wqe9|F@i;b1@^j;WaufL}DJ9nVvZlCK_(-yGdAo%qt|Ek7IN) -z072ITY{DZzTUd&jup{-NpD(t}SOw&Gdck(gA~XA+ZcW<5~H%r2oOI%-Z(az<|uxX2;Qb|>=n6x1#lOLUiEOWY2&wzyEm#1eJv*(AeXLrpFS!72k -rYnu4wb#7-`q?BF(FeHK$8z0A}+%MErv0%3FmKoxufR=;=nDtIjaRq$Wyo)7~uakpT{%(Wje0I?ym77 -bL1HgvpfP1DE*xgANsn*R_*rB_$?I$mJM>pmZ?}xEWHn_u?)QRs&G$@VW#<7W~7FFap?e2Q8U>9hgi_ -x3ULEo&xdkq=U0Zlfb`~Kb|Rmi7%en*L>WH9T;6L*8*(B_L1$L`g?Z<0(V0)+|-&b2<&4cE8fi2`x1- -adV01H1|ftHWj0kI9o)3(b5#GZM>r@so!~QtC9Lf{5>ym=J~MKlwLH4qLvKvH+V$}--WARAex3`)n{t{R3bP^Hm -{Ac!JX>N37a&BR4FKuOXVPs)+VJ~oNXJ2w< -b8mHWV`XzLaCx0r+m72d5PkPo5Z;H%fvOKMutm{afB?A^ZO}XfftHp=HoFq3l9V0q*LQ~0g_LY>5(8^ -X6nW-8bF`vqm9`8TF6yiSWB49c!Y|fpzZY`9s(B^OFm}g2eSGzC%igOauo5aE1 -$m1dKtpyFW71|o+k?eD#&V1fMuBb{u)O4!Uq?9|0P2fD$V&{P7Q2p=$t$IEx3{+{+i^6xKbNc+e*VCf -SHYQdTzxIAKL4pubhvgDT1kqTUks1_<2~pfd?vB!YO`8VPJ(9lLvm(l<2$#Ovj$}IdBwu4O+-b;ZBXcOzHDa2 -!Q=QCy;9b#-8Gma}p5uo`r-(D@p>HDCA#M*sWi6THk>ti5H4%AvS7Wp~!yesq$d1uHf3*V1FJM+8<+M4?|4dzGWEDuJ)2wnp(a|_>W-)iD&kK8XRCY25 -I_JE~6jE2M|Qe5ep3vBLhuoHYx_KoUf`C4nG{ni`x5RRNANKoew96LEsPtIFv%7H@$4)GScu!v$6(*|OW8b^)Y0+7I@4m05+UWApqY&&o#{eHFFFG;wVZebN~24LO(k|+`)jdjGQQ$air@c<7DO({YgD0cO(vb6T}gfY#2XcnNy-&KueibBc -4;i?byMA9|bl-RiQ4iqo^N5j@UdrWTAEAbxdq9m6ntxo)y%39e*6!w(l&VHS@lUJ;K$jGRivGgjAC3G7f5-lI(Q-bHqQ5&Mk5P|4{@BL%eotQ<=nOl#xM$eDL@#A#?9|>vv)LUOAE@`X -iq$GqhXaXt!45DwIO6FQxUN}HwL||4GwF_XEzS#=8<8@OnUCWQH?xYWVBrbA=R#%ylFRZ&><^Y5c#A{ -Er3qyFjNTd~JF9NsmRM6gA7b_-3LM5t77%J>dPTZ6YtIfXIqfV??)%pSOPIzCE;{haSqhh -nS`#H6KhY6f7fC>~MY&U=R%O;=#~o2skp^Q0Ph6dat207XLzswsXL>p1i7*k_7(DSx5=h!{uFxI7OpB -h0i=h)UpH>eWZlSjtRdtn>`YGtjs__3te&m8@z}*m(XAV{ORUS*gIc0$O|HReT`27mLYtjoI3BMLLa` -3#~3@3i$SUYpg7KCz=_n>DguUPYO@u{s&M?0|XQR000O8`*~JVd&wyNI{*LxKL7v#AOHXWaA|NaUukZ -1WpZv|Y%gtZWMyn~FJE72ZfSI1UoLQYQ&LiLR47PH&Q45ERVc|wEKx|#&nrpH%qv#N%}+_qDTW9Zr4| -&W7N_QwC;)M0NoH!X9+#4m5*GkaO9KQH000080Q-4XQx!0vPk8|V0Nw)t03iSX0B~t=FJEbHbY*gGVQ -epLZ)9a`b1!3IZe(d>VRU6KaCx;-yKciU4BY({tR@?9^#MVhp`D68hOR{+2r(T9ktIP=f(AkUy_Dlej -JW7j4Ki)=$UB}JZ(DH6adALXThE=`?BrINEkB?S${J9uvp#~8J|M_&2}GaGvS#d{Ohj*_=B=$!2djmrD0hQRM|N)V|>HQ3kA^Y)iB7O?@kxlWnvN0iI2WQfnD$_>(lXs%6A{2~w;wpu-sZK<6=_i2)= -y8!1v!1KSLE295Qgu3B?~WgT9Kplu97Kn0)7$QD@%te2m{B$Itzife2Lpp1$_fa -J>)h(ug`!&2BrH!*nwlQ-#9&P|HzV(##k{)nq3Y&kQ)chb=LH|xj+u0Jc&ze~LphsTPF;&~h9 -dRU%xpzxRkdiK+0L~Zy03rYY0B~t=FJEbHbY*gGVQepL -Z)9a`b1!6Ra%E$5Uv+Y9E^v9Z7~5{!HuT+J!3lk^I>l&T2F!rF8cQ}5XuBeC@-Wv^7>SO#=;D#ITQA7 -J?;KL3NXd@VEcHV|9iID*hm4|#d^-C?etP|iL{T*0<+>J%{4MLst_8EJjVKC!Jz7&C*{C>{(>-<_ZRI!iCk-=`nWX4BK@Y?^+YemDcfD -A&SMFnxqhi3VpMFVoBEd3ui9f{n7eT=RkrMfBI_7oYFu)002x$-l2oem+STKYqR+j`u<1UtXZ#K~_*H -$sq%WXJ0d>-4~CwA#1<}s+=uJdc)LE1M{)=`CKY+DuiH>5cYKq3|XE+eArl9JD&mkmhHww;{gl0Db(1 -72lm09k9EZcF*$kT!;Ngnj^CLgTUu-w(ZSC_D&mM8p;b<{Oz`E-$;RAZBg`qJ=1UY#s#+0Vfyx?)Kf% -;SR8}7iHXk5DVOZ9V;!Eh?Cu9~PgYctO%-TQmbD;v_hjgxclU8L0% -%E9$iwNao!rqYz>Ejss&RlO_F|g~_Rj{?&%bAoXXC)(I;Zju=fne9_{yKbO+q-Nx6{TkO(F@UHp{;8HMn6r7eFhggwt4~iXm-FV1CTn|(6BK3 -wB)CySjnZtP6<3oy(HB1s1#X(Z@Fkz+f3?J)JYQ1pkS4;Jtl9+>!+$w3r5t6R!>y~8W0nWE9S2jzOVc -M+I<_tB`+XeX$5G*moIO>P;mn>ggn-P3Nzcan-BDs=_UgSnH8HH{fB}~dPvt>8ku*uW3ts@y#@U-tVY -g=D^|*3th3fv_}(?(=6o=cD_UYPM6C-<1)?1Q;3`d9uS!-d6ZZuqrt};HPKOdGc;10ybu)=ICfF9*T3 -vZn3YfM|eF|9@+y6t*G>XGiIv8O+qkSVV3-I7?2=3dUPsA=TBytV`T(|D>?I^Cl_y5=^{KQ-H@?py$2bW_j{z5v#xGD7>Et4792kbj|Aw^S`Ez6C{6-w -=hQkgF2p~^XT0Nx)Asry%zwc7L{;lCTDi{^*q%DG?_}NlZOK`pkZWH -}z*^~VnBE@p9aKajs>$)z0$#1lfN1|kD0NXCVOSJ5j?>Qo6747#+v>W1P5h6DQPqZtZFO*J&(1|e0pw -bvxJIaD%^}TuSUM?|OdOcF8x>l(++3UMaV>UFscDb|cmhuirG^{6J -V5Y2y+{aF*3g&eMy*VORZJCirX!OO^PvsYYQg^?hoZNUy`?s&`*O)_wJw99IK|7kj1s(>XpO;3c617}B)uuG(@@S0$ -_Pkwb8-KneF9BH=R3mmKgRCz`ji1Z&gqy1D{aUR`;`0v9YsGAhjli@-LyIDbF#HFoQ@s{!}2hGk(A#T -H2$a^pJ8ecU>j>d19u*0EQudqr(3BP@uY;UKdWE1AvdL@Nt~YSk=Zwh_B#y%h8Yeb;^7@ZydNv~{{Q7 -gg4Vbx((A4+#Bfo2>1;dry`Ap%S)Y^ES}FGWJ@t+X{UG+8-LJVXHgH`pu~`2l!iEGA!ds_sOI8(lqZu -@aG)~BkyRn!gKvF8_sQUN4)(t2g(C%+(2=NMboYxVBs#<`&cZ(N3*@7{WqMs*n|%c_88h`E^hnH1M%# -gM#}ubPm2RMa$=Lm7u2)tvXY+J==T&Nt;+cB& -GTsf9{!*-lPoV#7SfT&B=MXp!3CrWJuxR!_$MBd|%!xO42eQS(1gO9KQH000080Q-4XQ)1|iV`u{a0N -4ot044wc0B~t=FJEbHbY*gGVQepLZ)9a`b1!CZa&2LBUt@1>baHQOE^v9RRZDN&HW0q=ub8qIEuvMCs -{j`Tf*S3mIpolTVGy)5>t#)m3Q2k682;}aQZG{8I6x2`7R%wxH{U#Yw59|9)JuI2vg?iYaBa<3)Su9o -@Ui#OXrW4=<8F5h%DwC>{)oCYw(3RmAnwVw8oX3)MredKS~fz-ugJCNFg2Chqb1A=zcd)}7rPIp>x6F -qwAR?&Zr3Q`99$dQ3ID-pZ;h&VKp$i5FH}8RI2sFN5;=qeX*!6$&L`QNK)^^511GpcklJ~n6t)$C>>? -WHP8shwhby>Y+VDH6g?(ZrFr(Gee7qgCTRf75)Y5ZaL`nD@s$;pF7L0ny;0-14#(8^tOJ4pl8dvSa$# -?blaQHnQ>&09iSAQVg<~E~4P!4^uPi+(Lm#tHU**=TAO7WNfxk?U_oYJJ?XG&?zBp81}TZ6g#4;vqYz -B6<6m!Lfnj`h?H7SW;{y>T8}hx2_NibX$IO)>hvyIx_3)Qasc1)a?2hWO=bWU`7b0$vN{aK)LV)Mmv! -df~mDqI%fF)fLRLi7^R77e7#IZrtzFWzBqkv+c@&o3oBzwB8xPD7FL&9g)Q!f_qJ#H9T!+vk70Zy;`O$c$cAm)fO8SD8Y`vx(q! -3t5olvt-U((+USM07vp|HtVhHp&NTUVlOiwQfwZ&3wLH+A@}q;B!cq6qKc{cWcGEBs3dHG`*Rsl%dyG -s0PD58gm4T_bD@_=xt&pTd?E4k8vbhJ>`ceFjIxVs*fwh~tP}h&q-lw(KGfBM_SLqJEh6a7V*4W4{J` -BVpld=mC*>zB^{R>c-buJOS9Qy0eFdS##|#r^=;JkwR;nDV!XjjhT~^Y+`|1Rw`rjsVd(x5WVv&M&1iZDY-|=-g~K{Rd1`6<-$1(2No&}{UnM1|4JeQ7LX$1B$LLU>{qYp-YW*gDE -=8r5M#YW{JD@D%jDJZQX^Wqv5_h4@3P|#XOX6K>1i87tAks>@9)3QWrz$L_)^A0VbWG7cpTVY_nQVQs -OIEPi&l{9C|n%?r)glBt5}X{?$w%e>h9n$kwt?u7` -TxJp_xevY%#MtI=k?j$Q*QlqPIP@6aWAK2mt$eR#RFqbX4mM003Dg000~ -S003}la4%nJZggdGZeeUMZEs{{Y;!McX>MySaCyC2ZFAhV5&nL^0;Q)8*`63njyuV)tE>Os-33U3yca89qDf>D1QrkWeHY4Cl~jh!>M}J_7J8K1fkMvbc{y)hW&5F$I) -A9RSu_JmG*7iEiu^9FxYA-YQ*y=3rUD|&eZT(K#aVLt`t0{WGPm86(@OVgn}7yZSq3Lv%Cp -t6Zne+;GI;M~RH5CgHVNB)BXA7L4hOu8Q3MPzqDX)cYnL%y<+X*i2@kXrcWiHh<$=i%f4|%z -NEud&uqNoxh2^mTVpGkb9eAbu9guv|}nwOb)h*Mb<0+d9x?7IH>?FFBU(Fm1AC^l`hCY}rPmd3&)DM7 -SJ5{*WqEzX)+|#S$6MR}_uL7i&7F=skpK -=-Q;!b3b69mDfsFcv6G@5Zn%9*hOmf53ncEQsn9D}{VV}7BThVvDIy}x`5i$_EyNpvBNP7*+TLnA~xv -DA>a!Ppxfyr*&{G^v3H`S2VhCHF$-DxpEkT)0$C-v?te&mx)2G1r>%k2?(40^nTZ)-%}Y?dBy5*y -QOr6b0B*vxQQb&~Z_*my4H?}H&SOC%>EQMj7;r92(JdYv!=nIex2N}x_PNR1$DOb{@KP}GTGei1#E!n -Db(4-yAh+gu0xS8-o*I5JIb&ua0JfI3N(J~c)borP$-;#K{<#UbYc!&}EfU_)?TG?d{6Tim>&d9ZP5ILH+qk@$y8H}B|z=6&dqUkSrAZaACeqR7VJZ$z5Ia|W2%#n -}aW`PHkKT`zLYR(!+mgg4`E(<8+h5;|&N9a&h68-R6J8-WxN5Hi3X+3@+8aowYm37x|H4#2AyFSm|9& -tF$n&{1^2wDMWY%4@x(3(I$jVQ=W5`& -95B+k6(ul&f*aj*OpDo@+1^t!pxq-?%7!thUX_yfd4^{+h*vLUU1TUzq$gID*)+Pwu0)?zB#CcMKtmN -IJ07JC~tKkXGBkq -B!%`x!Uh~O#II`$CqEM^y*i#LoS9HX#lXFP@0T$DobqS%1M?uBE&FnGcrIh=QljC~3r@xW0v;-3F%U! -IZMN_mN>IeUM8%GQ8at`RFlyLKXIAIyxwD=DMbxmkq4zu_l{COr^HZR)25A^abWdeI{GA{$Kh6FW*SF -oChlI+RyNI@gDbI0ajk-*a#hox1?ufQ8(Hg)p9`pts=GfP)?xqc-H_B=M57Qmn1@#RG&bv;p+aN5kjt -r9WxlXJO`W1h_dILM0|Q(x=sPTtKP!?t!Z-$cLi1jn7N5nNn5;QJY+QLMhGkEk&Nep|af<0yd3kTkR@@d!F$Eb_B$fzB(yr|p&@B(UozA&{aS@7wRg(S*PkI|>cAh! -M~+_I`&iFnxRNi-hrHU>mJBGDo67R>HFjPmx0d;x%`-!B12a~Avs#m$JNkKa18{|=lwD2v&&)SBb$CD -dZrM*%|%rs8|Zpg#Z+5gB}r6-JFJ489CH*5G>d9Zd-s-$B+*MDZ>1rCUlv)BBu!#(w5Yh$%IAinU;w! -~k3fzGTSNUi@*9+SW}2B6*n>H6#__9w53Xf`##_0`pl*HHhP`^rEQ=PQi2TEYGYY?Q^tC1ss|oL;C)) -{8&pPLj2Ydfo(w?v;g8l0L6usImzsI>Jj63R;LiuAiOuS3Wid~!9xP3%I>hDcqO1cZs+~CX=-pVdsU{ -Vd0o7S?V^1oJx*_v^b>bVzcB2%+;>Yw;+@O(q4Gtyjp|8!+m`XxF4hh@{<%uOBTX$0TUKGov(3$B#SS -CeX`Ins;q~_2E4g;V=;rwLfcr@>qmZ=$d*SmH#O*z_t@OvO)gj(7`&bLL0mXM~8&Bi0?l8YqQI|IB2o -G)$1PwnDF`u_s!534A#|Th?orEg5dG`DkrcA?||2)6_Llo?1qerJZbFXmiP7j)ywtjR7A=ZMcbm7H|p -~s}JDw|9SK)DUY+c`F5SFn~v-#Zl{kflK#7@e;k|X=j{U)rzZr>Isx|KorDP71MN{q({ -xWSKRFOI&s{bST-9q#Ki_+JqJBJ(obYG_3k_Ffhuf?vro2l_CB_A|$4#7Q({6S3|9)})^?{MLYve-&V -;+}6ow6QzBA)rKx|F~PH7%#Nd*7q`v8Uz>rwlFcEnM0INS#y-^^k4p}WO(7x1CBeuiA|7&WuoeI_c?v -PNFQq?H+vt$%W^vpm1&4q`i7kq!YzAQhVGrBu`zhb2dLP{#eP)>5^p@dqbclJ74#1QcYRANm=mRKhLH -3jb_Dea)KhX1h#z>OBQST?-8q_vn2b5;OndI8R9W#4F;WbL_CB^NIz5Fcb((a30(k7Fp?%aavawJL`l -T(Mr?YKLi%4e*(CDFErCKaJ?&P1qpnlg1kywd)Amjlc4hzM!PR-0lVk4}8<=7&jxowNgtR>!B9Ch7XSEFN)wH?_aUy5R5o8Cx(I(*2vEZ4S2)Aasa!wH -gaoo7N}53d1fZ1b21`p648&r`|V0~9-hNM5ZM~f)=3C#R=_e~PAMMX#KyIau*}nrf2XxlfEP|o-MLH(Z09OL-MVtFCwAI#%zgmNC -T>7fz(ZiI0r=ffsSPaBMVY76@LMYho9P_e{~T0Hp*-M3jXKc?@N_m4xIdLJ_%DVW@YA-I8DnP`?F+2X -A>-IMCY!__SwjtdqSpCcLH)CN%@qf7qLH^|E;YBP(NsE7jV;QC6t(BH1q%vQ!J-c3Rt*k{RU*SzmBCi -&mGryqmEyp2;0M@oVf6;NB+=mNmF)x)MLZkU`5g91O{2pVD3M}zE=+Kc;FU6*@8cP;=bN=p{rD&ic`!ma9;nao7FewHCG`f7lm&OE!XT3Ud -HU~0dj>jM`g=UnO0Jp=Ek6{dr+muP?@EoGVH^LUIPB%1Mc7KNUn++pW4X~HN*9a3_`t*uieC2Xj*{$Y%9W(^K -wA*Ch@g0KXvo0qO%y^0&ZY`uZ4NhkdZ^a>L}>(Xfw>C=S%hQU_z_;(DH7vIu%U$S)FI%KCs -6l`V9S~6WrQ6K@jY6D>yLui5Z&3PpUbKL2KUn#D4b15-e+`JwOJVFiow8cG0%_!^vc9leo2{Kgo8<}} -LFrv!JZmmrq0|x96z>GsbPlNicnlPKyt7jF?m+x4yA#|5{)gZeN){WYt;Y%JZye5)lAwA&i48q@_d^4 -!E)CpwY|lk?4%m8LUd(~NX0dAd;3#$6cmfY7c!JX>N37a&BR4FKusRWo&aVb7N>_ZDlTSd1X*dtJ^RTyz5siewP*! -`_NmUh4M&w@OY(=Qz^x4q;({!NJhK)c>lg@M=43Fi^enZ?#whzW4`4A@;(v+GG`l5$iXRyhmcq>MwE~ -fu=TVjZq57y!j2LZjF691j)6{)2f!nSh}%fl*MP}^cnSq;859a|55@S2L;{!?syKGad?6=m{Mao*>&2Mwy{Q!REIPu~ -DtS&I4PEdSIu{Zm5s2`~u%|sc!JX>N37a&BR4FKusRWo&aVb7f(2V`yJjHW2>4ze05=w@Wuln-U5KSDJRCkt!?e0#iY{V6FBm)huXxI4E;yUv2lHvR -*w@A{6aH?X*@qUJ2*rYVT~x!m+|ucKUv)d{{LfDpm3HPMX|0$Y-~LOz=7=?OW+g7;97}wEA79MwD{6R -T;QxgyyBqrS@EzTt2iruZ~x*mt>=Qp4k#kx3Y!sX~1D06)%;~T47^d+4w`p-ZCe1k%d&$p-4$V3ItXw -0!%t~(IUqWGF-;Bbs7(nrkmAjm1*HUyOzRb#dT&YdA?dPJV}zo|9>)>sC=zOke-D?LR}_;vWDJp065E -(XPGs7b*SMA3zqq)j(3XUk2Lk#2A`(z>3y;XR;gCz)+bw*MBvD8pWHOkKSak$848=ngtru!aqWgkU&gFKm -AC!dqeSco1Y@8f=$7+2+*RyQ+wD7K!H0B_J;w;C3d)6*(HNsVNtN9Llcz-1@X15A-m_s9Wj&WLFciFc -RROd7G%GT%~Y4g2-w+c)GLz^=5&BsQEmDt3E2&JZM*0uxw6N_zOQ)!2^5)|1l=G4Gh2XN6E^-Ph%IcA -^DPBCY3uWJvQIFY(9Yus8d-&kBipxVT_R5klpYM_`Xc$?t_TIR1K3hMr_je=#{HBc7z??u615UA`*zLAY8z)2U3{mX(_M3i#URF=$SFgK)>Cvl71Fdf_d>y(8hdCyQ5_ -|(i?^a>Bn5ubZ+b&nHmTnai^{jKTp@bz$O|uxW|Z~B`w~}D-f%cMw89wt64V8-GWz3Q^p#9H}m~Z*cD -YgPCh?5V!6HAV<0l$Q&7-Ekha$9J_M%PUSRnHNNn(?W0=fnX@drkWUMR*ml!@%*R;Qt@KQDa@n-d*!oC=1WWz -#XTlreSkv^&KUy@x1s^tJrXCn>iQ%yc=HEMY;?;pmi=a*YTp)rO$qX9#Oy~4ueI#PrNq#dYbMYjbG3o -XQO;~qJ2}AREyy2V^0Y@V4+QMkCe;aL0uQhvHBj4|om@;LL{;NV}x6M9$h_+}VqW2Or4O{P(!(gkLB@ -N@7Q@W#+IxWpy4Z`SjU|@&;d5!rCoYwm^x;b<&ghqKwn=4#!+c5Np9(A9X6kx)O{=(egJBtj}&6t}%) -fTs|*-dyo#Cn#h!?!2w=SVycs;o=aTz%C}f$onD*`n5uh$`l74dc>yj8YN1c(EU=LPir$fD!^2Psf`9 -?urFx?4<^!)`y~jRS~bYLGYE#CW_X?Yt%OBl8@09_YO&PN*>udG}}n?2Bc3Brbf}kaiS@9UAn8CQs{K -ap@E7j75k3O;G9#M3=ezeC#rT56S}gFkJeA5i~w>vesd#vwsa$DVod*j#UL^IeyS~eoQ-+!=ZQ40=e` -tfR(}CdO9KQH000080Q-4XQ;LG9=Fb8E0CNWb04D$d0B~t=FJEbHbY*gGVQepLZ)9a`b1!pcY-M9~X> -V>{aB^j4b1rasbyZ7m+b|5i`&SUzVJ*;lz(r~t_-FSp}|)4%EOCLJ%TsTi7j`kcL*rz4R=qeArMLKA>$+2pd;9AklX8E2s -G9Nf?Z<@Y)bgGk$j@aaG;)VhfsbaJg2TD1vWVA8aXk;!&j?hEd$f+ElmO^{%=L{4D83zVD4PdfS^$g7XyEUn(z=Qd`t_ -g`xSip{P8kM#iHw^_w18oa2VG#nZp#8b2Rf#f6W)hPz7*=P<{!&DPt?3M+&CkfZL{~>^lYK0FN*(AO9KQH000080Q-4XQ&9y+WxoUf01^)X03`qb -0B~t=FJEbHbY*gGVQepLZ)9a`b1!sZa%W|9UvPPJXm4&VaCyB~OK;;g5WeeIOr1k*Kue(6_F}*Va@Ya -|if+*6W&~PVqHH!4DUehQb+P}wGn68Ul9J7-I>a_L&yU|cBCFb153Y4dX-x&bt))JL?2iAC&ZqFX_R? -ssQg~=;C6Z3EmNm-UIt-!xvv;c|H!773g43);BbO#W=*3NTk7b|k(XowtvVnEIcH=R`aXW1LX7GUViS^*f59+FxEWHpM)u;ptNok*&4p+3^F2mBsk -1;5dW1+Nc0qL9vcN$E8P2hffNWu}AUogF+Jnla} -e%Z0e*uw?uo*=yNX8}s+v%kPkDcq)X%OBx-5t=&4vqU@=DRumOgCN1Y9InBe$oaC7vbz1z#cg!EgR;? -i$d8k;MHF-H-}Q0)Utvtk=LY`v23bxl3|()2?SWGP)hkjTi86P?qOQZ%dnZ=(>?9dmLAmgWsmB!`*uO -BjMwmfGDED0h>r-nx6Wk>$y%DuHY{ULBQg16>Z}AG#xrgKa_Hj4S4ge7cQJFsC7cnhRBtD3R$BuCryT -?pq8teGuHKFg`)6GE3rXwZN0efhm4vyu1h>zYN!sx6ra^8nFa9 -|yk$@sZ9c-m(5bCV~5|FLqs@`XmC^u9rpsaspt8YC_q!$c_+Ehh0*5X?EaHBp2ZFc8mSO??SZn4O{11 -ZX*j5);)(!=wt+S1%FS0y_}k%YNcji -*Z4>arv$UrtqBwyBud+WLFKsZm8+kY|df?zd+I1%W)kR5699M|@PxtWi?Jp3PaH^n-$e*aa3Pn?`kVR -O7`I*!H%En84ph6=hT1h$p!kU0Hqo9Qtd#Dwqz{MoTd0H8qZ{2~VwLn_uck4_as5}v -o4wQ>d-YqOo3a;B>>pNsMP^CbodR#DS|tE^(_spm`HklC#X?!N1b|-&qRpoiFsfQt$LE=Si%v`p?rFq -lBJ-_F*c!JX>N37a&BR4FKusRWo&aVcW7m0Y%Xwl#aC;K+eQ@q?q4w|g~+W -fC#8X6y1S%w1A)>m+mL?P-H5StZBHX>#LU>cPMZJTb7!oVt!&y-s9`~x` -M75KC2%kYz3^qjxGNNV1Hq6;2DcCb5%7>8%x&G_cZ3J{*;DLwJx*tmTfxhGePARWgdAXrwlkHcl8CEs -fp?x33@)htr=_<|g8`BO@0DO~lX2x=~Q9RF$MVDsyd;N@nlnid{OnvbFIWx$5ew5^hRaB=nF`<&-WP& -DZ5l7ApUpBYcGb8SiK`$_k0V{ONoo@B`kL9Ud6IKleu|AEk!G8G=bNy{YgXdpZ9MwBN-SS3j`x9hvj` -`8@gNVm#$HK25&97>^sHIQD|YRu%dAZGQ7^ddKn$e_HLmL2WG;s(q;n>HJ&c>NdGw7k}O+IVhZ}a;e8 -(0nwzzN6FjE>mPsp?J}H=LuffIMGEeeOVij=v7GqO8TedSXU1ZBE`HG^%^OclOZ_}IQdtzq5W{okt@K -q>yA%pJ394eLO9L8g -dFy+3$i|Ctv8@W_;^-s|=#gPV2p%!xZu-oHkn{~ZyuL(MxVkw)aJo>X3}Yuux;3zHF`QOP>c`lb_}Hs -V%(M7mBK8yUVeGr(0ljiU3k&v*zz57nA6i`wrg8kKZSxhm6sqVOM|s-d@LliQoTu*%dj780>(}JACbY -5V=cyGQzT~B0F`JNXv8oN0Wxtl>;_L~FxhRz^BWn-=^;gNA0`y5%>!JJcI -Uxk{C^I1q3;R!yqB=1FwYLVtBI+;zn96zq0H6_Q=O-mw^}KzrZ((=6GSkwq^*%x2$CM;rsCzM{DoK`5 -`W(x8Tp;tf#vrw3);EH7>$o3M1{*I{8ZKyKkM8)$AXrd15>Pib$KPe`}Re+3?lgbW_+nU+F@WvSbgw+@g155R(xoxl%*AUS#=~R>Bz}q?0`J|(>c_lx(tBK9TpV7>XZZ^ -y=2H;_M-{h(dCGC7O`L2I_j>rG0R2JgG>nus%I)X=%Z6plr@IRq_X}lO8`oqV(anG#E8fQI$vFBa#Vf -X!cSxhw_1cXPiL3ZaZo(K$=rL1=p!l=Va0G^?O$41%dBd|&@nyIcsQen~fN4A4Cr@MVWzvQSd~tFG%b -iwv+o2^ot)kKbHDW7C8f-$G!mAX8G%IRCKI+nc+bhX{hkg~vnx?}_;1uekL?fe4);#uFAaDwrm`5?h< -Nw}refxg+H+aAMyG__NY@D910^T5<4y^xkXxG3(>td+EKm!loP7=W@f5f9l;2;dR4sD@F{_{c^ppm~a -`^+TD;w%0SjadbYEXCaCuWwQgY7ED@n}ED^@5dElSO)RLDy$DbFv;)&+7BOHxx5N=q_xGD|X3i}kpal$5 -vtP)h>@6aWAK2mt$eR#U4zE>cPj005pZ0012T003}la4%nJZggdGZeeUMZe?_LZ*prdVRdw9E^v9RT5 -XTpHWL2sU%^vwsNGZ46u3{u?E$&8X@mCKUYnvHf`uYWw5?lNR7GkXqv(IX8NLoFd3T$hfoyDv91iF8n -IW?*`;M!#81LB2Nga8w+PYG$=-Jo28~7+!t5>2|RohF}_KJ`Ds^)C{@2OMm7vcoZ|5eGh#rIN)e%d*v~Jq==1Y#O_ -+-4#PMO1|2U;>6TtK%(~j%l?wHFWOq=?Aunt-|q+9P1WV>?KE_JWhM>nP?Yz)Z^T&mg(}#Drvu+%XFO -+LzkU0I-{S+bPq0zkkeUCZd7Q%+If&jdyEt=FZJ2oAsss^?b&2dycD49nCuV%niNk?x9OiusPxyF!#; -KLH8@^sK_*C*PcVA%iFsbl!08N4-8uMi2Cvnd;PbDr;f+EZtIdrWuFM#h()a3nUiJALa7WZITpcYj7C -^{~ -%`p2O=zS-8&e$_FEWTy!p%){62f~1FCs_5?k@!;ohptjd2G0x~!+CM8#vaHSG2EsVcgN$vcJkK_p@LxYk0RYPd0y))77fsdu>v7lGf82bhGu2fs(t;*(6arC!W06u;>2_Pn?2o&0f -JVf8=T!jSnl-1Jh0f{?00PXu;CaW0|%B%R+(fjI$4&*j1fZjMf#nztB$3fC7HCZ?|3|qgfEQzS0l9qG -vz!BCz)mZ$M^_i};0~V|Dvdsj=NniW3WQA9Rc`zLPwDZ4IE|yXmmbzT!%yF^@_^iwaeao8Cl(aEkE&! -YJx4C@Y=-($jOm!ccT5qL?)zKO9&%GP(KbJbtw>KFHgyy@$+&1ji1=Z@e{W62Yh{|tX18?d$yFS*Dk$n%;qpkln|jIke6Xwl6QNM5$J3{HE!8X2y~& -h;{>v6`-XqOFfU2Sz?`e&#;s-9#4QXWu{k2`!jp!<{(C$mtO8@gkA#j@BXEML-{b-xt6<`9zxxT#*e` -%`bihJ8Ky{g=L0_ONj#(=5Rs=P7Ublo!bzikzwd+6@XU@)T2SIDUhuJ_zR`(p&I2G*8J}g$%Xa+JTRM -ZhTxUUc@q2+Le(={X_JizaZVGZ1rz1$;@@QjeLU_ysq3*xxU4WX**V7xqvscZBvGVlF+WR(OM`TjlKf -aFCiYdW-4qQrs307z=~CJ;;vz1S;pZR|@YBhXl!Y0!p(wZw@=j4dD^h~}CU`}xOj*{M?X5jBMGtt>#H -@Tv$=>s3`B;UR>b<^ulTq9`s9Uwb}kyhofEeKHZsb*S@%EMQgv60xVnsdO$Hf@!4lJ*)y9s%+aNNcbZI*<6qXT -JTk-@$OMjebZc%V1gtt#BqKxUty}5aB~k%9Y -(%yKY-OPf!+1ADMql$c35YhWgGT42LGk02gVgrRil#U7NV>nxVfH)uA>_7E!nh*XFT;Xw!&{2vg8~u^$_8I#LtA|;h+Ga&Sf_GhK)IIL#^lx5@&%3{ecwxn|O!UOl8?V!on?B?8Elp7n~undn4#?2HhgM7h8i@9`W#lBeuQB%+-&^kfwoI-ix^k7(Vl6;%F61?^D_x)B(_1IT5 -LSOU@@9lfMW75%(gAVi>7XnS*5#G~)HGB6eu0$ -XqCfd`H`GP<<1q448RsbtkjAKKsO#nY;fZFxwBdVB>0|uV@T6Z;YH&%}oYg#N>Va9UVu?)#0^i@8bUE ->f&V#ix>Q4r>Vt9d^6dwUd61w@eo<*N(7*+&avnz6|f4W;TYV2ol+>Ol{0Tum3~eId*n&rWTb3QmE=07*PZ{w`ww{F<@`!V_P3XFX2ebsHmYp7Pq+6N6(z?CKdBjixZD~l2K${(_F3d -H1I|@gU>b;vAZ%WbY)`P(1QZcPIZ3JJ2U$>34(8w?$8Jj7~~G)1*WN?Yx$^X7`bgc&y6}y$a-W?9njvJRxKuk!$5>I_Nl2)xS<{5-H-Pmu(zj#zMpo3&9ia59x@GiM5}(H -g||y(V+Dog4QYIm@VLgV^TCPgoSjCnEobb`|Oo=E~MFsq&KDGk46H&(ZKzCHpdt5*vGEY5R*% -k8a&@tVS31`6Oo@U899m~62nWSz^fr@ABDKXodcY2Ak_c;#pgiN9G=J99r>Y^3TiZ*vCi -BefU{Z&W_}6tH#6gb>E;=Prm8k+9;J(?G>HGS=6NMAz_4#|FonnT!M@CSK?y^{kyHY1jiWEZ+J_Kru6 -u6;*OJiXkLcI?>}Lc3$m(xqq2ISGi<&69W4d@kM>>tU{i_>&A@+ny^ohj_)^9jTU;ltFN!uVi|NMb=f -B=LJtitmJeE(z-{zZc_~{*1MMHc-u}o672ma -EVPpn$5krtE}wmO3nIwbyX*pgvp&1sAS{7jAwmWZ@i9^kTYpt0XPiKoyP{6&*gqHoKZ+sjnCPgQ3VZw)fHmL{*`9{ZS4k!^&-w6Y5#fmRaTfH=CEE)2H<>Vk>o04)w=`4yXX2wdCZPp9F@ -br&cw&*1@N3qN!7^!F-H;=J__gKhCO%h)Ht|CuW&_oMp+^q9^hxAR;txrPGvx<7SU&q-@_$R#h-N9G8 -$}haLlPYbAOJKSrz_mkw!?k`Gw``RIl5jtPNhfZcC&vXmnRT3+ScPpG}c`>7mjvHukaA2^;l%m?DVl% -xo}SQkceUhEw)go(x5())5+d6?3F#U>bR@_e9Hj_^xSkxd@2yrh`Np<7Qt#GpDY!rYm1)LLE5S8qVdD -AIv19M5*J;^u&8O=ve!+FU`C3NuHrCL(Bw&hvoTS}5~A7jQLKemVM@^_VCxzCzGrfR;!eu_)HxROU_J -4%G!${V+OQ2aLdSI^*czK*w%#KbDhxl^&&zs|nHtpez1RtlKt@8#dI(;zu;D_dCU*voyfsCHYOY^+NR -jVja!M~*B!xg9be*abBQ{dg$I`}>ta%ntBsU1BL%!b3rWGIx2v?V)8U{054s%%SMhA#M6#Hb15XT|`Hw)SRp_^Hn83ZQ!E?*uMxo8lM2iFu|Xod&xnCHh$5L&9`|Fh&| -3jBokvPfvQdq56fTxI}3Xggdzzt`@ArPmRC&()cB*6jYp16VbflQ|r_x$Zb0mHZ4<6R;Co_y?H6ic!JX>N37a&BR4FK%UYcW-iQFJob2Xk{*NdA(IlZ`3dlz2{eq!hr-*-? -*$)#D}Qh0*LljRgsfS8n=#ZY-gAK_lzBHlD1Uh0=eYFGjHb2dv@|X|9o}z9a`QF2x2r6M^v7Bp%u&WJ -kK)vsI3R>vfklS~=V{Cy1M+hS}tWbgU;(cXS@dQxfm)cuOOz{b=g -2T!BR+>D%)tqj2phAC;Rd)}63fmUY;JaCJzmBb&meo_0%jC7iv5Mr*mtfG&%EvI=gIMDuouuOMGh@(M -1vq*N;L?vY%fq??SPM2FoRU!%3!x+0c3iFf@~E4-PDk}-tPVMe!Yl`HlA>S24%`KBRJ@Vcu~XrA_x3n<60V~_*~K^f)uQq6?ebF~utpe8rLwG*bcajc< -6@3tF;}^JP~klUF`=R5F0nccq8&&daD)I}!{iWfAE*YtfxhRKlmJo_TbkJsMt+QNN_%K9O-c|U&=w9> -_&{#u79WBnHF-w(x-J~bzFGly;Jl5x1UXiQL=5e=Ftr(KhA8qvWVmfBh?05=AuL~z$P8$Uk}VyS>k{? -CR*>@*aML*PPcAat52=5}M_gN{``Y;nxQOS<)14f5+l<8)!C2j^3L2}Y+ra~EEDd_H8?;&Ep2pko_Wr --qI&iA@_=I~L}}?zB+F!$DW@Dgu`4lxfImviG?0#OZjwZjK(^&2|I$f%=KKj};<>w -ey3Zz-P~c6>y2w22~Mo$Yr+4%dm<2hjAO0J)OP0D>h8r6DAexxtz!@k;EUo{!hrP=Wub7f}Vq{kzJWu -egOUC?4LuQ89~p&e#auRdjyw$8AZ=R#VS(V42k|yNSS|7{69>F#$j>P`@}B$4Nyx11QY-O00;p4c~(; -|#ZZvi1^@tn7XSbu0001RX>c!JX>N37a&BR4FK%UYcW-iQFJy0bZftL1WG--d-B@jJ+cpsXu3y2aC?I -XBkOKW=zy-E6U4j;A(6}40AP{JYwzVqW^t&q)1V=5;xgCO+XQu;vMhhx#y0J21rDTM#N*NU(dXXq}PSrmb0IcelLFi(A%ILgteFwP8xDd@U+gE2rZ4)A{V=d{!KvemVb8T%P@Ll9A6$wW3i2 -ExduV;DEfWdF~u5=2Uu3E&>j -a2u+rtDC|8^2MvbX7s`52grPBur)>IdY|IJWCMN3`Kjb2%YHWHws+L~*2ucHXz$0V6L9Og@=Kbp0qC> -pwEMUh5PRIvpq;8U^Ex-Gm}ct(Q1CLTH|dG*$V@(RqnoCAGiwN%iGhp#9fE#B -l$Kd!ymUo)HQ#EcoS#C|y0G>`SRV@-1QsRu!kmcUs6|PpQv>Q8=>&ivBweD>Ys#D6Kgc5H48_z+++kL -2q-w=Rcfo6c)Fd_crEzYdnDk$>5S>Y=EA5XM)Si -vh&PftTU?GzDi$YE``zrEM#aUoTIuek9u%j(hu_x;@VBEeU4 -K(kSa>syCu=G&hNy387$eP|y2S`h-F2`$%He3T7i;jE>tURQ?#J!lJHzf~$t!3EE@$Y3POf>Y4*a^hs -7O9FDzNX~3W3_i2;-WZmBe9g150rI&36-~0uktxnLpz+sz+C1fela&HsjIqk2>w+2Ef-J^-k-5I2NsbI`+}#ij(C=S)LKCrg-?l_j)&gmvuZCKihK&Nk4T#s;&_}-IUj(Ha^3pg6 -pD4`d-iKMHc+p4}kwTG9%-%i3(2PlY!E_aMvd9^Un8B9~MXOGYTv&q3Xe6V`wN>(4d+nIl@(B+de$xx0CsVn)KIyu&jPNprfH0gM@wy|9Pk`5k13*}1 -eo-R(er9ql`=qs=@4?@rCV*`YE|1HVvRuKs6SFtpetSxx%8I?}`S4nOqf`E8ahESKys$Mpg3P($)ikF -W;Z12jI1+#NUqNUeR^F3^WFLL&#pTlU8eF$ -2(i98S2n1T9Y&No_BdI3NV*mG>8B!7@%W2w&R+i@b<2OS#2!i+g`M|zhe`M7+q1d~-yN^K-%w|c#iy} -$bimd~DdCSYKxE23vgf^c`-PCoZjL3qB1)0<)$yK#a9(O_%Nu!G3em285Y$LTu4U>B$cfXQCqNVx3_aeI!_=Bs%TlnJW4z(<|E!9PNh}avMg5ql<6gQQM* -n6-%VmGysRmqEp-8FT~{VWGc{>`&5_D#g#fwWk{TGny$O_W)s;+m%qd<$CR#>hfP~9%3!D29j3(M_GTeZ(d>=MC0An#ha_!PZ3ibDj>-#m1x6Fqv4HnElG -Dx`JiXBSz2(d*$V|2UOlCP5;$MZ82qV$0T?rpvx&kCG5kvgzD5jwx+mw<(sO6d7QY`w&uW1FxD}>RCF -9Hw8>=>7vDj_(?F8FO$Sxz@?DOsWct-4ZJE>U%D#$yh -&5j|yK*!twx}ca4qTU63&bVgJxeY7+`H8&+qm|&nrqT8M{;{^lF -t;AIA&QGi->%LcK6_t!V9R+XvJs)##oo2`}KCgF1)W7$Mt>Kx?*vOo>VZCNSdslCXd>|4lU4zrkel7K -NJmZW$L5H=2-RDJ_w$jDE&IWX*=5>wtArEIxPcWd})d-(`usMhE6u8T&Q=!zM`x+vsBS1Eg5*CNA$;a -vv$@$lL*F8H$?6f%nWS=#`W#wP4>C*dndqJq*^E0k$&pIp-8(`8Chx@yGup(-sDnCl?syXBiTiY7%Vj -l}TrKKe8Zi3ac_NsuH-!Y(md6!~|61QLT_mS{-J5|azXbqwnykuc{&Rf@hLc2L^D;N-^m({>O(38hyK -P3L{Bj8HX1%KkGHf&dW4m$4;CQW!aGwPL@Xoedm|6WzLCf4bJi$KI#jItqmE?nqi8$}IrG3WGpa#mcbHwKwFt|BSK&0SQ_phOB()bFy$V1ocj?8Z6zmZWX^)k!3Z4?q{x!_?!? -fiVQ`V+Tv|}wXQ`;r$#qkyl8VfsRfX%_!w{$1chXl44hzx!D7hqfsDL-^&j4KAkhw2ULwz -mn&{iYUnrT|sc_a`qN`2nDI6SwH@0aKth!6|KoL?^X<rKqn4i=~~G7VLXXo -2n`Xs@%*5v;<`e0V{(!%^^*IjV%J%dDnS;r~zE1y_+KV=K`t=rvfZXQj{r -F9rPDT!U$F=QH}Nc%B#b~~Q)`3q%a%Z7vvO6kYi(y!i@`-dA6(ZE%3mFHR*s$tr~puXCIJ|lzRd -DyHw0$czp!QnhpX-Vt%uzXh$v#oq -VC{FNgs2z1<4s}aP}?Ai>0Q+n85Z&H;Lkrj0q0A0*(e1=wb!{I9#{3H*pf*;)2~Z*1%CEAEk~mp=(R( -~GTj!(6+vmR5olxY)dp36R~O?n-1f0fM}B+<&TLnCdi<53g|zm}-wt=g>=SlS-_cTOW8$WjJ=|HCDlM -RR`M;hl-sC8} -7?F@1YIl(9)sDB0zOWu44FPOBcHBVsX8~vSlnm$5L4{#7R>K1Z%&Fd5*ct5ngCLv@ZlngPC -8Z_ySl1=KU*MTrRR!sU!|8&kPYj6m{sB+6-d#X@3E}>Y+^kyRHs+(iUa0LWGicA|^ytpu@Z2Y?gH^h! -UlSdU@M(mFVd>}*G3iEm->p^R7)|4AkcOUupB>so~js$tzlLs -7D*7V~Zjw@EhdU@_$^H>QbeHF`W3Q3`ySxM6x#xbR-d1}0+Uxz{!Q#5vh7(*yNQJ4U^exQ|o7nHG(P1-3X0FzY)bj -EZ&T|1VzXx#cXJPZZ~B@SlBQAESve+D+`pnd%I(7}9^SP~lKwV%B!Zz3My~4wO@K+-HuDZc~qG``u^4 -|JEbgF6<66_%E)o{XMaUOwUPgJ;lvjgv2qQ{U1Wb#B)VeO?%7I(TP5hvWGMofKFqx<=0T*%`t1PT5m -1#GmissucPSX-7xq;txk-P6%v0eie_j9YZvfA%tGq-5I?XtL973J3b)*ITd`Bg7>Ws!Kh=za6GF0)K6 -!(Ciz>Bb)8l4zhOtFqV%>`4T%&(8DxT)a|AH5Zqa#OaH2F5W7Q-#^DYnmHF&JEBRP&BZklD^Eb$n|hb -w&&M23CI&Fb8%7T@Pm9t)x5U+Ri`O@UnPCzq8%Y;c7MWXiL1PzY>^zNkKy0|s!46|HL%Y>K##-7{bMb -FCH}Ag;VheW38UXK03Hr(OgLym`AEe$DIS6_fg09LsscPJPdk8Hru_)K7QV)`7JX~+d5`qA^Oo+_8!l -Oeo`W~Z)SIehF@*Ops)5Zy~u}ApTa6o%eZe9dXB?(XzdEiwJa^b>5a44u-#cujqR#7Tfah+9>%pX))< -Xf3nqf42jVW#qXxB4xbD96u|PxnOy>zYYV!vW7zlL{&sUdo!7kYLcZ`I2{$@qTlk^G>Ys5OloA47(8CDY?gp{sD>)K<7s8IIw8P6 -Ne|0_gX+lIx<}FUHv&C{M0mUwr)VI=cFB`P1dQ>oZnuxBfxyaFW}Jz`r8_wOSQP0Yyg`(78cb5X(Nki -SL0dQ}<$nPx(ya_Fy}Tl`#8@f?;Ygy_WJ!Y^rK^c5<>*RhaEhp8uoVU;q2XjG;{fdZ=-T;4AYno7)Hc -8}}GzgvHpy=P$ksPwBst@5dgWojpH!{-XZ~RFrPJcVKG!;0RIysnf8KkMUmHgM@Ywo^??)cX_jsS_A4U=s_Cc+b!#yZ@kV^@EA=hY7HP4mt?pqxz(W%Z(uYe#I*qe-T{TZNf1?tSdD#2X5B -QvAv9E;D&mF|pnx0%Jj;yc1jA{OfRez3>7zJ~BjEgPNxK>`4mOhV_wtD({0$B&n9Uq$?R`6_T6_jj4!LrVN9-qZ8 -l?iaZyKjcD{3AFenzQLO4joQt{PpZn|rJ%uU4D1ZA+>~(DG)4`~i^~Su>!)3&5)f?c)(o2m?=Rm+m)8 --mBBcQnhx;h*b(E=l86|2u6UYCG%PF!mvjPqw{rw_9jYze(DAprDF_DXEO_FDU+Ch^aTf=CV0qS-;ir&S&HStg0cIjh17(W< -697g4dnxIFL|eiePIfu;?t+9G$LnCSQ^^I`F={8lR}je!`cnM-&pAg{sRm -#wYHMF=Fv{na1MjOg!B_znc%9|DkqMdmo+x!&j9Cck_{R&AKpVli@dxX8!w;1Xv$O#?FLFFnM3b3inR -)yK?Q*nRiH-h3JdFuyY+<0L+l)d2rWUx|#ZAhD+n}QRTsmUA|_?`{$Rn?b=Lxo0&7 -)&iw!#k(^+?)P|L~&_Hg=n3c!E*JZoil2x17_>ejyi-LB|v(1024fYWW@UiNpm4U%2m1=7rHfY`X)U+ -dB!lo>#rfIpKS++qhmT4#~3aqv65djI;}_4V86m)9SzFR$L2WjJTOtKcSTmLW_%kTIFgLVn^28Yt7O4 -rUD}^&naop8~i(1-z*pWMN%;P@GQkG$dyj!^e$Ez@-$1Ct%Xi%@E12j6<65;c3Mhy6-n$fe79L{!Of6 -l_5~k2s-mB+gr;F54nL;mHVz}F^s6Ft6fc$Ytx(|`@#0fP@CLZd?5}E0;DZ8c&;oL9-Ql&S63fz=Au$ -t322Mc@Z|wDVlrw79&BQ*=N-hJiISO?-N&G(e->W>EDf&0w>d3gDRMQQHF^I-qAESM>bK=HGxK@)@#*XK2YG5@%``UX(>TX3Ej+$LrT0qVu0 -9yyCB~&Tr0JCDX3{C+ek{#CSEoCVsFNeB!xH&~$f8*5mH;$za^F%-v`4iE1RDgU~Skw6p7iK3Y>xGZ* -v`ruBBGr~DKu5)4X(3%!^Ib54K-GfE$oRdg@+dOcwc4MVI?OF?WOA;Sb -J~fCEQedMi`h-%j%@<<`J|-wmfO;hx2m&7o{L3wU662$VK*Ltm#3&4D$zx_(`jWcW0Vbb%Z>#mxyqXEt1xJ0@+TitxvMCH3W -%D3-s=q?-32Kts5s2~zKJ+scS-2NabF;Z8(!5`b$esN -tG|Mjmc&5AH8?y9g@Sw-b1UaJz@+-N-nl(&1vPTsL$zJ`G0bUvOdYI=%q{aK(Kzhrvx}#ao%Ls}0sPG -|=jgLyO+Vx;p;Zk`rqjWrx#0_uQI87fsG>Loq1Mav98qbCayllEuj^Gy}e&XiOpYlE&u -VF$`-VpqKJO0`q(yQhvxiA6@`9n#8FNImNnPTQwY1ZP~V`l15;FKJFs`*qK&>m5a8p??6vbU`0n9ADu -7xy5E-ECuhpppV&rRwEVi>B!js)6Sz^+buF}%Jy~ubmIMnHd*DBvu!PJM-gShqv#MJzlzH>%q8_!_(D ->$5d}m5JoSh~_vUSB?RVD(+tO%6B|!jjjTmmSpY~O7c%P_5>|1QEdk!r%WBb+pV-MkxYj7!Z`gcYS*S -3$7sn$16yWFa_^@IaPt_IRmE?;!?49WX-IJ%j;|Ay#H-Fbtyr`?XkJLoaJixU$r#FdGzrJ&%8)Amtd; -vAKO?pm6{oP3}rh8D`M09PzC$xT5SEP*xv2m9!0ySTmc@jtIBu1nS> -1xKYgrhd%=ML*jaD_0>nmKhCl()5|yS3k$|kl7M9Tiw!u?`Wd>^}USVI}jIY^l)wStI5G3 -QWRRtmwTMDM&+b!p4E#?qdC0rVA8ph(h2Wt~Q$gnfUP!|?f-`zVGFHgU*9v=5KPm!(1X@?5Q%j`{0;SA)-VDo@j3^skMTb2?1~3q5Lw1q4;aG077swy3Oq)BELEiOiAm!Vv-l -cLEEh?UDXm2{GgH^OVKXaBwED@Lf*w`_~K=Y-fU0`^91G!V(0t^zb}qb*rtVnwJ;?xRLUwL6qgm(fk| -t7kZhwRTP{MjVFIc?E7M;=w6Xm!;^T+6o@8^tNAt8+5F2VCx5o`!DZJq{ -S!q4a||ad3$X*!5E^c-)N*~Eo#b=C$c?-&>myn={A0ftWZ1Q?&!H+N!Ep?0?bp|bpd$GK~^ZMXO+8IX|0Jt#x$TbN6+>r;AaEZq2|Fd -NY1%_@#s1i_^_m6D3^yrFJXGgd=h6DN^`bOS{nH3;fQ5Jm2`V~4gD*@@2tX1r)skZfL7|Mqe9fmI!;} -2?qA=SqdW+g7|^2AH8|z5*)X&kx%)%4D1#8JX5@QHBfRy0iR3U~AJFiNtpBGE+?TIbXm}Z`Qrj5^ArI -y~TxJKSo7sKv9ie=kGfz?*94QcTPDa7m2>{ni5zahtq(zKG<~=ufNQ^j5JM@^+$DEZYC9@;jb7}J(O^ -{LVNK~WNmEvG}E^_%8`7VCr+5b7Gy}$gOd^8aQQ-mbVY-}9pj)&Men)4RPF+Sk9JBKlB_X7`f5opglA -X7KEVse`R6so3%xjqmw4G7y!!2X5!#_V|6_S$(}e6hKHHb9iZCksw}jnL>6iYmjV^zX#$D-5j%;MB3`?oW5_Jj||XG38fnH9#n9n}FEu(M -IM)%DWG+O(bELuYU0g7sD~6JLwKXu3msv-R%AwmrPlp8!4Ub4PQ=+vj&9)EPPHUq9_xFf>?)QT%7d^+ -^28&NrOKG>OZUo3)LL$Nn=3gy|(eaFQ<&@9Yc2Yjl^w&&b*y%QbE4)4f;IZ98wNyH_$xjmOjNRv%a!( -~bV;Iod!rUEyXuzH&PSgVCn=%B4h)ulYc_dmL+fs4Jbz;ruQ~uPN5t$SaBCZdEQ#XEHTFmzimTj$M~L -hiB#`D4D>JD82Pk!=>h8f&V5GRdD#rl)4!l7GBxGU>UUy{C2>c+C4SOqszZ$ztFJheC$#4F2LABg6YL -yzQaM)e1{O*Rg-SgQo5$?=IJ&jKf4nny<*QgVg5Gm`mWFlY_#V9kGy|MO1PeM2kOj^GLJaNg_xmEHnb -pEtLkgh($BB95JueMAByQ^D8sq9qG6Ls74e$Vd4Al-jpo!0Z>SX>q(7Awx+7s}dI5s3vHA1a?eX(Y-m -Akklpiw7O_{#)ZiQIxxfpJOx9k1bob6bH+T!kH3?TAhzu477KHtKq}Y-|UtDz#4y8+Rb}Epq=p{^N-F)M7a+;gcW*+5P*V2m)Dz0tV4-^*V^LjFVuFy_1Uo=nWo8}a&*vH739H$!@}+ -*xW(QK**(e89D!w09j1GOS;F44m6zt-Asr4D<7S4|lm2;>8GTNd<;@BCZIS8ByH|hsAP8@+z3ai$k;W -wY*KmM8IE)<)g?^QW&V3RS3Lb5JRp{QE9RdS^?R&iRx)`DjWzq@m&A%}sGZR|p&(8b8v3h>Rg}L3Xwq -}FX*VJ~$;kEik#}mI|q?gJUMLT`$YWmYWf4l1>{aFF#Oq@xdt&6IXxu76pPN&T~tZh$iGBR~p`sES>s -H>xGc1UlEI!j%BnBv>cDt~-(7^li6wsr--QfQHrt%)GHw)>ln{oEJj6NyT3!fHZQrWwO9rt>7sO~+-j -;Zzzg+CY{bq;FUdn_#7Tfzjq3&Fg12<=Dwk*-*yL?=nbm(>3Xg2)Rg=_!sRiHg4jt#-^C4lTO)XkAM? -@i~;9;(~!u>Wb<|*Yx)qYj1wUbDz*t<9HIxUlu5DOBIptOqGmV2%q0CL;}q@#HNHJ6JfI1bd*@EiWB` -d#a3mwVO<9MoA>Th@9T(P~b>IC24W5O)D8YB=zWiV|=uR-ff8*fVq`dWO%_~nfyU-N6{W@;9N_wEd=} -4UO>9nl))uUM0)?ZgxCNCl$RMrgOPLs5q@8S>pv*TXAQhG;XY1C&@QU0`FA+%?AB~*84b6h^DJ)~bw{ -ZCL!0|XQR000O8`*~JV(~nj$y9EFM+YbN$9smFUaA|NaUukZ1WpZv|Y%gwQba!uZYcF+lX>4;YaCxOy -+iu%N5PjEI43r1kfFvk(d=ugVsqNNA0k?=96lf3#OL8b}MRM8Qr4=RczjtPrcgb;+>cN(|oU>={$FWu -^*iA081;{j4+QO9#?FeoyYGIWg3}SvIm-%G=h*_lcMB_gN8fzx0iZC&i-R19h57&1O^UIqLcfZZ=Z~w -X;!Fx2h$_%zqWJb`LNGA9>#N0|cuORNwu9aS0sw~5hPp-9Q3a{W{i4dzKQEO%zsx-yS8W33oHxCbgz! -F7-+Q1q$g3@v!>4szjB8o7AU_%gBiXKI9Z;Lp#ATwA<>yYTz;bC}ua`@)(T%5cPg45I2r-##{b9{3)J -v%*$gTtfqC^`$LN0Y%TLi!V-FbMBL=0Cp9LKP9K8OVr8NxLC7#3H~@@M~4LAi`J_34O7OK!md{y~#Bi -Lqv5MFEb&JrGY@BSQ#cO;8AFa1g>IOi;W?O2y@B^L#PavY7Lg6WKch%1~Lv+7RyDUF(TS9hI^G(#}p` -;B#Cl8LGyUag`eOlzoI-@`+Q-iCvNL9zi0vOTekjRaxy6rj(QH0?iX?GPt>ZcXj>#e&)vGM`QH}=*K6 -xfUm>eAQ;YISY=}PG#Cu1@j(k|a85ku#tK`QeQxzxJs=NY{1eyd1@AP$#CeNlS| -Ubj3BZQ9Ry8dVHDL-eUtGyZ4g -N~@pS-0}bsPK1EwqP$Od98(V?qhI&RZ?01bu^|ndYw9&LI0Hf$xNx5ZP>b7tD@q*mk0`1Gs7o_LTz;8* ->N{Q|$PIy6;cHZvU_Vp?^VIxCCVO1)~ara-^x_>bVR_uyZ8k1CQX={QQLdr_*+A-fXv6|s_&~1l}MEa --DO+mj5y+n9tNz4R=?aKoiz@)t*p}jC2Cz6t(g1>3`G$g25DK`(gz}fb!Eu}#{lsR+z%@jW9SkTDexV -hsl7JuBNdDfeM0Z>Z=1QY-O00;p4c~(;`uX|s*0ssL21^@sb0001RX>c!JX>N37a&BR4FK%UYcW-iQF -L-Tia&TiVaCv=|-*3|}5Xay1S6G%NrBY_zE0vdNV+fckkp~b$o|{}-gJTEV*;*m~ch1i5rqG8iIp2MK -{(P@4rSw!OTNg_1SZ;-OwXvSas#Z{e_QFe}6G~~4U@R;tb2vzS=wBPFLTN3mgFeYd=R)saqWs`gI@@k^aMgB%#%-;6ktGm8p07Cld|@?tks_pyl%AuI+5JG=tQSqp~GC -z$&%2e0^4#MqMaV!nevRN&K}&j_yTrk+y%mHWEj6--{@@gXhs-g-$%4KU=Y6gPHG%;T|gR|%t9L@p&n -^EMU{~@v+F#Io=yLb^@#h5A&qV=iRxprx973Fhz3LnHtheK;tk+&YHuSwMCsw=-{6GPKeG}GIOp0y^l -0K4tIgD%Nq^ZQi1Q^jQv;SMU1yu_|1tq7f~YpRZD78*nzws#)ues@^R#%B&UHLl_jGY^-&oydM!LM08 -Z?;|ucK~z*Z$*z#pwtMb33|;!8q$BXY1|V#tJLFEQDIprjL2PB@j2bapd(V6%f1tefX`oade4sx)*B^ -@xqj_%v?Bm1$6QkP)h>@6aWAK2mt$eR#N}~0006200000001Na003}la4%nJZggdGZeeUMZ*XODVRUJ -4ZgVeRUukY>bYEXCaCrj&P)h>@6aWAK2mt$eR#WNCue~A$008w9001EX003}la4%nJZggdGZeeUMZ*X -ODVRUJ4ZgVeVXk}w-E^v9BS8Z?GHW2=@;hDBsR&rdwBQU3slgA -WIIpo~nu5crQ+8y!s8~&hMi;DmIc9lx3Js=MNM3 -U|1fsGOElaE{}r08}e+v0p=?XBqQ2DCmWaOEa5EzTz5y9LwS)SlZ%hJPp(oJGkKE6-n;0s?u+f&GlkR -8O|7i@en2TM(MGVhn(xq?C+mR@j6*Ox^@^^85Y2w11Jn_)4y1r&rlf(}n6eXRS*HbT -P8xEoJ}4cS2GIZfJ1$BA6{p2(yrYd`W)U(8zTe2LbZyQ+(i+QP5YOivQf0bKl;A3D1eA(Gbj@XJtYk! -;dB089mgo3`xdB#DF!$wUtS#<{mY3=Loc@?{Ae$Nm22HpNrXtC}89TJG2k}`p8byzeqM@=$75l4Xy(a -1Bhu@?WG9%Ct24+652~~u}c<;!)=w53e163yiU1R8g78MqK0{%Qmab_Lyl`4`Q3rqe)Wd~eLSH=k3Lz -?`I3O&Z=71V=QYLywkW`xbIKZX6P97YdIrU%)Dp2_?aubu03y -B)A(Z#j-3_#`@(m_!ItXd*wZ5ncnotx1Ap=8BZSG6+ -iE3!&IZ-I32Q{}Alou5!aYOWj=FIIn4M{uc`!a_s2@_212UaPM}6=2rocy{2DFm%y(fvfN2MCqw^D{o -s1fza|rx{B1+qLq$ym-9&ZN1NEBLqrJ7|>}?;LV=tj@c55mb#|!Lf@$(>@vD<#o&`ptLu9^k!c}w7cx -)Y#$qU-OB^)Rx6P$X=|j3G8s&bLq>fAlt!pSDo-<3c>+2)u&1r_$x6Vz*eqwsVKt&zynL!TQ=ItTw1&FSC$iy=mLKiXz2<1 -77_CibYA5cpJ1QY-O00;p4c~(=zz6u&A3IG5qCIA2;0001RX>c!JX>N37a&BR4FK=*Va$$67Z*FrhW^ -!d^dSxzfdCeMaZ`-)}yMF~wK@kbFg}dRf4+eY~mTQ3myL&@TrPHk?}<7zuuAOe6|WU7YbKWD*D_a2@>VfH -w-vLW6o)tON(pb?(>*IbwGF=ey}_eswIm;zS{TGl*`(P6s|zmDhUQ-=o}RX@0u?EH%9*C9JLzws0ysZ@ -;~M|0%nEJ+?#ZAuUP)R+6=%%I;a!u%1Nk4V78LnxB9EF|&(;?U-P7&K3(aDkjLrSyu{Dd8gOnOlvgDl -Eq??S5!)py|xU#t#>@`?I&4lhJk?=4nSe93CK(@=AboLkZWyqH?VvQ_zJcoJHgmj79VeQ#(2~1xCH{= -^V$lw;$U$ZudBnFY&oyscL -(OXrIE#oD(PZPY*^IxoImk~e~XW%YG0AjI_ive3HN&o1T~|eh8(5ZJrFj=|B?>A+#QLVKnOrQX(#^<+L&9d%DQDe3S -X`_+M!+0()NwggjFL>IM27l4#@>k_l$5GlGHr~|KjzfLb(&wikcO0km5j`i%uSU9vXSi+o~WaGO}fa? -Mnq_2H$fC!VE$tl7MGPAk7{K)`+6qx0Ncv**t6>t&(8qkV_&td1Rz!fEDQZ!dOF;fEY~pjyxja_f*2s -aFRXTr;Z!m0D)l-K?wY#cVNdzTB$B2k+@qgb$czY%!+7@X~bE*WGYU)Q-(B^`Mn{YP@f~qwnq~v&O>u -oKA=&t&Rbr4GD-+Xa!q%f=~$E((YWfQ+laTG8+wt_(d8=^w -Qxd>%^x;0>$eU{69oKW-)5;m*K3Vo^^(SzC@rU*J=7rO~7q5E@TC!^a-y;NUM6N+RuXbP=7iuY;e2#b -^F4KG8}<%QK)>p2W}EusaX!uB?j{@bpEV1z(K~%KX8JG(7rCSs#VDS(*pHDJ_Z^kxie5 -&!J?HAP{a6tjlpzen0fQXvLyr%q3BAHjPFzBAY@v&%Y<=n?J8_Plg%>< -pn(_n$2(NV^b}$OXH*+kDV{I>V+83rDO$|9<@rKhg(Y7AK?x@KCPV3M`oh{vIpO!onTPvf=n44vgt!S -Rcb`BYoJ8%`$&M7(_&9qB~RRI<6%K-O`q_cI^Z%=+62Q-R5MdkF^}D -#?vJkKGF54cjcz{mEajm&C|RAb=b_fh>=$Ug%$|&jG6+UZ}2qtd|xDVPay5nTjtAv)5#E@DOIP~^3=t -kv-pn8YpN0uM;!Lk6JB(ioGz{KDOg3$LwHtjJyQ%`6ZsD?bwaniRbex2TTX%bL(d~(n(r#X8W52`KYX0!oD#M*@c8WW>2V4H>(GR`Y^!_bd`DSM33 -YwC@`qBqlwG2eOBwyHJQ_=mlpyX*yp0qusa5fWh;%NX!A|8fxJelZmV8G_MKY3nbMx+@~zNZQ&xOx_A -N9cM=wmuqZ>(L^7UAtLUDK+*wXL^U5z%SkGJ@F@VIL0Eo=vhMw`lwHQ*=(NutuN7H9{LE}GNK2RwyMr -_X)?n@FlRP2Mv*!Bitn3NxhbHg!YGZPmoK|J!X -|?dK4e?S#1K4zK=_8&0&WG35^oaXj*M=2>hFW}jRW=G#6WaSMGt^*&aC@{VZ&Czw(THjUB2h!^VLUCF -)*YR-A#x6T0erS9Mlihxws6Yj3p&pbnNu2$>5C#KIGtV}P$1AVzFm*h2S~)_GkZ -!~QN3Ia0H$Q!-}$bGL1DC(i6Hx8#w~zqvB&1$`4Zpt5km4-)KGL`hcd -dLs1L3(K~pPxW9Wylg(^VBP)B!@q0RG-5#&CSp84wMj$&Xd`%W#*`%-uIODRpWsDmu-tjwrek4nnq@# -=vYC%YuYc8o-7|I1i%yn($(C2rB&CTGGCI;ov_6vNu%7eF`tj@Kz@{<6kJq)maE@DRZ}&~u*7iTyaD? -?M!=A5ybKLJ;Ai#&$AIM*R`jPa%gOQ{WOv2GiQx~hFjYf%Q{c)%R{2(Q| -T}$fpx?IQA?w_?gu)(>vs*q!t+=#CU96Z(6FcK>2XHyn@nKwM|K07cF_t_FnoTAh7?EttZj>ocgk_#8 -wvRnLAS=}n%v?ygkJm`P)h>@6aWAK2mt$eR#W^sWd^bt0016c001KZ003}la4%nJZggdGZeeUMZ*XOD -VRUJ4ZgVeia%FH~a%C=XdEGp1kKDGI-}hIrvuCUZosrvqxB^>DgC>qI2JFP}OLKr>$ebC@uDTjY5jDG ->1o`jxypR-0QM2O&y#sF1#=8>vkbK_XsM&1xx>K#}lonEKOWk+n`p}n6Ep|;O%3Albsw6(Rn9XJv7nk -DQt%7$|)jX8-jo38%y{dbmZ<|B46>B9rwQug#R!Df*?3HMn65f}!=yBqqKXfVwF#BVBycW3Lre8KKt{ -|(`b6IJ1aZ&Eurt3x3+}yw-_RoE}SN*Y7+CFI9Z~7KCU0v+DW-nj}?##S-2Jjc+MXx%!uGB)jgflM04 -`#s&p3;_WUfipC+jMy=H+K@3%@L%wa5l1T>T)Bi@@G}laJP -hZ2wmSbP*>ZYBYG4eS^8!831M3Xb}`QEjlT>_WiLGgy1mH{J1lSA8e-?fY6ET70jz3mSkAUzK_<`_1j -}AWiIJ3H+q{C;BS!>v-49s48xks4exqVjF?P*;uI4toNmKQ -j+iw-Cecr@&P5=VKA9%(VXQ-MiOsWPPLF0M9Aik7$j|Rv=)WT^zb<0`+z&t+YmMVXQPrC(XR8DNZqrVmcWI}_M;6JA) -8y#k6BRkf80Z*r=>`T3|nWY;ljCF9BE;{gTI~lPft7 -4C}G|i_(6f#OwGFAI9$Q67Z->Dpx4T(^ZZ8jFX>rUkR~mP`NhS>R_#OquZorp(l$_;%)mhZ>jb+j>`T -iW*9o@ZL98UML-oL~)~Jb!Qa7x}uEn}(kp0E-?<3`KO*3ZBdOl`6b2OefMXojOv8TfUPR_Os8T>vc5D -#U4>mN3!f9XAMYqU;oP#P{|6=23VS;PpK8?1uGt|NFzCObVOlcl8L5nzA -1+c|)V0YdlaVpTL?297Ps@g3CiYFas0V)Lq0Yn@puQ#ET4t7Ea3H33LbqyD?nhArb0WVjhPQ>Gu;WCaZh(wIZYSp!!0RAU{xo9KMF44GkOzEg)|r4fEJ^m>EHW|Jj -g9!johM|cOHrx*N)OoV8^CS9ZxOBIbr&&MJI|cYl9%LU?n{Valz@-7E9QD-}rCi9CSio}5 -6J!+lZ$4XuCZX>#HIJSFeS-&xis4BhFa>YDK({pG-;JE~vI9esk`#VZfWKi>Qlm-kbMqwQ0ds%w8oV= -fB)!r0Q5M1rrUBuiS?0-Tk~8~EyKHNYfrIrmV#uSAuwB_%mC!eB1gTQ@5L}{>%p -^EkO${rT{XCC`y;Znby*Y4@2wDhT2YvnY=?#eRVGaJq{%Lz`bDp0;q#r~moazy2v?IL4j$BVcTn_ -swT8hne*^r*Y_gprLPJWA5Zl2qYEZWjzt%f+=rDb?IdmM-vgTkAD3*YYSQZ%$>0BhlE#eKd396KL -uUUESe~3uA|hh^0TafinX$~cM)@LqlEi^6=`;hEFx26 -1=Jpl;Gd$MEJsM_B?$hv?Bz||ODbuq$X)$@y+;b`Manhwc$7y`bVT}DnFk=5@%pM`%rXPWc$!!2u)&$ -dqZNJSuB%hF0xR+JA{eSHph900li()CFFAW~>xm=p~c4;##8Z072Q=jtF3d(2ZliPrR5JVCqdkt1zBaP7>%lc^M<5<)PZm$Fo7QO~PdH?3+5 -@mCX8v?z+491Vle4zE;!1WWHHc|bOg^>>eF+O+)pvt`ka!az<={?Kg^>jn*?;_hpVf$ok7liKryWyUD -i~IlR1o=JfSa3?V2Gu!4^mcu+ojb<^IkVt(D?7P&i~qwU_C&Kr_jiE=LjUZ6EN;MNFlc|!z3@`v_yeT -&bLEbP?7KQo^gG97hNpoEXabgCI^}6{-tK}F$f3rSG1n(3xW}z|*&!`J+V7P3=QnS0;~Kd4w%iPH8#} -j62e^=333`q;C89L@oEToBV}NKf7&Z)B0|TpV+0z~{#|KQJEl-k+mm2du17oIgKtuLt?wyqQlGLAB>< -4tTn?t{Bc1yHbOUg|Vf)HO-Q)5cei<&lcK+h2}fI8Jy(m3sF1qZ{h3Qj?B04CBtnY=g&DOn%d#n@RUlFJQvM=fX^3huzmCLhCq -847J7_;)`%`+@xr2qBhR)q+0*A>Vd2LME$40ufWuKY!*W6+Jhr)MQ~SEknpEZk@#A6V0i8+GA1 -@B>Y|5Yk(mqhbD1s3RrfTb0cT+71^fTElHmcH@j}ezpfWN1P~hZlA&@L_yovCsVB`qmOB1~rX@wnGA^ -1-o5`IC*&7&lY#|Tt*utDb7>3PI|q_Azu;6#RIh~Y4OLyn;jj6}UPWy~r_1`>aedf7A`Mpv{Xfg(TBK -BaLWhdT(#xAuVOe3I*gJU2LnRuj7|MDg*znzGJ9#&*JmF~Z>UA#v<15_O#gKs{+r2;YlcBK^5wm1zBD -HfUcNmx0n15MVXg>^UI@(_7;V>JvR%gr}QNxjaGkX^@tb50n5%^2}W88|)C`sVobT -!_7`&TMlC&&i_p -db_}udo-k}$2cALc1iAvpY`ZEfFyiv+aTzh5f;bq%3HysvG)-qoIL=n-fk4qL-&4jxHyIf!{x6oy;;+ -I3^K7LcyWH1XlyZ7grouvB0j$;0P-1n1B^Xa172 -{9sj$KaY;A%@vpXJvi3jrGXKVI|v7bE4k<4ZUghP3Pi}?`3o(qi)Tc{I`vBj|w?0r24O1Z;LsaJ!ze25Ey>C;{1Ppjmk`ncO -qE)WXCk=r3s#1)xV$ktL5eAILJS5d7^g@(TR(@@Fi3gcb(eF0e0q*;1z)go5CJUei+fgnVK08C&5#kO -O2w@LzuQ7qbRU%x)FR&kTzgb#G21$(t})jYP*zxU6-H{0@eK!NR)P40E_MFRzXbF{beY;|{WKTv?xJ` -ABFzGFQ!gxm8@k``pvU(IxhF0VG14*(MX0;lm$J66Ag~LBfi%i!Yk4ynz6^qWu-rf@2z`HWDNvbRhPi -aDZ{)xVWv2qO24;Il(A&i@Lp)J}kKS9Y2}djti0Io)0ss;d^U-(?B@7>Cao!^j2XCgW61djARDr!k;% -6e^@O_OVW1Nv(EajMyKbamK)ANhSMqc~SrIkgYZM=iodm<|VGA?XDGtcuTK3BCqfDWXcpw%??H*% -gP8=Wu$!#qR9y23aYQ+@#O_P4a2$@G)iXm&=HlX4u$186y_czUxbHwtzNYZ@qBE+gb8qw*<*I_z@G{RVo$ERZ5&Nd*Zu(832H -W_SZCB|Z(I+X9LVA}3M9&G`7h}t!_XKCYCv}G&0L+ud=S28>acDWdKk&R{7xq@MQA`gATt40MO{E7H= -75zO*Z18b7NJbx{IrM^KR1k9PDLK>+n7ITtxl^hYHw}780&AXucX$`u#ve6M}ad;a{zf4}4`L$h#+I9*4%;ABH~BJM>V+DJTii)IbZU>g#LWc1Z9zh2p94LM7|=+ -x3;XUJEXE{g1zU+1Qe1^$O=Z8&HGh=uQRFagQ9=>_QuJIsmm0a^kK`vVXjw8$=N&*b}E8CcylqJ`whQ -;=R)N^0xY<0J&s!-*&%klUhRPvO83!Tg -doUnh-aXpMt3ad9eZmbO1i*Ry5f=O$f%Da)K>8=i5o|ASfOYLMLdM&8I+2RW1f-C+@ps0d4FJ7pt?`f -=EqaF9jA|b3k=LYF~nb@{qDZV6j>bz`#lwrH0BA+!*XMDSOj%z*H(ZNua%MKSN~!HX7nF@-{ieYU_;A -)v93*RYRUWeTocEyC-VNpb;Vu9J(6{9tQA!3%f-%^jndSm7^C;D5kujS)HRR}dHlpRwoAgP&WeMUvOJ7 -q6ceN^5nwzVTGL+1CFe1UkbNM6R6MIB^(A~KCsKp`kt;J4ZPRzCDA2dbNE`c$Xqm9e_VI3sC{O~b|Fr -MR*!ri8#B`IgZ4*UYyq-`{r);QXBYBfx{B-Lu{p+BmQO_;V->%&bpd+By$du}OFBTd1=CA7R|k=f~_y -4gpNEdb)2D1D7~HTEH|o~?1y!4tcI&A9)lDGT`_v1DYy=%;tJstUfF12Ken*HVe#H{B*Cw_ZmY7-W|t7P|2eoGF)A?G3d;C -`Tt?dvbSo>!d5?=t)7S4#aqbg65Z+7xH3VRYQL!&zM$11!SJRb?&QgytWruwdeilYgX$Ld53L3v)qP} -+bYCnv<(2oItanb_lKpJhbiUGR9CiAiHD)c6)y#@Rkum+TmAjQW$T2>WBsSF2;FC!?tp(epO9})+6wO -K3Ix3g(l?D}eeok0G4iwHrFaVQZXpcs|hiz|zqtA)5C?zrMq|CI?#uIREfJn_Qs-*RJiV8Pgt2>S@`lbN4)MG!B%I5lhg7Y}FZGr4KtBHQnG$)4{jUh{fI|yYny`>L!@JCMH3{)A -FsZ!Oeara1Kjr>j>!Q6)`&vH;o+ffyQkVKsp+-OnE5fkC^r_|50p>WM?Wp4}276!P59V?)pjH*qci%> -O6fZ**Cbe<@{ -OKNMW7AlMO#dwoZ1G#se0jPJ5@vhDHL#9woQR!ZGca%QUUfpv9DXwoi4UC@MN25B%x$|++Y`iT>{ta7 -_Jt|pZYjQK#_p)x$|M2L@WN_`O!7e1*X!BgxD${`U?}Y$WI&=x`^2Qd(# -MwJdb_s`|@YWStbxx#v+(uY6)$4V;(dcNMW%=8;Hj -z{rn`7|Fs8mMRaDx#HX6`dd&u>@w*gDkDo*aD0Sj?(CEy!JvJ4hylcNt;rYS4TY&|IvNMv|;VqZYHO@ciS=dq&5EU(Lsz@F3wqmaUv3ThxZoP=Sg%{x+0;t|8qbj-BcK8VhLmD?f*mM`J5TPpTyJ -etItvv;os$ap5t_$>oP`|o9eqrQ`ycvUtOg#g1@0}Uw^3CVGUl*QQI+*bBZa_5NvzQd@pZ4!U{a -`Isd5pmdP3ojU`B$;X|eQI!sycgpEu(Ja0Y(##j*4OX5UbrozcbJg?^y&%da48{22f+j+mcI)O86C*T -i5v>azE5Uc#?oJs0-M@$lHnoa)%}&DzMmINK5{%v8D?e@T>%1nuZG*PbXt={@`w@Cm49$R2)#R9|pgg?(hb55gga)LBBhxfC3d~>0JIp2IKbNfyIlR88hr#9 -H51DM^Uay~W)1jS>j-ASmpP{wCsCe^z)4OkK*bHp?OCgEw5&pOcUssmH(CcyC!xH21vW|bgl;|WsDup -3m_%*YKAQDfOU6WmRgB)5y*jrxly`_W+nW#EFa(`*!Mxyr#ge-xG#dwDyI^#=ML6hzybAxu#w|E60dD -UqY-C8L*;a~aDrCGB -21Y2BKHAMnr3k|G+I#y?=4XqoIGPdsJq`_rujE=@$Qn@RP4jJDo@g^lrwUtyy|KBaDH2kh -?3y#{oduLxH#Kuf62gW=@n%##kiuG|p?CSUbao3;sF%0Zh!cL|UEN}n5AF&Wo;eT7QV~$_fq3@ZQIrc -j@xI(<&?4FKfhamg--Hh#wO9KQH000080Q-4XQvd(}00IC20000004e|g0B~t=FJEbHbY*gG -VQepNaAk5~bZKvHb1!0bX>4RKUtei%X>?y-E^v7R08mQ<1QY-O00;p4c~(c!JX>N37a&BR4FK=*Va$$67Z*FrhVs&Y3WG`)HbYWy+bYU)Vd4*F=kJ}&+z4I$Z?xD7nMe3od6e& -IIcGX^5Mb+LdY$ow+Fd)E5^Y1&@aXvQv;DQ*KH#6@&ilX?ANokc7Y|&y+iJS%Fw$#GL1&D44ErO<)0y -RUvjZzdvAq4f_g#<1Nu?emRPhQr0wAuyf(SQ8#Ngjgo9z%rF+w~a!=G^W{8H3?ElWf9Zm66RrM%QTfb -Jk-HJlj15XM42iPf6UFi82n253a>{t{4W(Q`HAbT^&7*ho0}%7XJ>Du=lm5FoV4C`3nVXlM6>7gvQOx -7BbqzUwQ6(dDrm*8rANYn&lk@6+Ca^fh??_*O0juBfw8 -gL$DE2hvydc1xn9T$hq9O&-ZoeeV%)DJ94=rt5-$uG_&+DKQ0!qHLl?)j&-jrGpbiFVZchN=U -C#H$BYSz=gaqme)J@;h2Whse9vOWejF&BPiH#EEAj^dzJ13dKE4T)qGt~g$^B}S+Kj|z~*-LeS%Kyxt -P1kZ`td_BadWy7b7)fC>^JkQb!Rw?uhd-W(54jzeJt^ChPI&|L|a0tj8uNflnfp{>UzVW$tBiy{U@oj -5%>NdFjgS^;Dvqw(7!RUit5m-MEtwbN2t(DX#@QX&Od%@s}Uvldq2K&9lW#u}b^~P)h>@6aWAK2mt$e -R#Uy^YJwR8005Z;001)p003}la4%nJZggdGZeeUMZ*XODVRUJ4ZgVeUb!lv5FKuOXVPs)+VP9orX>?& -?Y-KKRd3{vDj?*v@z2_@N;XopF4IEgcO635o-~y`9-XkZTHnZ#4mhH6k@0lcZnr>lpNo>!&dGp>ld7l -5oD3y#7_Gr+{NY;S1dum{3Jp|kP20>AXp6Y%$4I<0)Jj`*%XZ#&;K+&UfJRv_9J-GmK8d53&Y -=%*j@^#iKdgQJDz!$x%p?=h8>vOzlU5a)L(2LxY&@6)d22c}@n1>IOa~GA+Iibxm@E3;a97olVp|1A< -D%npxyS#*MC@Fp$S06TUVkHxO|<@$WtqZVQVT0o6lJZEVJ9Jr4EUM5Sl{qpK`onS7HWMfNC|hJfwf+b -AguHEa~(+V&<8SmUN)^X6uJNkL&N5v0y40D*uwq&dy$O*zcKdS1c)upjMW87rKUtGH@?$(6+1V`u(?* -CcqXGZd1n=ic9(+l73{UGxs6lRNy7rG&7<$J%&svfz!xQild+uw2dft9jeEoM27XzVA-3{%js~MN4%! -Bgu;aZ!;bDLm>CU5*{^C=`$JJEm*dpq8$;lN@Jsf%Ht$7=vl?SeB7eEc)0pi|ARh6Svq>fR -?FoK867S|M2Y*Gr%;Gtida@SZ)Fu{RL1<0|XQR000O8`*~JVr<)D?@&W(=nFjy> -F#rGnaA|NaUukZ1WpZv|Y%gzcWpZJ3X>V?GFJg6RY-BHOWprU=VRT_%Y-ML*V|gxcd4*L`Yuhjoe)q2 -s8U-5?vAqV|hp}!9)-g)oN>LT->}a(mBgyT;=zrfyv1O;p+PoN_@4Nf--FFJ5^cuX7!VA)X1}nTWnzW -`-6{(FSEi|Iq6K4in0g=jitF}W(ax9~iW|``GV|{=$N;lK1aamSd(~(~Fj4SQIYSFUopjyd6Kanx-a| -m4NCuNQ9K>Kr`s#VPON+Uft;Y<&jkHK>o_)|e2X-nzJxGS?ibsKlI+1*6~PqD$$8Y; -ERvYGzhH?7q)S4lpD6aH5ItTr9vWn4*wtOq3gL+b^_kw`xj-Q^2YA7EQk)4l#}|b0skY$a?Z8rfb$~D -G6^-|wbC*%&gE2OLvW-8Szm_dCL{R6v3|v5A~azLSO)+>wh2R&CJ*WDfdwA~V69pRT^%yItD}GFkZT- -k(K(2i`xZoD1_LeKv+}!~rdG7L&tf@D(8kYI5A4Fv3gzH*qIpe!ng!>XaBT)W5K{S@VlT8vZLmR}+7# -rHe0slEN{EtO8wkAylTwe3?_ -U;e!LqE2C@Gl+(nIju%#T;jDy>@#Wo|(_a>PVT0yrWcj3iK?@=r8FM#BEA{G*Jl?lSkJU-EdY0wv+%w --zhXPA3!Bseez2D*gU2?;u2t5UkB+K!6oG{ArVKi&>>X}P`EqD9Xjl3L2k^KWuO9KQH000080Q-4XQ{ -p@lM(64RKcW7m0Y%XwleNs(}+%OEi` -&WoSA%R4Y}$J#2SN3#B{kwG`u-C>ifMwreY$4yFIS{+MJl1nP?|SxRh9I#Sk1&B~`!j*DMtTI;z(_wMa`e)7^e^kG8mviq~OFCV4W=$A-k2z|`PM^ZZQFz%hV2Mja -VkH(-ECq+jQB9CMY@n81HSR#xbZYSovNBWw5wGc)QC`7{;Yq5juHzDL)OZH}p`>Mk(>YY@JeKr#6f{v2bv0S&^VQuDFfgw?#I5a?lF)uGBphgf!>5_Q^aZ5j4X4Fqx;W*>p41|t|*uEDgAcV!=E(2zfik#ROmTY -wgV&i}Y#VsmDE@4fsY@dZ1se7bP)h>@6aWAK2mt$eR#S(lq{Tr3004aj001 -xm003}la4%nJZggdGZeeUMZ*XODVRUJ4ZgVeUb!lv5FL!8VWo%z%WNCC^Vr*qDaCv=GO>fjN5WV|Xj3 -QM^q{V^*MOvvGC`BLvt$IZ$a*|2n*0GK4bXg((JN`)0-6p~pXJ$NaX5PH;K`Y}xsIKF(BRrYPlGdmJ+ -);ZkOx|1VxROke6x3YP@(E?54ArRLzIjIvhG~t)&KaMTdi=(%^&mUAi*JJKnhAmC6oyNO#zWK5abc+) -D{WMbc0n?ulxWHWcYSGnQ^6Q~oOMV@=pGGR&129G-Ph>L%Tlrd%d#MZG}uBla?zBB9#NFoaK|cvIRSs -dyFFNEcG0X@{pe@gui8%G0Uc|YV`ak))=rt@Nv^{|H_g}UltlOoD;`Nkq9vHyUX{GuMMt5C;R^f*tGdtR00o#!N@y)}xsNx3~i{<$d?n>D -*7enYHNoUe5>If>bW1q$&N(@gtnH0@`3Tc6s&@t>cn~q2Yv}3!Ci{IR#lV(`Nnh{lALv&LxSn9z92%p{cB|PnlVS5ofD7dp7AmaSXfet(MYC;$i5X}VCKdJUDW!Ro-uIY -oQxS;Wj%>J#l_$ykS+Wx;H}Ue~T4RKcW7m0Y+ -r0;XJKP`E^v9ZR!wi)I1s(-S8%mhKtdg1do`dQx=D%x#s+B7y`+I4OCy;rMG7Pp*9iRIJN(cGt+Xi?s -1KIK84hROyf-xMdLcDoqHQT}BkEp-%KB0tqDG32=u?j-;!QiM^oHE^nhIXB$hq{i&kD*lNL!f)+fQT)i>Bw$>$%<69ejbS9L$4rj2v_k3 -R)XLkC=A(of5X=g&Nj>=xP(F7#y$)-*+Ym6IvrSmeKggUb{c#&9qjaBrFhDor0@2amv#l)Ra0yS{n*> -D@2Hz8@5jtE)$ldx5{SAnG8F594tP<`~bWp8>Dck?KPdW1Z>jz3!Gh-+*zMRCAftXZ{p!JwGj&dG>Hh -WWMA!if0ug0stwO~Z+xMfJVCY6S+FKQ}1okqvS>;L5BI1jAz#Lf>s?*kB%nZexrwz(o#?Tbl -C$Woq&Q#@veofh&mk@ipW(dgFjxds$>M>$npHkXOyROkV9VqeoG1sx7kBRbWhcM|U`cK%4w_>{~MPk+ -cWsU(yb4G7kpF1$=V|PxE6bITAX-h_lVN-Uwro76a}pMH#$a6&DcmS1 -av=#9kaqPVgjKbhae04RwpF4)CNp2%*fa&`_y#ii6<1bPurFkVRX5BV6|9%*U^ -HMubPRVnkus2W9pRB%?>tc`@6aWAK2mt$eR#V~K6nixQ001)p001li003}l -a4%nJZggdGZeeUMZ*XODVRUJ4ZgVebZgX^DY-}%IUukY>bYEXCaCuWwQgX{LQpn9uDa}bORwzo%Ni0c -CQ7Fk*$jmD)NzBQ~%u81&NKDR7OiwM=<5E&m;sO9rO9KQH000080Q-4XQ{h^v-UR{x01^cN05bpp0B~ -t=FJEbHbY*gGVQepNaAk5~bZKvHb1!Lbb97;BY%gVGX>?&?Y-L|;WoKbyc`k5yeN)knn=lZ3_gActPD -&(>4?v<1mHJRA*GlbURrwrPh;`ug*(TTgeQkpSBzK3DfM;jdFf-1w?0}u9FMy47;@BfdWu54I;Up>0h -HA7n2U%bN&lcUN3w?nG&)AcGE?AajOb(iigPyuhs*bgW25=YcpJ6T?q;)y`)M2RTbNG#~BdQwNMAlLl -Tq=jgSBv8)`-Y=Br|z!-_@>U%kQz|A_nJm0dt1z}kQ$|GJe_;=V4J+)$($Vnez~k*t$lPSLt}S}qa4BFe -vzif`{ZCDRZF|<*Qiv8-0j(bmJB@DERXtlM?+@_k9YpFY)u~D37KEvt-aNDxzKJ>Qr)ZI$nXiML-|4>T -<1QY-O00;p4c~(=*3H8@=1poj(5C8xw0001RX>c!JX>N37a&BR4FK=*Va$$67Z*FrhX>N0LVQg$KY-M -L*V|gxcd97FLirh97{_m#{4y87k)dLK;kkSnlLIX*lKhof6>|>9zw&Y0i&JH2(-g6}1Jy}xd!0wE7E; -^U*TpY*o8>mHbWl~uunnpOO73+*Hz}f?o960-I!Sx;QCZ^9kisLwnWZP-uSflG&s(O6XHmZznSt+gSo -opaYWe)0bl>VI#?$x6- -TD73uEg^U+z^k{T)SnC2?~T#smRPGxKv!&Wh89N2_x|Y?dvJV+%*ZZPiALpk`eTt++G6AfWqSj8DQ{X -7_ilU+sXh*>3kfYZ^HW-rReoSI|j2M18-*4x -U*;VjQAb+=p!qC8I#s4VA?RHshl_oacRZLdiivg35K}dV9g?BI!CC&O|C`xCG_v$pQ6`9dEqA^PJ8-Y_fIGCPnE=Wo-5Uyx$AqBUT_U}wQ5u-78NGGTz(zN0dMlF! -m9$859$v$V$990DVxg?;l%2KR@YD!7CsVmLm2`pGm?%}`FMM3KS~r?N{{$(><|-Xx>ICaII2mor=}bQ -8~H92Xs>I~kb_H1wUUU@%>BSfM3N*N$?Fu^fz@J7}^J@kC1b&>rZr1*E@dsa6697Eb`V^CH|mxj5({W -HXI+CL4_?b%|3ZEUOl=<-S8D@jogK7fqI@>?Nb)bZBM6mC0l+HE4FQCA=7-vj6mXn{uR -VFrZ#MtD!G>j$(Z4-uRSntdY0s>5pcYzHvd%`l-!OYDrEL1OW*VCO6al}iVWOYe+V**_;&;eSBWkeTufsmcZPH#dBTC?1u%u7uf^~2iS7Af+y^yHrXI7-P;{D -S6;%Yuao$l=l4^N{bIewO82Ua4_ol2J54Xxds$;gWlrM`0<56fV!}Fd0oqg4uhUlIIdDuc@|QO6T|C2 -f_gdg0G(h5w%!VI8@(tuz0gu(0a|d*x6UF1j6dHQYh*rn7jYMW~>6kE(B)>tZ9LYY~ -J!rkA=qUuycB))Q5XiOAHFw7F7GY=R-ja^_m6i^3`W -dFVPxN7->T;IF8Et_83)?{YBsAw$n?ChVWC%?oJvw|yGA;HnhaR~PcK4CwFeg73 -~p|o1JXX29-@B%P)h>@6aWAK2mt$eR#T6qSSrFG000zg001cf003}la4%nJZggdGZeeUMZ*XODVRUJ4 -ZgVebZgX^DY-}%gXk}$=E^v9xJ!^B^Hj>}%dkTb6la-mWf_teC5HmcyHMk~ey;fTTd8ihH!%3JEIB!#xF`lNWRPd-nI2$MNI896$c+ -$@A~|(W7F^U%n-(o0gA(j>$6D{Od`!YWdpt$jZLHto*H{;Ay&d;rSp~XNzn)o1Q5M-M$+O>dTqGOqBAJ17)pY0@UXn0yN|`vm4+XUnd&Oh77wn74ZK1Flbgh-1-mj -Z?3~Y;rUZ_`Ctq*05${SPN#@Cnv9tAD_jqUp&wmwIF6;S}Wh -*>U{W;TH869?n(MfvI6eL?WT&?Bv`esa(K2I$|h@8J*%s>t92STZCZBiTjkGymNrTNzuD0)r{cuF)(QpsUxu!dcJ|6H4l3UA85t^gW(uo-_?fU1 -(m+xGT-9Ud1rgvQA^v&kGg2qF*oJM_^l}x@qM-NT+=a%Cor8=|U|_Ga9#Et0uYDVWac)OdWiqo|lF0$ -HY$sNbT+I;Vz+Tb!4`S2o}OqdKSjC!sB?hx7VWtEkzlIE~2Qj>)D>HbDcgu{oAYZ1{#wdJnvgl!#V53 -bD%u^e)9$}L8NPqk`>yC&eamM!c_z6W&vAm3jpt_ufJCJ@6xOoIJHA4JVDr2Uuh8CLWR6Fr65cfK)<= -lb#c*N&S>MtF;v$e*KkZytCz|BKRgU)xG^K7OVa~}wGvOrIe5*eDUSf{0tRvvs4vEwWOx~I!*Di>Qca -Hn)QZKuU>4P=cd9Tr3K(WBvoP4h>riGe6MDh^A7^EJ1zYt+6XG%;vJr7`k7L@vmN=r#mEPU}OB8hMHn -0jC)t0KN%SzYzR>7Y_B?`ER8?)xMS&&h4k--+BaHz -w~X?_cE1r_BQ5yt!F>Y14Y}xo>Z9H|E$`!mt~<)UBItH>~q+bhy0kD%S_Me4s`08CIU81V-}5%{=WM0 -#~&4F?-#G05VLgyu=89MTBk^xrB+=9TZ~SrJ**zxTUAlkSlO8IIwZu9jv_EYy$oj1P4OiA#kkID+@T8 -%(!=kfS|=$!31_wyOrR*+zHx*$QrqAt;W9Af2xk5w7p8oNW|e&bdJq>j4!3QhUMof-r1%uHK)VvXaYyc@t@AX)HTqbmb6NSala1wDY8*NZb_neB*hH#-O?U2keo@S(+LHvJxl^OhBgg@_*S`*m5ixJ -^{B%YilsjY+XE2JAo@1&(QkBm~hMIKgmUfzH{ov9&&6WRK7CE>HO$i&Mu@aBeyZ3D4blLbJ>O#UHCM0l~BN`DJ>ap{p2_4x2X>?vi7j@ZHG`qpr -fiI4Ox92Vll$`a2L7H=(bRA19SR@Nt5LwBu!>6;S3?Eu -ZjQwjRMzR4O*=7X;E -L1!y^wp3+Z>58m_kFadt|YrbffLWj#e?o7YsbKL17F4XS6x|Liq?y&!J94Wx?7F#}Sp*RC(5h0@U_?BU3m*!Bzd%qMEF2ShM?k&K%fpXDhSJ+DO3Q=j<|rExSb -Dj|2yCSLIrP6><$YUR$RTp~D(f;38z#Xj5?z2HQ>0CQ2&UV$yI#mZ1#z_|9YtBshTVF2Q^^Az%w5eIB -9v_j9{Ofb_oHpf=t#zBw?$yoWTSwFywXDO2b7?&`I(xNZ4gS^fQf~#IOqy63qj87do&!8h|+*`rT=hz31-%WGclNIwJ#}8p8Gm-ewT7{u}@~zx$>i20U~9V -R#J!gxOn&UJv}>!ij!?jb=F5;@}=^2Vbaw6gnDGg9h;4gEs3uJE-R)X}c$&b#+_z~sqR -mp}kl=|{V9?`J%BcsxLAfF5|gE#VfgH%O=+YVa7=YnFoB2`UiRDWC{yj`p+?BnKF~Ej#c=^cvdqPH+H -?ny!N94eA2GUUs)RIC5GY92DgNJ~?2*9hiVp#2{uy^igUo;f4Y;qNq0yL!p3%w?T?P)PgT79}Nl&Wmm -T&G+BI)QgG3bN!A#!V=_}jtN!k3rMS)V6gMd7(33p_u_kNj_u!vX@rXj^lhPltpE~`Fg*`D4!V>^u=J -OY4#|woLDuEyt01R>`Ah2X1guot}CQ1oS^ylAyo;xBzkXV>E$(E!U95wy6f~5rF4#oz^y#n&*nd}o$; -0WqYPL1%l95JR9?k&tgSgL9RmPbbJkgFv{n+87>vhsVW6Z0`f?M>m-G=|NqE^itpll)lN0QLDGw(@4a -M>ZsBOFt0ec>`duJvU=G4m<+KZ8R}HFPbgUo{55QNN4K==n5^#&H%>1Cft$|!G+k?k$Qzt@Ff5)L~Y? -x-XvE{0osAltY&i)mM+?7WD=kVwqP$cGU~AzEJM5X!9k02QsM-nM-J1%cnfLCRS6=~RArG;;382RLW3 -Excka$gfjOwv%j1{o!5<$|Z~Xq{{XdFz2;!DEvq<4iM+hTr2n@!^ctW(?*Ihxj=A2p~G8hm?frEcBii -nXka8&sh+Sq7cfqG(&GH|`A+O518h9Hl_T4941xdTO0)qcO+1yIeayzeU*fttRml+#4YLjb(x_8#&{>?Etm*skpf8!sZefkCu3($EtAz1nr}r4607yWfi$~p+p1X{9$t -VR>y{Dl;IP_O$zhdMhgs8fx;gyAR}a5(HHVl#R!@aaq*$Wx92caO!GX=mCMc_@?OCgc4i^<99ojNpH` -Fck;?aHVmh=sb=EXmpwgw=80btGxH=tW<40v8+{C?p(z!+Kv5vNi5K^JDLaXK2q#gA(5s~3g(AuCdtA -()>fHMxW}04l(ACV_1b9QmG0otMn%G+TqQp+(9VTwY#at_$NKSG~~=ef^!x7~-^FrZtShaX{E^P2|UG -e{?j}#hKn@&)G11Phn{T2zpyWdo-TyIjyBBg;1@d13NqHsE2;g855O*0KddGZQu%H&nQf^{aXuDcCS3 -O@5P|ER{a}E^_U*wLb)k|{<}1`c@FfkMsF{4focTb+fn#Uo-M`m0?~8!%i&(S@D%X^Ox$GSV)zJlw2C -u5Nxiy6L6QxI;~1rB%*u@4?YX=fL{AK8mqJ6NeAkM0#=@uU&=rD*Gxd#n;EM{Ucp@=ofUAr-Su3C)cx -P^b!thTGJEhqx7V1A6;5!2CqX;m(v#>D)GnN$S;dxMnz8Q3~^N*^s3I{;v!%_Sw_y8DgPa+-Oto@u1j -48RL33WFBIlImGvxy#@kuzyOMky -7%Nk7)&=!;r=AsxI;a3zl1Z}Z0)hGrxGCN`6T^l<`hL@0#BAkZi;uEr{HeCx`CXl+m(KK{MF@$hZV6h3v@&5U#j_2PR_s$*5p0-_*z(2{-KvD0K74#=)(dLYEDipj -BP|0^V!`COVF_#_bl(-|=8Gbn&g>mU1u`-$vDzC|YYoBd~Y`On1e}3w3}n??->6U`L-L+*k07#(NMw= --(;$HCMV_Me5s*>{XzE2M?W|C3rdP4GAnVkEwjX)}z2osU{3O;tjByCRK|4A#Jn93DBUEE6@`4xwT3@ -Hv~qXO>iV{!o`X5m0p1(UuZ*)%=@BKFKczaQO&!ObME!TX}@qX`|$3evC=h-6kqI9WeEGS6LS=r_Tr*9KW|4rK1TFlM -)GE6HAtAK00jESv!sbVY`Ml00DMf#hb9j^-{5Oc*D^TXfZgU4piFf!%lO61vy&IkPk;VdNknaD6Vy`va=a5mt)0+cMx` -obB{NilG$$qN#X$0ut`eFk89QU@Q6!YJQ>IK6vMMGAe2X4^!vNPw*C5?qYAhp`L5!T*+V*EG(KGShHI -2=pVocdA!Fg1}m9HHG9UPwps4B&e$-d;1$;X(aYJD-hC4#zvzMM+7rnU4}Ix7XFlmKSe*Kvp>)N{2xH -!t6850vb`DPz(>)!fqH($eOzJOA%AX@p&(9l|948HJDiB-1O^|+V?R~kEy|&Ujl^)LWXz}V83^F`+pR -+5&3RIKydhbPydgg*Q!aq;;HQ*@i$%*qnvk)zKj>4EQ<*be4UM#Shc0MlHbt6}Po$j3Kk1+?aFy;Lmd -L5VKBJ5}Y8uQ=&6Ti6~3>w<+ev{ZB08PwSzl8)(;5}Cf_VWknM?KpY5zE1ZQMG_SQsnjrjt5O-*Qm;#%I^|9kRzR -9^m_pz1IU-ntVvA#Di>l4AthaFk;FR}arw-F8?5TjQt}ob5ViSdehd=`KF=JK={52&2Lhg -A95SU!U6%~ghd3-s1H0mrPl}8QCL05vtJ<+E@MgfeJ-{}>F0F3#?^yRv=zH5?_;R=Eclzwu1$OHA+jO -vt3O1YVg6UN5@1o?htij-oIfQrGt)WvmIu$nCNoS`_w?7#$z_+U~VHS<|ZTti$#dVTrsR^h$w#WkOj} -Fu$8`+OQFX^6GueP+;mY$ImM!O1f$hoP~}W-wM$JYk47t}0ozI9PxW -t_4702tA+aD_D}VsVZ#IRU%Mr{m7`6WyS03PrS&HV)EU-R2Y{22^!L9Q=?dsve6Tmj@D7(?eRhRU~pm -tT5kbTvP@iSFu;$%$u1S?r=gL)WJ#Py1;#bLaFpVBr2<|)NQd_}n{W&rZ}EUBg{QkoqlDf}`DoMHnY@ -$Rw!gWg1SfSuvFObjU@z+B_qPRX-n%o$8P19Qu`GbdgE3kZ(^IQ3+M%C!Dy9)4u!W&f#2`J^P3Ia!E& -hBGco0vdzQ!09p?-VP?L0Z$suApdNt&ks3OF>#39hv)$txG(0M*05Zf_zu&C_?F1(WOy -!m0g+cv7Q^%z@q;@=Xa}ailWIYdY!}~-Y`ljorGg3xlg$SDdN^ckF%OovU%(6X<2jRJH7QUcG=3Kk+e -s3%j!)g4V$pkF!aRencM2Z1>;ZxHs1-LVmf0%=7KJWVj@U1QCZ&-Ont9m@#ZOREh!RrFtM2(RbWlmha -d%qyH5`pQ3mXj0MmAKhhOA#zn{l3&>RHd*5U4by%`C9C7nDIAACAOX`;6u9yyw%KDYE}nZ+8%#UCq8bit_L*S?CTm8&B;c7>nl}77$!zEKJwBpnE^}a&b7qU_lPSM -jf8YZedxc$W5dEyCc-zpY2KO7}qd_mn;$1msbA0FK&={kNX;dQJRPMn)B`rv{aaocx^2A%OFXm8+0QupKJ`|@C!1(8UaqwktP|y`AJD~OR{|K1(?tUClfR=nJio+un!}bmnP -A&?%rNmPHpl8?cG6lPN8E8%&3?@|E=OBP?3B0$AcBt9C9^J{sJE4yp(VrM+yfKZ%dqQYMdb)MZu;rn)AWBk>3&;ax_4-UByk=)hkiDMBI>8y=9f3tKmyj^g-J3!Efy -c!^{?9stRg*kQYmy^Q7Wx1qYm&JNqy|Ws0yhC{8ADPML9O#z7j~f@dz2Q$AulfecAigT)CtX)+GJ*Sf -09v|F(POZBpIOI-NGC?ECXlJ5$S#WoFHhEf)OsYuhK;h(@UM{kTDq1*mQjXi}k8?5kkm*?>S#=zVa^J -P95%C}b~U^ci!Z36mi>=v0KiYb5-5PY3AR9QO#ZZi!&%LF`MI=NbBaMsm7E{mW3`aSmwB!nQh(~*aS# -}_~ru^_0<+Lq2#dm&qMp2^(!8*v#$0?RhW(7h}c_cpqH^;oh(7&AB7Ga0v$Lm_U;sLvH;YbV)caUiE` -a7Sp!*R;|1i4qZWpDR$K -=1FZBBB|z1$U8Y<5K1M`HCSa$>#1A4V3uj7#rUBz;aPy$l1{rX>`(cZTqnkV1<7aRJSjPCk5#p)O?l+ -^YVt{8qY!5?Dw312;S>CYfkknN;b8L9bk_{>bC@pPLXZB7ZStQ3?G>FAmZoU(D1zC&cn9`}%A9JAp-d -A_;_Si(Lyj(5`E53fv8`YL7+9Nbr@kb{|3aJXQm#E39A#;!`$!+D{zAEtE2H1k+klnIOw{(9(GllyyY -nR>XO1D(CLg>ql(L9C22Y)zpCSb8SQtyJ`y_tcjBQ*6f7Qy}sIe{t%^%^@Bdyg3?Zh_`pN%>D%p+O=!ROcW=@G4jkXWq}8y!ljNLT`c7bwVG$dGQw94Jb@d0<_8dBpgj+&d&E20rjvs4!{ -z2w|w~Uq53cNyLY#>DX{$S{sR9!qTSSdL;k&`R9Dg?*eO;eB0ODbpK|sJ&&#cm#xfznk -wcLgU3Dt6~DTK*@bJ0vCXF@?%MslF_^}V1wv-Nsie5YTI2@KbxTyuKxZBs&qiUQ*G7+;xodVna~~qnsDHHfj3EEH+%&tyvpYW(!Eiyzd6WV8!8~z-cstR6tFkX`*F`8yvIM(`(P^R+?z19$3WnSP_60OGX*YGv -9sWawy|-+OTv7FFOf!t-WjHFTG|sl&5m6b+Zs_G{X?LSvl6D=X^^nr;6^=G>8hl50YyFDJ(8xVCIKXD -Ebc9UzZov6a4pA-&krLpKtpM(=1F=TOmA+G?`hQ&mOlwOjK)e(#9jk#C55Hu`8yWT6z~K3ys02-B{Pmtn3T`aZ@XS1-bc<9M$MREM~%ZT_hUJ9WWP-eHNmQ*sfXjPJ^POOuj6Kxb4T`@-)Oe?zf -em91QY-O00;p4c~(;hq@{F^0{{T&3IG5d0001RX>c!JX>N37a&BR4FLGsZFJE72ZfSI1UoLQYtya;h+ -c*$?_g83L7HshK2L!yZ(0y3A?1i>(mmm~-oTybKRg!ZXO8>nhTe9VAw{)R~#J1+l(V5ZAB>bwCaimp3 -KlFJUHqqrJ;Y6G8X&jmz;X2t@=)qxwhBG@KCF0Eri%mSy^cMCYdT5u7U0N#VkGD^{lQqqv;jNp -~80eLm8Zt0_CD>7PwLVD&mkRIVJ$1L-C0E0V*gw$tnkK)Oht_d(<1X9>||@zRV64$?K1b%VOE5{sebhl;whqYUc9Bf -p?tNUbwp?)3_)Xm7$;LT;(ym@hq0@@Z7I9bqtXw9<{xwL3Eg~O)mCwlK<_H6KfIEFv#dKMn5YmHw-@s -CB0@s=%{Ay;(s5SmDm|kumR(2mF&5>HXqFJYF7{zwwvs4;74+{!a8%`I;T3Kirp|?&53Fkv>q1w7(Xo -<|Fv)SGo$fe`{rqKG}CP#vNu7Ry~Ta^)qrVMZ$FR}2TwCUZGo>2@=FWak$=XUA8;G|8(-0JHgx!lA(b -O`Xj^~|ixPYFgk9>>apP*ROA&0CkY}$#{DktHI|V?)KG6MgMb}YLvF#JXxP@P++03iSX0B~t=FJEbHbY*gGV -QepQWpOWKZ*FsRa&=>LZ*p@kaCz+;U6b3k@m;?Hq@n}EZ>S>4n;$ -AQ^l@%Q^@+b7XA`SAgaB{8E1R`0kCGfLv_XEW*ogLeiaiX=WH -*pkt{&5XS~XpI@GmnTG%%iEEjo^@fNVQQiI4ttAGH+OIv3pS8B2oWeub@0$>%LX)W4TH0)62qJizJoU -e4Bo@>Dl3bq%e5;t__o1)zn6|0LH=k=DXz#~=}>@7Ew;dzru6 -h)H>upUoV%Iy}8Y(J&4zcpgAY2==@$J$5=N1s*4IeSrL?VSBkqs(|YXFm$~e0Km*&e=~D3{Lb?AS+3) -MU~4YsX^i#cS`oWxdJv=jQ^m|a|@cD_HNbd>YA5D?i_2*oR4$pR4uY%Q#d62Xh%ktU2RhkjyyETPXq6 -4^oBP|ylfiT%vp`Ur!KWFABqZ80N9m?kC`CWIfo$`kqr>8v1NNui&7;8=+H;I@XhhfYv4R65B_#$Rat -=cn?>Kj?`J?ku|#QM|Ail8z;H_ZlWvMqbkyMa^KmH*78d%<0SoR~d}s@x5cdSF?B!9YlsDO~xE4gu>6 -QCJWzHf)17DXS&5Kd4>vf&-*u`7Xx`IBv^3aqWnKf^BX)9A08(VOlxd#HEIfJ3eBu#pDyZDd~6%8Vm$hS!O)C7{zkisOZe)Y2!5?r3nFn38#Ds^M4Rp6Ci*!}9?jzSmzO`kU#%X -!U(Nm)&54&Yg4R30lJ8|DXr$P%y~cz;#Pv$19o#Vh^}e`dHZ0RI)XwdSc9uO0@CJB_YtvII2sYiJdGV^hE5D!IPBo2827CB_4^sZp -8K%L>NlXBYVzBn3ixuIfNN<2?|0fv$&W;97L*C_D6aQ(b}Cw#d9Ys_S4(%oB>0+Ghp|S+P~7&vkcuWL -`~N*_nWHg>d~%k>&54vuS>qeKTjKReId5nmTl@Xo9q&&F}00^P+VcaS-AWG1YJ5M6L#*aWgx`nB`$OB%3B3)_9C{^jD*lPxQL7W7gO+7l-a -AwHXP{ANFM9BwPR53s#7y}jPGiR2V+?V;~xNe^q76&M6DZD0Y0qb}^?w=pS -tPG>_bxoGhSq4y>=o8I0{Az+t+14`}NjtpXTaK?zvMSk}P>2~T87A08D@z#q-Gw0anETGeS3UN@x9Ws -$(jkY0WJPFS^0MUG#cO7!@_GTx)E&z>S`&v_amwzQAPzXd2z&FU>52JWKCMqEpMBnZ}KET$ -OkJT2jj)2_967Hdb>>lwJ<6t#Ota&4&W+qTk)`_iZ^wFVF#S#Y_J9o{%ASkyNQ*^4&H1Jq@SQ>)UggCVj)oG4}J;n9u<7Ub>CEwzHCXqk2vL*47x`$Zvh9+-)i_>VtfyDUpfl0V(0WFz_ipsqYo2#oXF@w*y#Dj@@CuuZl+S711S9wUbdx -LDLJPJ0}pXH;OYne!PV&VjxUn@9on6HyQ%yI$!l&h!>{xtm5~x($%FMm>|}7_AOD8YRKs-=B2^A!q(YUAbr~^6_rkMY+4~KjUIO)z|ryD;y^}@Rhvw_XX!K(>m4zG^ -(+C=*|q!_dalu$R=HK;!3pDp=HVDb%K5$8AZ0C{h0Dm0r`0|+MX_LG+_jLg37@Z_>}7T@fOYzG!wH$q -X`L-#m`rorglwT)(pm(e>(JnjY=TTp{(2Y|*G!fSHe7)i-{IQ;;O2;g;B61R>I7npByc~;4=v4MRCa% -iMxU9)%2e2w#Zh6|4wudo=HW7U!Qn#lVk-S)?L#hlK4&tFQo3sc>yxuI5>G#U7!bqIhqiyole8ax_>I-TJ -xcMN;+K8e*TcBE_#y|7aMECxvlkPQrhBqT2kfRH3ORsOr;}2AqKfL&1L9800WG!BK(k14;*zQ_pisVX -ULMh%H9X$G{g6u_?Q)GHMYCWiUDS4jON7F?CQue?`Df=?@feoJ*R*e{?aervKrjJ8;!oD|-VyfW$by6 -RvVIQNyo<~Pef&@*qqwqR4)&S+;{wn0Q;k0|d(TniRf^pHn7v>)}Bhd~3B0q$lrx10Xbd;4)$GgBY_R -W%gNg9Mi4;0LWz5HV3wUJ*tnvZ=z2=R(dpH2yzo@r40P@D>sZx=&w69*6OX1w?us{y)+KW8%}Ji+tiB -sWb(d&3*-uJu|GIVl(WtYLi^G?uLko%NyZVqs9JMt!XATgE}JDwNP@9fm!UK6q0hAX0rNJ$8RIUYBXZ -#+}F$I8`^*hbcykC5yf`>Ks(&K1}Cjobf@l(fl#hX9~rNNhmy>>k)LS*YD@A8jK}$y%En`--XN{0Q!! -ruZ%k#7#`RM)HQrOKs?&S7@{|QSQBe|no+>+3A+VkC(F*t!K3WS#JT{#R=rQ3x%Fg+!W4)D^yhWKtxE -fnIk*O$k0Br4_x5tpR{>+0g3b^;2_o=l@gLIHSIrk-{ZrL7)X@~@m=?@2g~Qu+gPK}5jyL%hybty8F) -Y}44~|2*^icO)-y*OjC_i))-(ecuMcA9S2=5hmcl~E -X@YJSz55vFFG_@>%?Y=FoQ(IMoQ{!5a25V5;k2t(Ql$#!k$q3W4Sb6=HAD-=^2{X1@`963#ebrvnnRG -TV}5+5~94)0!E#{;0w}iq0$I2Q3ACVSngt0H9b^FPd*)Oo{gPjcu!)q%fFWH-Vcp;c?O7K@cQ|!*?vEvg_8taW{*4O42?cX)0syprb9p^Ibb&M0g -|Wq5RFgx?wftljs5C56|<|+>^X+{pe0uhP`vU;?eTNEz%hIT1SAoXc-Nt>XIp_V~2MZa(E27P}AexyB -lxa?Ce38;XS5BlCT%>g;yf@*bx2P-=g3Mq3H&weQ-VKD)5P(>8*D>NV=n0C|0w{aQE)I;C=NiHTP}}X -mGiFssI4Ub2sW2>;)F|_~O%AFW=xZ&i(=60{qZB{OecY%lf#CGA*RsPpTLT4<92LxPA$#rQN^#Jb}p&_QJ5t9)sWPuH2T*+nH&lMr;u_GH~by -r=kQt%q-fz1ytLM>hE@1vz!faY^`*;o6HeIl)}p$bju`2K1#Vnm_Ywza^i6Fv2|0V=aF3gO$hE)&E&x -n|X|u4|sct@14PE~Euns*dH~ox%pT}UNq={=qBdXeG&O!qh6NcVfty|m8g@dy7e0TqX^Qa1R -OO0VdpZU4u?X{eQrsjRu|zAiKjS&9I2X@V -*L`+};_y3Sr=@dqL;0PrK^iCEY`vH6|oy-SZhgdXESGbYG%h*m*`~1C|+I4F8wP@4#6U8;3+};MiOA( -1qOdWW)YA$VS7dFSDfsJ|@Wrh)g5w!iR@&7)kC?j|b_1qpf2Z^_gt=f6AnLj%@?DvzZzCPk8NQV=lNe -{7KB$-0rKWlV!+N3XVGk2LBSk|L~4MYF{9RL6TaA| -NaUukZ1WpZv|Y%g+UaW8UZabIR>Y-KKRdEGo~ciXs?-}NhS{C+vnP`{t#^~y8@sdZIx -a&?ki{`YvH)dACzJoa_W^(a2|3Pm_RF#li^TK3UqHs=@#vlW=cZ6{Evr_GWl>7eOumVdm+%QC+zCBMBa<}Lp^Zl&tJ(_33BetTVL)9a*Zi@M57emmbZIAAj3tu#fGW<>=PPWahv$@NGaHc@NxYLP<@K{ptjhQ>v|zeoobn@`6|=%x^cKWqi<*UDK@f_?Om -;IV5ruxx*c^|G-v$fzCoL|C>; -`Tn+bpwRDD$1hWio2p+iMm`!1+vi2SK?MGO{A@t;&j`tYFduQN7&bHaMX1OVK1tM{0O2kzsauwx%jFa -jri*;`ZyY88L&{6i9>NQCh5tP$3x@G5y~&J((9s<8oF5txFEK^fHkK=x9SplVi)2#1^ui9U0h)6l%)W -FPmN&Wh%>bSd-C}a6RKTFn92?lRl -Wvt1F=BdktjeiVuel9NHxzv|CF)?B-#<6pyk;Fi)~U#DlOYIZ4I&ssN=}mt}pYsnhAn+ -40{1p?S5))j}*JP>=cV_~__JoV5$_*?bs--)9|ff^tk9liYtcOS#czn}gI_W#5CZ{B~x --w47F7ti@PS1}|-k!}!r(HNwrfE8LYBpspcX^4tL!8(tFy=s -%wrRgber#2q?QXAXZX7fq%3&xloF26KJUd(w5Y*{xuK@)kD~sx8I0klcRaCj!4qA}byV@Ehms0I&HJi -*#KZsGa|(7p5$a9b^8dF3W^uYXMD%m%cHY%+z|9l>SU7Q$O$Rs8bu~L2-X?ROTddDUA6FRCR+fII -gtSq7HyWXr+|-^<#OuYJZ#Qzr(H{-Gs|!@aaGr4*8~D7b%y%7H>P9l{t$zrnR6*F&=+Af3eF0V<858Z -ZUpeN%yhj`bLovj9UOF>i2WbvbrwMAOA=`j_ZD{ZpoDJTTiC6G7jj&uqq!UbI)S;7`@<4%A7W}6xG*%SXQ#iFhrL5u-r9Nnb#rfduFSj4^r^A -7)&gy1iM5N`P5@udfkmJb1dF*O=TEH-~x$lAGF2gvb=h*bXHP8)b=Q}`hFxa7(5D4L)HG_U9AF<+iDZmksQyVw~wmkVITmhO_65Y(airFY -1V2rx)FDc2!y6YGmd0!F+aY8*^r)YSo5`(*`Kjp*(C$-|?b9qv@nP2NdRm!PvQaf~rcMB));b6z4}K`+1m -LWJm=L#Fwzxm!`k!mS6N01pU2!~`3&x?5s!@+&crEmx%nNcPz77~+zz3ins||Q1y;u)F;#$&zl4{&6j -YuXK_vNZRd@||H10xZFjQe%pBmIi&3Id@FTmx2b_jZ;a8^8v<-DWl#8;&rz>OLE~81C_K;c*iFnt3k5 -S1UszZ^WOpDF7vb=WiwF3xFiBIm{to@&Z!;t4<;VmudXzvw;5+knm$NPGN#|-gf*y-k*<=W3h*C)zRE -QqZ)^9i}NRXpV@gZt_bfMBL^^q)ErtVZnum24zHVZPrL$1oy`s6GjZ29wh%b)$6QSp%>b*_;%A73EO` --!EPDRjsi*s>+khgW;_!wZ5-i?_Y0%RH@~{te*t4VGVUM2uewJ2;J=#rsIw|xV*efEw?D;S+3r)zrTS -t4#r#tM6>nro3<)6Iz*#>}W{Faa`hvLK{byXEO3=2Bw5oc9G~RZ$;iT< -#rHGGp#m<#<-8+A>XEE5Tu^zK`S0-L1tx^>?Mm~u@$b>z=-5%#NpAw) -RMyt$Ie`u>DD_~6qko*3Ri?bFZ~{cV=_@}L8eHS(l#`*!vLHtL2faJ^Q!vlS4`w4i16XY~Oqf)XGs~N -1feH$#m~SBD=9*h2`VkgB=3B~RJf=&0{_}EGUTN~suSzC6AUW(O^&Kc~{=@EP-_^2nfuQ%6_FqQ>0Rj -JhHrNnN(qetBzN0D|^a7&DTx0YUousQC?3^y>l{|n}sBf`;;8d<(RG{M2Ma1!mdN0;-iTEsi0_TfG<} -Z{`3Byc#$bWja{WlE9I-EV!#(#y;?t1LbYW;;k0)2$}nlcuvXoB*4t^m&S3flChL@IG+O@YmWu?Li9z -@$$YLt$PEWr#}7Mt@?W$ksXKS -yAy9XjXhKFka=h#E{1DB_ApOaU;QlSt&N2^eWF7%L0=7rVG`}G(9*czn#gXJ!GM;Q<$wk=BH-}$$U<= -sR)d4!XO-V#I#bBy5_ZGUwUS)9k480|_JPT^)RD9f=~}Es<8VLd6(^MYvn$4D73+M<1eY2E1PB6KoSR -)F%EBz-fG%os$9@I89K90E_rRMPf!eim#0T!4W{W`Jc8}bnrP(@yYq#uj`Mz72 -W1@gur1&F`kHAOqe2|#i_dyB>2R{WEg!pGL%;4=UTWzUuG;;H9{m-2SjM=UfZZ;eH8dy3`$%3Nj@)#1R?;r1}W?!ArN -t~m)#JMG1-gVIW@pn_ZVY`0fUxdz9tqA91sSrAYC>(r)y@!?c9i63vB3s0DARdT-BnW*moiHryd@g?v -&RNoKd(kLf(FuT3t^k1qb;8SpL84*Vk@L|B(WqShSZMMNJ-qDmWG#&a?drSo&kx2o^lk>eT$2T#Y9Uq -J;Fm4DkRsOJkuwx(IP!^Pihjm+ghPBLl7x&QfHEUCuHiE!f71!YXmR$J)EnVMYinDE-80y>+Gvm>B5n -CLBJv4YZ6%DBjqw6lhM98$jLyTVn-anm+hY%^=>)6xwoO}^HWJ<#+{I1M4B1AE~J%HG)b}z*L0<=L?8 -|{owKa_TanC={6*NFF+47uY0n#|2PWV(I8=M7|%bX@Ot1}5sITulqHiz{BE2MT87 -RVKD*NJHqee2`R@sliBCI+sB^cN;hkUFmAHq;eEoFxk5qZ8gP0Q_`IKdl@L5eGtq@5SX6CUALqGJ7_prmXLmU$Md-};L2$yvgv*sXuR8HdkW;dYl%RykQ${u=U0jH -ms~f48zT~jY(=K^@NxUS5_om{MCyQ&vv06%mGWp;3I4MMiL!XwQG4PDVWz^M)jt-*90JpKLCKYF}DxN#bp03s -A@8_Ww<$dcq%-cwfi{Pf52dEkBS*L2`{(2?{2f8J9t~v8%{xs#CTzMs6Y8G3f|7c`Ruu@Ny -@od+?h)t=Qon5k27-3($(^mHX(K>HmU$UyUjeG$G56@mw}B3-9s=()-O6e`Cup25Ap#8MnArP&ziHlK -a{TVeh}N!L7Lzp-7I4lc*RI#iQ1uz3`kzyH^|fFZj>5Lys0=v4|NAwJlbO4?S#r -%t@{o`n}VQJ0hLPDA4yt-P`qtYuc8uPL0o(!m5D6_ZTljjvvtYu>uqA9afpZ0BCbC=}AgthYw-l3%2~t2h?(Sk -`n^9VgYNwR<|MnYt|O{b7C-S>x)lQam34*3zV{&1AKE;gR2O*YY{|E5cW5&JFI}IZ|cJ8SU%xQwNtP^ -IokI5hBNcVIDKH#Et*YPI;7wU!M*SP?aA-OpQ0AN$q$;MIlwUo*zJc8nc2ZllvS9oas{pw1PzGp;C86 -J(aDR4H#%tsS`ij(Hdi{WLET)ddefw}x;_Ac0|~L=;MpIZJ$v@#*&m-hJAlQx{-iKiS{JQ314XAV!fU -7Q)~N}J#E0xdc>XnkQ80TJI+1)eQWO`wBke_1&4Lm%<(j2ukTonp>QcSej(6C%zI4iHCxn%8(x?A6)xMGld>GVG3rV|W$d){8OX6l@C -p)%XLa7<;KIP$*4T*h6;Z-MIGNKU$8730F3=&l?VMto-i+Zz2ZP)h>@6aWAK2mt$eR#R!EM9=9W000bx001BW003}la4%nJZggdGZeeUMa%FKZa%FK}X>N0LV -Qg$JaCyx=Ym?iywcqzwu-wU5nrPWLZPLs(Oq=*|>lrup^`pIWJsz4XLK3qQ$&%Erm7V?X_dM_hs9k$* -?yY9*l?dPfI5;?OfTziujYwAQcH3Up%{p1OyPaspWZ1O(ZIxVzq!+vPN>oV+58Itcy0(V$x=BWCxF7e -u$WBg^-Jv_2C&Sp+%kix3#=33FZT_^}4&vmb-gRw1LhWWbw(WLM54P=k4ZW(Ltu7cg`?22YUk6>V-`A -D;{%hT>>a93g_3bVh4;_wQ7X8>97Re)+m003P7Rh56)*^W}ioU$qibe9g9^m&2InG7$TKsD-nkAIHZQ -#)f1MTWAE3QOSwY`GeU9O8>4DG&OiXo_#b>;H1#5rZVvcDAlpxXVNejaPk!w_n~HlSa(dS#v+YdUmdS -+8oVyVvT;YjLzCqFFaumQCB#%W_-)DvGAuiDNCVpf|vjYWolV`3Wt$vL8fJ4*aFKngk%aZyx?ByY9=b -vu5~`9{;UvFCUe#&N0;5OQo7Uu8~GB_M^ATK-8iKQphg$^|mTRbER7Rgb$uHS9RYuxDbov=k`VCKaGJ -BAD4nCO!xk*8Acdwi4ULjecQ*HZQEt(v|5X?7?vC1EQqeYYb&uGvTfa5ss_*Dd#q{;2xQPa&1@&evMR -^Y^weMtQK*7Xf>Ma89&vvK;8ek9!rh8>xjZPSvy%iq!BCNuf`_u{;$(hDk2SAMrbimEvlyZ-Kvl79jc59wJw`C$#I05L*veK@Vd9xSdGHD3gU&_RJa -a)J!Fw@o>03}^Zq{hF@5tB0jKnsT@#92VjuaJ7L)aRK~x-xV+zHL9W8mItb+pNs&!7;4xk%$~*I7VQV -*#-p4V@}J7$oBVWBR{|%?zj{%XF!^#1!=c$z$&txxxnOg-W9)&O4f_iqA7JPjZ~eZ3OymvvJeSrXv>U>K%62IFd}=Kx<%PEZ>XeU -dA+C-T|hsV -3jLstNE<%DN`^F?I23?Bpg)MBlvFC!U~EGrhzR^S+>Gha6kWD!EY~+J1i1L8I&Yrt3hVTIVh@^=aSYz -aX?DfofDp51T7NSxB<;*TaPnUI(H|MdpNL1F$-bqQ9l;2g1fpAgrsHL<0Kty%Q>J{Di}-kO2f%OJsFT -(XK5zZ>$AF9wdtH^q!GCUwag!%qq{h@W3v_?&BUp~{M}dYsTrsX!gN;-u-V7x=ol?fcvz^;alL22#|~CK@-$#>NepNW -1cYMw1j%U^aoS2YVV#5G>l1S6B%9YgcOglo#!OOhB~u0x5kZ#^r`6s~1~wRf4r;nFdMOz=qilEzn&vm -e3Khi+YKB*49yYL?Ri=y0Y7wJE12KN9Dw$I6fEpG_ -8lp6PZ?#vF^gOu$0}+UXEJst0vebf%3hsNT^>f?U>&hmVRgbN%uqMd2g6#^cGWHx+t%nxZu$mf~QjOT -=9h^Z9BhLPP2z0$|FUn7BgW5u*1zNU@W79Tl48#H@KrZ9dJYXaCB>*;7Gr@w2v$=z~z8Ll$?9XYGbjT -e?qY;zVIK*m`n8J|;>^hhg(To0J -$bzw?Z5T$TXt`Zev!|3q@MJCX3b$ZYKc#D%hQB8F{?EL?S3>Rw?<$qCW9rggM`Bwpp6BKj8IW65 -c&HCl{Haiz!;Myuv__rl(!*igs8CQ62>{=Ghk5`q^|AuTP|=1SmD}U9B4S!dcN#MSN5XMV16SG))^P} -*N)I9s(@u*Z@IlcMQvncXnEiy29O^e*eTIzCTGmPaYWEVdx4x49$(d^o_%3PZ>v9g(M++mQgRE?N@tH@VIq`baTM(rVZYudV1MS`&!D; -gH$;;%Muf9v(zItwxl~apg=H#E4NicJd^tlFoZ~GebVBnMTp@WmKMggb++l4k%u`HJxp_q-mNnT2YvT -P1Of!7Ths<6^PA>|ebxH_DXHHG9VZ6j};X-^dzYxi7ingU`^|BA0CU`wpF9EXW -AfTfdu1g-}JZM-FAlPDUtz}Xf&=%}!E!c`XC8T#Cf`T|6$PSn=2@G8)lEnzg|zDtxJ(*;zJ>CIkNmsd -JAcmyA>#fbBK)9-;UR;X4F`z0b`wck1jaywK~lq#d7no#-do}_}4NOda?P!>XS{|OL`{|I+N^6tG0$y -EaZkoN%Kcu%5WY|RjWI6nb|^P2$N+};pjO#pq&#-L5?{r&=J~pT{{5iv0YfuM7GY*I|zoaXqGkBl?)0X83l0g -c7&q}!<3!hCmzX2BLZnfeswG8sKa>>Zy@pv0}ivf&C{O3P)Ptk{L><;5M2v}TIJax)}|e92Gm-hSqn{ -3;rpi`^^O-FsH8*l82<8jNz^Vtts|J|c@&Q7w?t0V413Z!OYWKd%3%I3(f5f@v_EDx2v$yUCd!_b%nR -z&+hOjlLCpIV6~_+y2dj7ll3GeW3Jy8CRhlqEm0+(QsBpam-#Fc1bY+ldn0w@FHv!EGc~ -<$gP+7U2=cly;8hT->`7s}j6F1Lz0&L))WaWwW#WC^@fcNXS`j%e57VAJM$RKs=(YtZ3>XOOkF4=11~ -Jp#0eU7q|D2>~PpNvsVfNMTN0@BodD>sA+*s3%jYKbhCpAXtOA0*oZ8C_-QmGr6GsPC@P3!KU1Z1r_@ -%gBgRnjWuBz$LcY08GxoU6Vo{2j(XG)OF};wAJkbX$pLYT~rU(Os^!v~)_Cp$A##SGd0qi%bkI@CO$2c23TJsur)`Kf7h2dnqm`iP2J|vDC5^pRD*6A7sd_s%3 -nG2cgH75%_|vp48>HXg+)88O99BoWjPje`^|wFh#R?6jq9BKKQDiCtw9s2(zHr?Ly7k-)j{z5zxY(~J -hM1=5j|~_PXCtd!9QPy^y_S3qrKMQVBZ5T1Ddh{tPdTz>A&{uA%6{O!bU6;4b~CZQnW=8+Fw_Zp8`GO -x!iz==R$o|syu+{W+4{tDq_Owg)5<8CZBcDYqf88g==P6XfGy -AK~>&mwMo;@$KInEfRDE+JoH3eL(?l5jZPW&PJ?n^x*DHsPUJwOq*nJAiT&is%QdXg_XMb{jWF1Cnrq -pM&5%-76az!Y^xDKH&=o&{VceS``d`At_+?NVBqLV#4Q)#OtLhRf2-Al+G5LGpr!H{(AM&a8Hb*DLL* -2;@R5FvSYg5zy=hWr&1Ss?*ZU3fg&T0h`psr5jE`u7OSv)zuVjm_YZn^rC=;hyi$P#O^=P-9UUNGVd% -ioaiZewaQ(GiGmp4JClyUJb##dja!Mq{Mjk|L6!kYX7(@)gEL8bQfirLdWhHxU?aNvJjwIqA!)GV&Ki5cba=hjF9GPnxq4I`{6E4R=lw -LqWM^?9$x6W;ibdDiXngc7|2ZrM+3*I0|n52tPwXvLxaaBq#19TH^VH-lG20yz=srU4pM{(=j!wyhGGj)bB~QCG!Y6V41XI|QssOx;|L^%bt%*_o7CXA%Ua -B>q{!b6|myFrtt%D~I8Ns^x7H-8T{iIGI`C?y__p?oN{S*zYNm2~%x^iFqO(2{oK-6+=!*fX+Yxq$m` -NYxDvE{Q}MG4iX<{v>{>iXPAIsR#eU!&1zv`*P7hlWZGAQt%tKSAip6Iy9-faAVOwV2Og{i(JI!}fUN -KZX|1rkK#33^fkt~^YiDQVN!ae%VN5!KqbG?${W+>OTAD+K%X&!IJ~)JnI#Xl=HsN(isY}XfXSr%DSD -vDeYEBj|FS4PUdd!kt^$iC_Hs#k}|B*-!>KH+dY3Ym;8sY*-w_a|PQQYjSHCU~p1x^hJzb^YKAxbW{> -vllrl?Ca#JPgT;SEOs{HCoe*fD>Mc#^8n4#agg)G;MbQO=JF9}WXIMxy0`Ks#qQxSv}UtV9mygq7yENrmNlqs5To><%yex -|#!242pJ$lS-BhM1P~3T|%q)<$aicB0yEI|I*;CT-VRtw660u#?OOluIp~W|y&y5h)jimz|VY5hTifDfc5L!i!Pn5G_#Fpj&dwfMos&K?$v3X% -c9lwr&tCm>0sD)+1~~!)ru=_!#lrUHMKjJWXEXNuA*Us>W{Mq_f|TyAD}P36FQQg_)ItWLtspG$c#l1 -0IQ-GCqLy4|}Kzt(*O>JIqm1?2#FZz`Ngza+?HGqaZ2IPgI#x6h&z6=HrG+7#hpDlcbxydh(yePj8<+ -f4q<}v+V)Y-3B!CDtG+BL;xqBhI-$DD${Go`wT+5tjTNKZOf%F@fPTPkHf&=g_SSrVCPil+KxJUY7$J -6XCtMfU^kp)!)m3p(gyC!0#=V|XzY)skR2*$L%)=IOJ+|7L|CKuiCS%n$TiqO1>35^{M@&1pFJiyw-M -58)q}L4QGVypCOhEwHOgcOP)dFhGAlw3sv)8Tnrcyk8j4U!ptPqk{mMU)#B7kA57vJ0x4J8CO|e!jmvaL3;U1GTTrFRC4jF9l+92G --uqT(#fP8PU4?wNFHnL46SLtgXqY8Ou4Q=2A`Ym-QuHM%hjP`9L#blT3m^|(15kVvKWj)pX^&1xIp91w>W4&uUvQM`N&)uHq*vEMzLD;$(J!w)JumgUmUwdi%EPYhYM*1p -hNEP0Q=T>3(!34D-mm6MKH##_EUNw^s9LN7fe5p&>j~jfYy~L6ihfhOJP)YtUiV9Z>rWPk%eM-zgr6L --$V2iYj_W226C=nT5irV~c*tq-){(FYN_IA)AWQ7V$`(tM46K$*^a+seh;#r#1FyV5pf|T(-pC}FVD -_1P+HT>M1u4J*s-h+C)3~SVUdtagJ%0G&-hkETl<*yZc~?jPq=THpJ37wtfNWM5Ga)WY-(Qa1sA{7--KB -Ga1k4?OB4RMPUhcPLPnMX5WTnCCu2mZAp7 -Z}LpR5XN>a=JLhp)xb8R-ZQ1}3jrnW^nC2W7Wnw{BdS_lQJZJrG-vklUE4Ed1niO>wfs;Tu+(pwDseQ -wOB0JT{TM(jc?g<=TcqJ%@+W#*v;XZ+@Y&pB8NB>EG(uTV#-dB^f5fqdNirx>dupeLg@&U%jeM(i2qk -Gu>k=HOXU~x@y9)hcxTl{t;vmvgG9xB<2k9P&V~=;K?7^)s%cD^A8)XsJLfNJi@qk~IRHx!HWYI5xQM7}!|9AS1pn=~UunRY5lN9Mbz~PGB$`oAS^?lz+ImQG^SdmB1UYARjRfIP{6Kcrj2a!ft -yD7Albl{$&%MK+8Oo8hCI$T29p>(j7BZ{1E|@X2Y`kLi)ceWq<~w58dJPc|H~gfpbpH4kXrSyz+qUEmmb29O{>vKHs-5Odqjc&08WL-nQ^O)j7zKQAXRa#d9k>FP%)KB4Es&SAG -t-*#|@OVC1~=bY{lw2EI8Ajkz{Mztk;E-WlaL>1-k6i8WBpnZ=w>?) -_M}(?#QiE5fZqTxKsTO#_@=4B<>1`t -80gvG@%lC!=LRZ3W>N3`-Z4(g5TNB%n1&MA1_{Q%Y{H+ze8x)hq5!i78?^iQZ$>>AQ6@?dO+?*7mhuk -duAmLs$)aES`L;22!4@3R?Zo`{g?Zm`Gc`DL} -xj)qFX#NT5M(fSMDtZFja6S7M9mr@LR&Wk-HmIPTtpQVjc@$IVfAU9^KI3k;99@2zyMnRZVSSf;ZM%W -hn$mMr%@@|vwaP7}XjNrQR`ns|o}7ru3Y-noEHU`=;zRdai~#|CB&{~KfV!v}lDA3orYMPI80{FUGgZ -}NIm{-XvfVYd4beIO`zK761MaW7W&M}T&wp^TgTV9?Io0iA=C_#tEIAhyl5HRy21%w-Khc1a>L(mhp?T<9i_?;j#oAUJT3eNCE09cu;i2b -q$bM4AX;aenpvmPDnV6kludh~6pe}!>20=nFavMGM8I|ivU$|?N?2z#QfMJ(7PgHiV*--R4%0t6`e(% -fb&I#>Q7S0!zZ?jHDEe@os^lt2WRJ?j$84gbYk4;&yeITHW8!$ZMbZ4TW&OwQj=RfYAT9QBzsHZVqip -GHZIUi4up#1wdOVyJorcwO9k9iXz0Zw^4obqeUgI3d2oF~R?41Y~G3HAq5;T@*La^$*eP=JJY8uo47z -e)0|G29E7b9pr(Ep(Ji=oy$WJ=*nz!cJ%`))2i-=Px6SWVs-j|TGjOOpN>>Y&Z$2YfUH@6u -qh)(D+Ntu(j_B;R=BL?d%fZeMbT=dQ~*zo+=g%~;z>^N%gbp{a)mflqa5{vFop-NdYtLJObePCX*>v` -RYSco8VSz6=S`fL2}0{<)1`7yPZ?$}flgmtOY(61V-Y#3gZmvvfzt| -NeaIrLbbi_lVGYMuckSU$z#?TUAd=^`?wN(>`iWD4^#Z>Bf`HZqZ*96#w#@&e+0hjJFSdepr^W0;)zJ -?iTTL@I|P=l51+0qPb85qDv+j9?K}GNAWycJ|QSXQ)-U^mCkxAd3cNuiK*vAm5*r{de6x#gL;b2kp^2 -B}o0w0}bcd%i{5qr_Xx}Uh>fk!#JCb2s%+qZ~buU+^?&<2 -F1+f>tuBfI|uN)4OfHav;F+G2gB*(iId%!EUe(>AMyq*o``s`%| -FsJGj>~(zMM1sIZ{ke~h~M}LPkEQwhI+P --tIDy!PpLdPFuhPhu#Z8==x#N_A)871WUQ2gpcCZc5+;-~R{PGDst9RbTrDO>D$f?w+pu#k;@Z}G8>F -drr4JUGmruIa*xRF}KYjge+7UK&J!6+cP030B9?ud!71I6`o{Vl8%6E%74VJr@;7BV+qqod~Fc&-w<% -$?`I&2~FY-n@AH0&iP7$Nj -DjX%o$y1@ix-=?!YnL6tc_BM77ozc7Gu##l*p7XV-uQ9TI(O)sb~3{TU)C%yVnVkLC0Wu$OPnb6maIY -dvhP>G`;N?;aMTpoDUZRv!M>m+khh4!nfnx-k({sMJf2eI6cF#RhT=UpOF8xjFBr^kDX?C@a8FsBf1+ -&8?-JD$IuCx1i0L~7{3l8AdvS+57}9Si>5nN6dpCD(D(@ppM`jIia`$ViJ>U)~%5#cW5H7Eq~ -6kh>Zf3)6}!8tUHI!r3eA{4A6=qVd`kX$Nj1Z;HV~S=_`9XU4b9>@(+4=z7Bia@(;zCtd~fVs#w}|bH0HI=+{Xv!8^<=l3n~;4){Q`c}fmQMRHj%13Bw)fFwT9ykdudL!f{t>TSpM8&1}50ni*2 -c)7wG03%muNAa`mTwzvxRjh;{-f(u7b82X8l*0sP5^Qw{FxqNn>+Z0{^1MDc_HWn{L+gK&d`_`acGk| -x2V`&NMSCe;YYpMkk_GOyV3o{5zJ5aU0PbZ%QD++HkIUBGf&t@Fxw5oPYnlFqCjEFGSwgtIjc@kJ(Rj -{Nh;K(wNr1|UMY!^K6O20?9bn^cd7x$gg!}<{Lle6!1zK?flHfdAITtxj2ngNx72c0lNpd>?A+$Yfk&N>k=S&1$|*CGf6!1%v0O+UBeQV6kn8RQnV=@GC1>f_0`OLp -LEdvUrxF1gg?;Q=_^`v=7s`EP_$#o-=1x$5Zwz?`!5J$e`U{=>KpUotDov@%bxLte)%q0dN1HwxTny{ -NyyIHTbA}kSr%i_hQ?a?I|)r1_&wZ2%IuQ-H$`sc6bd4*>oQUq13C;iR$8yGt0aO9Xc-`UQ389!Ha4% -9W5L@Nkv{b(7pnf@imsHLHng3hjIbgJV^%r0B+ahF-o4L^u*IVd4fIV5_|4IK#nyRa%R79geOB&v>ac -{rqy>3o-VR?)$F7uwGI3J8kXiBqxI_Npsjpg*S>)mCdU0VW=4(c#T=_Gn##{>gF -l^sIaoL_gWm&2CysdvP(zL1g%0WqLjiR$(+kadjT`q+as0xt!!+#NOC!*5Hso?pZi{l&Nim}a~oU$(1So -tf?FLS&;Xi8(W%Bp_sqKo=(H!y;q{hbh6$IAVT(e}8jGBQj)tO*ASfg_24F`H>r!vJP1v)eo9^uE6tz|opI+ybUCNet+iGRoqSg35zwJn8M!eSbF{Lmcr#B>Xl -I9!Ol76+^(+CL1x6E9NOdnENNI|ieJo}s#CMUuHWzF}SLeZXKGP?8ewj*sPK&{K8wp%(kqr%lMG~ -C}S6-W(6Wx}r-e!Kjej;V&Mu~61zat%#;Wi7&4KDB_r)+NO75Y9|9NB|kHiVXLbeJ0`c4<6d4+Ux2(!hQT2 -JH;7KCoOHnS-g-ry7yQZwO1!G2JB;1z&NRLJi%RcWZ#6!xia8GoPS(TBqA3tTNy#QY6-U`89Unj3SIQ8A1ZyI>L@-rlB+sp0d8tuGlA4$%c -B;y1e`^}HB4vSXIsaB>)`Ukz%>kE-8?je#A-BjB)vP?wGqBG%vYOK#`Z-SaoyOLiODJhSq^1NrSl3=M -WqUTyzOlXs&gj)B(prOpWeRyaX$;PLT*!G;ilL`X?v@B(rOFIF%SRA(F_egiMv!jx4LgrIYc!*8SN|%^loP|FWl_aqi6raK+j83GN7|UD!Pa=q$RMpzArO9NjcTk<<_HT$0p8Z -dB|xYTD;8d1ixZ1-e9brN-RWhfC=R+Qr8h5}sa#vm4_MP9`S;i`90!60g%6`u*NI{lQL`b{cg&+04^L -$8Zp)H2sqok{clk^ -%?B?0Q-5v*bO*hwT=G1mVL;f4p{aBL`Kue<&L8%fbB|uMvNIjiOjYH+QGud>md%x;rS$J$pw+zo^WCe)V?I71C0j# -SG;$iT$V$9hhFZos7*;-Yx8M%8Z%19#Vh5;aE4Xni=gJubAn%*5dCt?xu(2of<{DOE@#$0zT9KIQb-l -et*YpEuN$<^A)ZupGoHY+sL)c(d{zCS8vDJpO{1@>GfYtvd#YhP)h>@6aWAK2mt$eR#O+blrnz>000# -b001BW003}la4%nJZggdGZeeUMa%FKZa%FK}baG*1Yh`jSaCx0q-*4MC5PtVxL8vGu7pfkLJq!jitZU -ODXwh|P_GAPCEzvd`nN&$Cv0Lb+H%^}KoCp3JKlYF_uX0UDs3FgwQ^$bO1YZJ@Mg>HcJNs0%| -@zC`>5?~ZS;=0LnSP0Ufvj!wcpl>MF_}aEaD3~MER -pdLNqFp}#Xc96V{gUb(im-iz|2I@y(5DwNVN4nAUtyOAVpb(XeElM92`Ai*TN!gf@vc+blPSGAoMv&J -B1#vR-p%N7{e)Y^pWcmH9HmMSc^VruW3|0vZWQLD%@Iu=8)ERn#x5yzB>uU3k)cz-Jv?1m^cD?tpEKM -f^KH|hRz&V2+_skj)?|6e-k94rh|;u}4a3INu*hWI{iC3fq5f|(=dvbr*ncT(~6b_PceFB1n?f=14jcm{nZUb!S#@^;JbHtszw)Q? -z<%AAGoh8H{oid{L#pVy@zjvMKUDQj3)e|o7791(aS%%6_=i9@$&X|tbn#1$d5&jOs|H9hs#4FcrJ~3 -DY&YucnBxu?~qYvG{rz`!on{I-VCauw?fMez`JO-oYZdlixBb%-aO -b7OM%01Q!yn=3{sLLMH(FW6bO;2B@0%%UYwVYnUZ4K@l#dGV?6qae7IPSl*2m1@ki!^<#1A=yop^R+g -Wn}kIVU_IZV(7uRjw|@RxG?_Bw$QO7FIv}71b8qBaN)1Zqy_aMDMJIwlp4y|v!@~U3L>{g)ypM?Vz4) -2tzxJAE`rmia&oRJjQ3~gO2C%%DVxA1B`TR?5&`p(AKFuvaodJ+ut;l42zW2|e+<@^I&>?CM^>K{f2f -G~<-H087N-HUqr!P$xk9<2?s{PsjaQApFo2fnEk8{qYBt8UO&qTmS$f0001RX>c!JX>N37a&BR4FLGsZFLGsZUv+M2ZgX^DY-}!Yd9^%ibKAyt --}Ngt@MJ_fB$UU@omL&S$;8fFPxH8AJ9RU%1_p^Gg$M)?02Iyo>c96q_KgK)Hz__yAh3J(?Ai0)vnY3 -M(+y(X-=CFyyV=+4p=qjKKWNNde{^rxO}#0%XPd6s34U6Yt8_na56Z!3AmaNDv?vwK+=O{amkci -XI!Ja|89m3psrpJy;mSFZLrjtjZ00TmcImG9S*urOVIt;@O}a1QwRdDk^vtYg)zbFbf44%w=#v$m6)^ -8Ld6-uE&J0EyeD>t@&HgNE_u#m~=vdGl(zx!i8Es;uvH2mLz(Ah*0a8nJ00RkPhXiw21P2u~y|OIa7P -9stC4y7#W^*ShQMOu&`*cMG~HfZCawa$gS8ecBUZ6+iNS)lIj{tMY#&&4QnGvd(w1S3h<`Cjr`g8mmF ->+N>xIvwTnZ;WW -4u@M3|}+wRLtu>#SXi{-x)yI;ern@gHck|f`EG9RQEZY2n5RUO42dmzE4l!ZV*qS^4fvI1saQ{zIcj@ -11FNNeAKK*1mn3cp@#KvJ;FwgiC?@7^J*@7|@<(GBL9^f75HFu;yfBb?@acvs6 -Ov2UZ2^Rf|o{;2HAVCmt>jwyT=~+Y(r>fgUW)i(-aN=cCuhQdR|lIdh52Qv;fQs|b3gChXV~QgJao;? -LdF74-i%8WX^f5c@!RS&0S+Sgi6Mv;{nW`TYA|UOrnM%Hg)z4@+Q&>veN;<^bfLLt`Mb#wm5*RQv!?B -^ZAJy;*6PmkR+jB4F4P-mgVo7kJZPBYfB+Ll#=&raZJUvHyV4VM&S9b1~$1sO<92sJ|=&nB;h5LLsj} -yx1rlK-g$4H_g5-7NX0`9)LuQIGYkAg=m|;FIO~^4cZ=}A-gj$=!>6Uz5edUAD_RZ*{bnwhoS8+FD}} -AeV3#41ayF?b=q{>i@ud>(A~-*xPZwvT>)*D3JF;(xu5JWyhRjn^s(U~qV}vr}>)0`O{T4<|zRzoKo3ljjBAQ_7>F@yN8yxL2miK8zR*=E1IV -&)L9hNfllBcpCR1^T0qEmG{m!`0P?2agbG+cm!fpOX?^kR+z}XhECWpAHiJi!0?kxmO~7Yxb~e+gp%f -DNxIzXyWn#AkCq`@b_hYDUxyjbD)E!j94xKxwTw&(mtu1B}||fA0Ez~Rg|rd;3?yykWa+l!2`P#uR&l -GZ;M0Tn`J>~1xB%hnDw&2d3hqDI)g#xJl!c4CLpNry5@kort__R=upsNXwP_oovy#Uv6vis*UoOA78CmlYx&W8Dpq~LMF!7;9(EUhA%`_x-1t=a_^A -x~7>)4G95}7Epz`iHTJw(pqgIPwN1s8S5l?~XHbqNl>sQ0@S3NKTZ-OgG5wH=c@ --OlWe13suhPvwqLj!lDu7~%43RsX;1IXC!3sK1&{KcjQUmKoL@BS7-1;B{Ng*us^l7yQTm91@?DY9rH -cy0)CBar~z3Itp*6&K=89mtB~uWJZeZS{!RcX#9z#1zo`yOS4KKF1hJoV -k^*zjJKEFY=spqCIoPp^|)3B2^4Lwg6)v_}eUpk!LWZ|t?D2K@fZ`lum_&x(vGL8X1ffaR_lM@#zZo) -`+AT33n&MidSwFLbH{02If$0(GQe~ro&`|k21%zs_&3+ug<;8z@OC9@R#1TO%Xx|iVS)obaqwdz|435 -r{}P2gjI8oEn84`2mD^Mm;FuLL-Z!p2;+>J!d74usOsfR{9Uq4mel$~0XFbj53Mwmj_SDU!E?ovh -(HTL*E^}9Y4s!xUmcmRW(B)5f^^LD8Zo!=86?$Vw;1pQ*Tad8|dByF9TvpcT_8Up8s*Jqo2|c0P^!pV -IcB&OS^d0*Xs`5Xpf(hRVGj%F3F> -b@$T&#$CrMx9eDTV(HR9Rh-Ql)`SkZ))pz+!&C+kj$P^D&(1;8A+db)Qt^Z`u@>gPgM -67*L}0S*n2Yk!k-j~xdlKt)h@xY)R!;L9&7P`#BS&MKtT&MHZ_IDdN1T^ZOwO~B=Y#RiP~a$A?1Bcp` -8TMp+v;VWyHEso9XXYB_%8_=NeXQr6g&g>R_5mG8>Hd|R}S0!IzT84$cf-tbR=0Ypt%I3b1If!a=`%? -K7j&6P;o&gOzaKogAN|Yk3+_8LjhH##8a2z~5eLVJ=*P+vFH0E@-ECZ$xnjnK2 -%LE6en#ElwM-T!%?fG%dAJDhy>x=3Pj<^Ayxy#(n0?gklL>>SAW4#|Hbau9xos?0b_ab&A)z05Hf9li -p-@Q3U6qkloAJ_g>i>tD=!7GTGPq1U^%oquAUfN$yA~8=bSF4$3ebOZ7QRH?2B@N@e>5QLzio)3IZ`B -hnicd-D%xy`Hb1zG5Wt1pYFEUd8Ba6hH=HUtfTuM-uwdpku+JPIW)gxRq;-=ACgrAKAL52Co=9Gi -c$8kAc8cNwC+!+BMcgG7wYvht}d!2O*S!D!gVzN)f8%b)1v<7>&C99WId{FOi>s{Nst7nc|> -#zFX}(7TT1lfm0bu7DB_&*;F&K6=E4pQ_Gw+^z?rXxyuX!2m=H9k<~!F7P|mt&T9u|D3)3SA?NQvqKh -CUyhK4p^0**GVr9y$7jrAz|t&|M}M%9K);LjlZb^)GNP`4_yys1QQC4LDru?WP-lgUNy*QW1{e@ -W_tEFXDC}4r$hddC1r~#68q&+EwyEqqF$o!5LuIs6c)J;mv{W=4_uiF-$33%3*m@mfbnG=PP1Tg1-;$q4xzArl)n(2H -G2(?WPsnqWRtqNwhh$h9wx@k+Y!b*J5Spi#*+lq*e(XB&piFchGDr}qT%<}x#vubJAH- -c*7gvd4Z6~i2u+a55ti;hN+E8um%(T;~UZk8E@|Kn~#{XqiooqT@FVMke2|{0toFOp5_azvj1({z|7s -!%&`*Y;2TMwd+Et=vX9ct6^`zbP?B&O9Z#C(84Rb+VQwD%l}1?dS(cDqva+`J&AL@>o?R|cxT%p;BpA -ckv-5N<{tRzRpb@2O&~|CGlFZ!tl7-}*c{cYF+U$A2f%#Ncz51#cOY7=hJCh%L*3QrfuYqcNwb{AZ!WUq^vBD$$83Ea#E+)Z6qkW2CbnF7c7~{75E*=@ZHt=+c8c@5q=T*& -P$Qz%+|Wcm};}>IAq5vjS=(lp=F&9>JqjdN}A!b`#eeWDayTN6zm#yAbnixEJ`ZzwZIO*6D_x;8g*!f -KK35GNi;$!omU2=!;&k~P -Z5aelMxjwDf~TcY%<=TIdE6gJ*OkoceLD;7KJgm6Wc~{Sm_0``&914cRldGEG0|VaCzO$S#%3uH)@Z7 -&ogllC-lVt;E5+!3mi)v(fw4aC->Pl-&--K{p0fT|TW}vCG#rjrg!4KZ^EZ~|jkYB4;mqNmxrtFfGB`CU&y~pv8o$)9* -1;sEijn`=gQDC)d~hNpF=;vA_EpTc@t4unIGmssue-fO`LtD$tS30Rci>El8N;=YynYw#$1KDTIa_MRvaj5S)ps0w-Wc!>&tbX_Dkev$Qo}N@1W5BV+66X`gK&fQ`_ba>I -YjBf_{~luF#sJgCj`L(ajq)%+F;`ePh)E9Y)1&>|<-uk+P`#3&X+B<9XU)lQ6VM?MDYs?jzKE`ZyQ8P -Bp&q>~k!V2>KFvH#}^P?$nyI=VG$s(d%6t?q-^w<6!*#?-kj3}e?|ct4yYV=A|3;lL~QR0PCz&AmH2k -3F!mig3C9iuU%YTqS}#;ykM5*BNq`S+jO9F$LC^1D>k98F&iJ=>!Aowpi`uueW|PKe(Wq+E05+AI -N=J?3X7%P;cR@WBQC|^^;@yd8H0w`e4FldfTclVNS@ZqSWOVYP+i|ft;7Ph>A?{_jYEVbJkK^9iOLpT -~v6fi+ZF&0LJFa+CqpajV7Zd0LjEK5(?q|JpxrenglTO@dNwC83+$}IX|GYm>VJ>DXxYL -`#uBu6sMSOL~^>$vnc}y>uXH?%#RsO0xPX}`&s8mXwz+d}Xbuqv^-}P5F)BJgVnJmVGIm&{T#-6zdeS -Iu%a25vLN#-#apgDjog?E7M8iKo-;+KdA$ezg6*a$~=_CMdda7a{Xh7nU>><0=x`=ByWh#R53uZaKXiK^NfHO;u@HeUPPcoll-WYy-QfB6*YC=r0z -Rl|`umvD@UpD(5QOm_ynW!>~KG;>99r`b9jRG88tjk&ejdgzs94CAVhkL(_8D -vBKx@$%607IaN86HL5dKNPP28@Po^P~Fot)+4l;Kt77W4xuOxp|RO}rZ4G$oO>pf<~pIwGiIX%#opTj -J)hLH7r*)=sLUx+J4KnTG*d5pht&Z@dse}tfnPXIs4y8@`nQ}axYr5Ui+2cMI?Una5o!JOcxb2UWXq# -uf^TqO`;$KHZ|8+3~Y4=RRQ=E1{XM07kEGGJVxEAj_$Me?o?fBm`H -ZupVuTGt{9-sf2hD0|=K|V%=N4mMLO`nwUSKK|-o&wHj`N`s(7}9Yr)L0p3O;lnoH7K}yOe(SBHE`>9 -U*RdYyrN=pbJmbfUpuFM+}cdNRMf_uHn3qn4C;tmrZ)Yts0i^$cwgPisTd7IgZJR+V+~EwfPJGv-`p@ -3Ax8zNsz&pR4WW0bbj&9~!)?R>jH=q+;Nc%d{gKq*a`^o0%O2%PCOA%!m)_pyy*Vy8OCs5`Sb5;aIVQ-3c3 -gQF96yZL`$Y#CwVZSEjSnzR#g5~e6Vk!s18SNEivaC;JQ|MCavVE95kI{6@1LGu@+NQ&-b6pBQX0_5w -41a7J$4s#YSzI0JqKRPC6<;}a;dm~Dc|$Q7pmhDTy_wn;$pevBYN7thTt|RXuhOW@zouGfv9+UYF*>q -XbTIZ8r=&%Ydh5+FVouSM2{sVp^boz!?)-V*W+p(S_3`T3=};b+;4OD?atjU$A8{c?32ZTm^@;KkFV+@q+xSJZ_ -EIJ%}9@=A#mOI)$@l$T^C9K1gNn_`rahPg+TgGGfpMF4>^AoOPPgW4jUgJ$0Os7()}&JYbS2mMp37%w`=`Pb2THk4%4RpW^tB4KD>FQBiN}k>bIE-#&6lF4VPE+I>#sU2;B)Yf$)o)6AgCD0%AS ->3Q}7Mm2DF<&@cH3kKCNh66c!Pca|)-&r7k4$Uo1Hx`2RQI3Dn1l#I;wx6VDIydx#ovFWe2bu)aqu#r -Jr=_i{QJGm#=N&c&2Hc)~Tv)StUXds!_OY%%H3T_gt9(et%db9~mW6AsANU^?vvw}%=Y|VZ5`c$tXWOpX -w^vEfHu1B?`s`cK3Ugee;Bkyqia}Rh%*G$2TL{)xTq7_KC%)a6qe(l@PEN>aH!&Hf#a}iNDn1B`Jt@O -82KLC8(Xi%fydM)Zi&XlV8j(wr3=eAZyqcJ)ay(TCqucr~r5fR}YCTN)7yON+OY_OhYdVUxIzlv0C$Q -|16TjB%WUUUpjM?>|*5!%#r5101mzqOAF4#hTz6TrMtHaYnr;mF(0C2COujueWs=l*<1;;q}r~frR&LhwHExHnvK}p>Uax*ZdY24~tK -+n4R_{)JcU@aFqD)my&Lg7xH7(=flB`|k+cURWi9$b<;An4_$Y2L2#&Vh!<)jcQ_`GLeY)*r$Q%(ljA -z+ZpZe_mOf=c4LZ}}Z6X>Mz3?? -322H&0DJWGxI>}*)tr+k6%OJs^8I}Hn%ZZiRat34K*6Brf_Dyk;TeW2*!zsXu2*hN433_OnK~-(I*;j -T-yqFd*?6*5JNm?hJ#XWZ$v@;mCFORUsKce;&2)-DnK2JxU_-(Ix4Q^4yAwJtfe -Vnbk%MUubnjM4G;nwV?MYRj-1Or7ZTGf5S0COxesjkHs$SU_U#IW=siD5ZW5_nd2vb)h^$5-Bze`R0K -+GyZ9O%OV6@LVw;~VuL{1gmWCwhG)xZ??z*KO)Fe4;)UqrYDU${;UkPpp;)?BJrvxNE7PU)Rg8Zfvlr -27w3brU16w51Zw`Bw;~|>8Qy-_g{#K-kx=30n=OA(zSj1I(39%EHKLMaNWLdO+0*cRTT@W#dbcLtWS1 -6jbOjh_sGe{I0qjlI2d^D(1B55reh8{RhHowW0|T}moveNjEkiF(-K;EP4$GX=b)o73zA?il*Foz$Ap -)?!BDg5vo4LQbZKvHFJE72ZfSI1UoLQY0{~D<0|XQR000O8`*~JVQbZKvHFJfVHWiD`ejZi^q!!QuM>lJ%Uz|8{;>7k{EKxs>I -G{z|QIuVt<%I+qmzprd#x5R1bf({yKW@n~mYCu1OYY*U>K&?mh<>R)uR -7IbtiuQ+FaD8hN9}X1H$gbasplw)z)YP)Fhq#tzk(xzHQa#Z}0#o}6X|;$))y?KYb;^E|m_EH}oK-ip -A372KkbzaXc*W`#BIfGm2T8$n+uz(iU^_mcKK-P)HxdBpO)kaOt5VO4w_5q)IriF~iOguDBz(CM^@tr -LV7(oGY5|BTfWGx1_+CVL0ev3=V`FNkA1Gq*#{>2<-Ahu<>%&&?N5O9KQH000080Q-4XQvd(}00IC20 -000004o3h0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZB+QXJKP`FJE72ZfSI1UoLQY0{~D<0|XQR000O8 -`*~JV_%`ETzZ3ufh(`bbD*ylhaA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHFKlIJVPknOa%FRGY<6XGE^v9 -RJZq2JIFjG}E4U7FZ3DIjw~r5NocEBKB-j9%OfZ|-`H*dBXoiK;BQnaEjMO|Fjk3u!o(95Qt%|09jE8S2x4l!A?R<=wk){1W5bSF_VuGqe*s^+XVvl%>Neb -cTh@hB=-`P(2naT0Y8gA6`AV@+LM;D=zU6$RtC5}*J))^^RWqUcw!?!ddU5=yaV&x)qrSF-5gOULS_& -j=EHn*J!dvs8j{HZPlDR|$X1ITN1>(Q3pM>p09TY3(j!uoSR(6kR7vKxNpigA+TvGdfc+K?-FLGvCWj -^+153E?4X={EsYud^B0Ovg6j!Ye!p(@!94|4((fyD5 -zISJM_62^fE2zx*DLnkr-bGo#b4O4vwL|Qj=U|wxs^Gb=?OrF}N>si$t4zP*+sxE4_@0t^AT2(-ESfZ -F2E5)7zk$;XB0;+YdzLqTEyK9ywpoyO61}jSjK@TEB00@`2K^zY{mW|;OmE>tpGoQ0z -#GLk3ij~%UWJM@Ph{wSu4uwbiHzBi{=94Y*CZ)4@^`L|uez@3qOVGSe+N49z36$#d+ts9sa8Xa2#E6i -r9U>c*?l!zrtp>^v7Zc?M83zbojt*O{q!|GOf5c{?d^hZxf1?)l53h$R4S`v{a}~9h2K+C8&G;qG|k# -hw7e4u#=X}gN&JB4_zxZs$wST3Xh2&q6fMwgCo9=s@}3_?nLLO-AI0N30Amd7D&#+$AD&)ca+0bT4Fre)jO -T+w(d$9MYj6cX&VLKgOOi$y-Z$5wO>|r{#ptlgTaxVA3=su+-CFaN!2#dWvqhfQ{3P;xIh%n<7HG;v` -O-7du*x$>AA!r-5Ph7I{QiD6n}OM5xyFMI7$KvD3P&~*J!#U>|JroxldS1y(KREQeWN*_`P*Hi&(HSIjhR -AwO-?XwODAD+e~U`>UGEXf9-Bf1b=A}oD)m>uj%fJceNLkL2R6x4HtJFrXbB4t%L~Rk014=d8~npMKQ -J95Sm30A}pl0iXgJ2A0&DRuF&Lzz^26qHj9XP=eHVm(56k7_}`PVu8Y#MMFzkqg^d~Eh}LiCiy>%G=&EinKbp11b9VTh~;vM -ja0d(8QV1Z7UT590an4FuN+(di<=f4%SAKU8|{Nej?e)=6%JQz{<1UgY9MWh)ZBP)_Pk?9SjdsT1dl} -;v0=~Ee8n`u`I+qI3O9-n)JKJ&ves$6EfNRJvcUq`Y -kU%f3b(bx$_LOFy1e2oN7EtjD3VWc+y!>aY(vsc(U+gmF4J&%o7D}NESy+Y9O)zmY`Jtn`49EZ-#pO9 -mh=G$o{1@^Qbd62%=$lj0eeKHDH=BZF;a?C5Z6B_wf2+_jZ<*e#uQ%nPX_zi-V2^KFbP&FXCN5`z7-#tuQ1m2PV8tCL+T1LdV3N$Ok@#@X6LXm1eT#OPP=7mNNVIDxSh#)oB -%GcQcvzBnnq1~g+IFHB$pzCGC_NAn!B<(Nfd4&WV{8;)di*Hnd^zS#k>X!XsJzn9qLNt28m*)W79%6c -x~8F23ZPy~zE&m%8=YiMVqpxBQ{XV;cG}=P637$pof`=}QVA;P2=D@(s{#YL7T{ou9{mZ%!R!P%MpLf -H;f#)Lfe|-6ksb31<(Z5QH#pZ-0|E-GhZ@&nWl3# -ww+l`DL388XDfnK7!eTPQ@ideC&zq -QOla5wA9`$IG@fOu?0SlHvOybQ6!Rrjg8=iT3iHS@s(q3E)*`f8xCL~%Iqo0mR$%>;*-}tCqarfJ4Od -esb(bLx)Bc-aF5Oo5Og!7>rd@ymiWrD)%pre!`wFTN5?Y_ZgRT6ciiwuxQU7$6JSl%}-hIs;g!%)+=v -sgjmK0D)FEp48kv6l^&$;L#EY?Qr}Lk^Zy4&oS|j1G<&m+66VOLRUSF35bDz~Mv$1mucq&!}gamJ%Rn -zPl`A?!qu#s0FcM!0i}>$sIgi7+3QkTNmd|A|1`EF|nEQ$AvOu+5&9954ff4yV@~%;Jiyg#G*xJjmxN -xc$P4$u&YhJjHDhB)*i){j=_i#DRQu?@siK -#Mi8QJz|$p2SNWxxeU5;JFQc!L4gv=`&!Q>N0Ao6QkSj&|ynbPFw&>aU$UdjPDw(TlN~D!Ol?n8&8i6 -eH0PgoqeMXAl@DH;4on#SFjrr;(WYNAj*{$>=gy?vg13Bp{rwiwP-2B -~G=%oaOvKG1y)rCO|I%DiL%)vNxO;M=_A}A-Gg|BLjT1Hh6i$G39&_omyAAdG%!rJrj`|b^RBQiMpq^l3pRuV3 -$+ZS^EQBR3Dh9L@iy#z2Pdj{g>_z)2lo*&-oX-}CN4$4_|KhtYV^VCP!jnY-Aw6OT5?442M-(vx4+^b -^%#!|u2$q`YJJs4!uNpb3~hcO0C3H%|bQ<8}8RwVvpZht38Oeh*m;hWGWF;5%4qsxEWve{*sIQldi)H -0!pS#$3$|tqf;VoaUOq));Nr=MmL6CfRcO8&+ce4=LZPS`D3vHbGRm7HO>6?1VOx#&t@yGP5Y~qoHKXiYXESIe{-zy@{A99$$MA`IuQ -EtF*OuIy1)3=uT0&b?^|y?XG&HVG@CKpx*39U@054^e}(3A1^0tlR=I}4hYc!HFSn6W2Yh5qn)UF$ml -9!S{|dvrJjS8xe8pmV!J_B*jY(!8!H_^xC>je9gwdM5@D+DdDUX3`zXn;l;RVpL5DpTIx!d!K-Zkv9? -KkI#qT5)NF^YqGWL4tFrztvNn3^3V)LkJ&(RnHiv{T8N!H*zrE3B}<)v7t94HP#JlUy%1IfbFe&tE1t -5NwVBFX!%anw4Iw37IM?u3C+4d)uI2Z?R0Z695f-bWQDKO5mVtV*Zn&j$%&`2yxR2;sK;%gcd8}CH~%Y@*p8HASUU;vdquQh7kM8Z}qed3X;Plw%@siIMdTC6bQ{LSW6FU?=0Yy})LE;1ASsF~9W6Y$@L$cyFu?{il-$AVr>NFVj@Cj)5GvpFjK*sADrViP5%?{-Ef?dq4s06PC7AG;{vtX^(C1*q^mf8NL;W?mRcu!t(794ceNM7n`ljYrhpJ9LFFWjHKL$d8H_FHF=IIAC -uc1{1^vHJu{O*{LBNN5SORe0wc+_l@*d9lhkUjn}mt>W->6z@6j4~ZqL7F3@;)4Qm21uw~4OQgNa|cw -eVV~uHDrJj?{F%IZqiB+K|)f$}skmQ1z|^KSp(k1Tv(X5DGWbB20K^^+7<2LU$5pQx4ZX8q_>$avvYF -jkmVhk@;I#hq`n%%N1HFEFmDA1%>`t$A=mAMXLJXn+be_3%mqOVO~=z#)ZpB#e>T9cpqpK#vimHMbg) -OyuVuR9zzimZh2_Fr#^8P=CEV~l<$+m(Cb=x>X~;nb-NWd)u!)(xz@0+-)z8z;kylSW3ZEwufE~opY} -$lcd~aboS*LS+TLgj^W%S)=9!6K?Uith5~M}AXrYE3g~Yx>Gw}(eOVRGsdAk|k)uRi^L{Y)x=2u==@Q -q~ReFX<>(y(^*l?4x^dt&gyJ^(K3xzC(l&k&E}1y>m?kJG!7;Bn`YWxAA0vSGwjm|}8lo}k -m+j>V$nGg3*&6q*++rE42jmB^Pkb|N3e6HLD;}zJQDLrXBRE*71Hm+}I`z8Dif$2aZ#@uMOipvB6iCI -pibz^$34g>Fa-HU5C3c6@z+pn -<2iEyPet<0Nekk#RRRB_}rWK~?ZNz=|An6W3xdXMoH%u*jl*}tfvDMcyB$~GgQ@AZHM?~73BlU$kLT_ -v@@=EYG8>LhX0HaxOm^gcmN -}pKeZ-f4KTC33o%GSMnbuIF<_bvB{kkL{I7A&Xx+ZZvdp?WWCQbZKvHFLGsbZ)|p -DY-wUIUtei%X>?y-E^v7R08mQ<1QY-O00;p4c~(=y03zQ=1pokK6aWA#0001RX>c!JX>N37a&BR4FLG -sbZ)|mRX>V>Xa%FRGY<6XAX<{#8VRL0JaCy~O?QYvP6#cKKAXH$G0Zk8JAVYzo!2$&7kfz%Y!;l&2Dr -O^-3P~k%i@y6VKSWWs<0YGeVL^h}l?HIo!Y|{y;$+;BYfPV&^lM4-!-nNjFPn~9h( ->5nJePJ{-4&{XiZu+R8a#gj!VZpJz{jsbMSQuceL96?iO?6Kg3lqjwNHA#YWW7s-k85K3L=DoR}3=!b8sN$_fbgsRmLwl2uUSnsTncgDcjU~`u^8xCq~VTqIkL9ckGx!t&O8q2&9b -^U4AU}6k{TM)7(%p#KI^3T3YDG{rDaawPc5mMj|y7g@^VIg{>5CMCj@_3L%7hxt#--$NYK6H#QU?$f# -}lRjOi)F0_to}0vXIqS_BB=-t{br+@{}uEWZC(riIIWNINHKo)%vPqBtK+=$x2O^8-8PNK!+AW59* -+VhcwVqVmF5f_od{%#KuMjRN2KM7t-yTWgVQW#Q?6$OWj( -IDuse(U(Qsq?U^x+bZRGeWB>g63$~G`wL1w+=hrEndQ6*PGUu>{3nLD5!br?%{s_CCxqQ$Y*j-~o|CARACSyDat)@3n -F>r!K&biSTe4vadt9}KntZvwBvr^t5$y4L%@lD15{ejxs7GL2%Re%mmDX2jE@Z78fFVWKbKQjGk^Nyw -SWcq!{{Z8ODB3`~#ZXUlt4u`kgi`?PX@M#v+LneotbqLHvOZ_Lj^yzUjU>=|Yi6ZWoHB?+UPvqMSe4t -lov_a(ks+zEpWk2vCdF{7dFMN8m9U#kOqw;s4Vy#4kvkB@7ay)VVp -hDMsuLP$I;b+)Z3sVKg--44E<$#Y8nYi)AR<*NWB#?Rw)q;p_q4wRV2aS#^?oqYt}TFCD*o5lhw*tu2 -v2gydGJTp;kYN^HFA;Jgr%^Mqm-$;KOm^tt`x|)uJehC0^^O#PNw%E!aR2%#Ar(YGDnGCsT$a-qu#cM -UQ5bVd!ap`Y5H4S^BxZb*QVgO-LX?lGV8Vl4r1=Vjj*o5zsa-^uaZ2w&sw%2bQl`f)XtTd2bdbS- -Ni`v0_Hy$(n}zYjDP|HS;FJ^#9x*e?A`1$+*E?rGwL!JuZ^ya9UiSW!zEV}bHQuS-8c@xk0wX;TQbZKvHFLGsbZ)|pDY-wUIV_|M&X=Gt^ -WpgfYdF@>5Z`(!^|E|AcOCvA^u)HQkaR=S0Xqq|~<9dlNajqx~gQ3Nh#hM~nE@ekG^1t8A?3)k8N#s8 -6&L3K7AG0&N^Z3o|D*3vunwAySKCcP+l#9q{GUX^JPvC~bB4X;c19rEA^@SX0ybxBTiZo|nKcFd6f=3r-B1m7k?zb<{Lf6S05tHqFvDJuVk8A -Qvq!+aV%rHBOWGHWmCk7~bR;8>Brrr^d-1ew`L$3(CniE6xi&`v?3oG`QhE -$H;%%Y!+?R7(v4cgTEX)(xOOURDzQep5-l_amjqt`i>Utgj~4PTFlM5iCGbFt9#O05E -kI!_q9tkfyS7qG{AS{Beq1EtyG{%zVx+vMiNW%%M|x&t5i`6vWPHg|k>IH$3(aF_KF#dGOxfpO8z*t*?bNwEfglJ@f&FGvw)vVfZOzq^d81<(N8|lP -lSPpV!378-H5~s&H$5^#dfRThyp5O5Q)wMbmJ3q%a^XlDjj%MG8IlceH%-aRkcrdyyehfc(sg>plOY? -tlCt5anQ|O0U)Kfg^?=3EuJ_v|L&zK%Tx*EL#tpDQw}GroHwRjNGXc1>;-Qy-0|1{Bx&!2{f$QSEp(s -hbX$naf#zNbm^9j9~_K-FpLn1bHUF1BQn^nDLJ9<&Axger;sBQD8CN;0WdiZCLq-wL{a#3NQv#@S)*F --0rg3EK))NOWLB?Hnt1B15Gy53liu#5Q}kvHqIs4mihNq)OOX9ZD^J7e*Cf8^9N2K4_`lY?6XsubAfR -z189J6;VljmiYDZ>I@pJ-i1q*l7Z2Bfbr2j(2mYxkMHJZ`9eEH|xALLMH@G9Q_&Y|3mA%RL^8sKQn-T -QV#qy>p{X#(bF9WG_J2MfeBG>GCLO+D$V;0DK*w7g_*nQ08NczW} -k}a0aWVC42LxPw&l}Sene~3UE-vYcMlt7A!Rw8#vl<(OLxFrVZo=6f`b4V)InuP?cKI7+zlIO;yVL1l -VCE=b2z_1yk6Pw~H09h3&ZpuQ5Y-j0|Jij--b9WyGhbF?R)@i^&gLSHK@2X0(;h5E(3?;c1Y*N)K20}2@SjCEModW>7oW@%7Cp$uuR8El?eFJ --Znu2dulQ8H(Cq(nadKtlsW#oETjmPW=cl-S=H07Z%Xh*1Ye8vKmi3he1-6hzhMvyvL7Ab4>KJ~H&X(s|P`@9K9C@O#;KIN(c -sO-W78C&v76*{z@U@qciBkjpDYDu>rx!!3tb9218A%8{cEQ;#B`4@oa#71(Ms!>TFKzNo6g68EbJG*lDSi#U} -7hn;Sz(2-Itso3hu;h6I`=;9&m*&4=n$baTI~)L|!ChaPnL-X8UEcC0$#@Oi!IYc}6efgDBJlu9SSyPI+0{jDrUCr*DTAvCh@MFp3H6BB$LmRH1a#xFLPVend@qo9eI%IW1^KM+CW5{! -MMt`CKZVg4X5Z-)LImxP6PC1Rl+8rrFKS+6V2k#Xx}zJvbh{ovvh%DTRVA)vSc@$RIDv~zUSwj92QW3EulES!W5b -_4*Z-;L_o@0jqPXw5gYQI{-?@r=z^kj?Mvcu)^{^sal^uOr)Vp!&)YM5uI|rZ+`0koJa=W}IEgq|Vt2 -EDC$(asavfgzop3$vdACrvL#La*MeHFug+6eP*6}Ar$fXdngn7vg6aSjNxOu3bmr) -`Z*;A#Z7(Lq#4H|J?!13BbB(y>qL>5vp`)mTE>FzVfR(uDCem2b{6^2I|Easka?^#`6Dg~T$A75Wr0g -|Zg{<)>jDW>-D>8qe0N~DKqx8i3QRrFWy^Mt)P#55C45-9-(_v6_&&18Z&PCLG3%AZ`=lLwV#)i5*(0 -eZd~B(G*F_x*O~{%Czd{WOG!;_g -tp#x!V5kl3KAZPFyf;(6`4I9sMN5xW1lLbHmQ_(yH}v#A-GWXNm4`8y1|3?;Hf8?vhBN8&)#W7`vbU+ -H@*&CwAKtjHYqTO^nHfTFi}Rk;p73P<_9a?7fE+Wd2kD7{!c+{r}RoQ`_r^D@bTxnvl%**xu*%TL%F!QqIy@ -vEA71D@+wT@UL-$_1!zed1}uo!!`^L9aQp7;_T9N#vXiueD2RWma*0b6TT{8jCqilDYJQ9V1sokILFc -#f|NDd1w2avXzfY~{h_)ZdO)I$v`CA}tsaqOC(>H^#wR-7ZrDYv5_k^#Jl{-P=9q4**&vP_fwZF~_hn -e)xbI&NAR@=kl7PR3Vk6LJ%9@QyYIn!odH7(CVUw1n;F_>W(n01#FgBWoq?J0H9bp7mQj}Oggf!<@Yd -(B>KS;}&d3{X9hMw8LI(w2gI~!K6Nepxi0QX40s)A!ba^N -?PgL+tPY)dv%F7*u_51Dx|4_1sJS8B;tX0~uzf>nusH@_ev?<}N`}cFf1Hm2$+D|$=jYuu>K71e!#~w -lvAf%5ihD<(^4ip?P7l>B^iRL#FTd}6( -;*=Y9ok*4I;6|4x8Cpmoog}rcO}m7r%x=7&K@W=}t)}0G)IC0*H1&ex#u!)KRYk(5V9B79Tr -qWFwVnp873g9`l&=aD_iFlEY&~jegp*$&2=Lm{wgNl#4x{Oq*gYtIRFTjZK`q*_4Zi*Kw( -c!278$xjS1t@Dq@Ge33{Tg!fyo{SQz}0|XQR000O8`*~JVWnpN=CK~_%wrBtVE& -u=kaA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHFLGsbZ)|pDY-wUIW?^G=Z*qAqaCz-LYmei`k>BT6^l{-ED -%NoA00-O&(B+)Beh2KP^3aqGpo$af4{2gSM`I^>?JthfD>YSNH(jhtLt6WOtsy$ --5{22GssUvUEQ2j>dt!KEQhwOdvl|1Z*QyS_H5m?TQMAV@VC&9@-qRy%b!2IUc7qy`qg*u-v41rx4x~ -GgZk@D*Zxg5AEmy0H^{EMsU^D_+D=`4U-f#_AIlw1qb}Z;Te-U2@61a-?1rjs%3580lz-jJW+}~;hQP -*O9(MB2rJiG1;vADRwVGYED;D>%S+!lUE0=dAF0|+XWYx*7Y=+*f^j&jb*464q_vjdeekYgJx{{rJ|D -(AB7#@3bKUB4S>3!R6Vb;IN71Ce|$Tw|ME$P;xfm<=8PWEkmFJV6okRS2R9!d5NZuETx)aT2ufB`xnX -R$1o8)@Epg%_`zp*u{)Pa7#~x{-QKep*V}k>2NuZ(%pP>V{^1-wtoueY4WL-t)V`-78!R- -#k4Y#V;b#hmB(lLqU>ksNla+M$%1=4=#_MjsicUNlb7Vvz5uPqSuMcJ$t0Q9e8kZ`nq(`Oyc2@EMMyc -@Z^gI7ot7YdOQKrTQl$&p>{Ec+KXuqRHQ!zn&=?R_<}yCjvc^6d`DcT?5Xa99lGtzAFvt>v+C0GqPkN -c+-Tx*8vCl>(ooODdq?O<=BJU)I27=GAWBGyrL_NB&*7=UNeXqG9%1uF!QCNaouUFN8b|Sp}2`2A@nEsa{Qv_bH&>=(G+#nRo@tW&H}OD*DMkoK} -*`YG@aC<}YzU9)aAgp~2fHSW8*T2@2E);_Y)V{1C1C!pAXANzHMl=7Z}fveWOVV=<|2_p`SKeWrXK2t -JQmwk`hyar^4SLME!xfwB0F!0BgIU8Sq9gZ3WM_P)Vot>dy0UCC09E<|^`+K^VFUYzr7O;$0FhE=IJb -RT*&d3nKO>{A?aO;67uK}Dv9QbPqQut%ra!U8Pb0hRJ_)g8p`M6>)Tq|l=c84J}BV2MRP{V*o9f_s{q -qO29X^27YO^I6bpr!{~AQm9G)v#FPmQcN{*He3?jL^(CKkl(s;@vEzzumBHc_$Z4MbD0CKlc`ylm$5j -GwX9*IbYz~4Ta?|hy;B#z~CG39@!e&ZYg0}jX?Ogx^3FdqKCvwKG&PoFLuOZ;i%mK93WCr0BPMaglWg -GNM992aeWP(^S*7gCYXqef2Yftw*iH*{&T$p{!>GCcx?U*UT3s}LF_6RrxxS%4GbR9TzRm2Cm5gJK6owt;4FCkW?$8-#k#EOJq(&DxAZDorIOFy8G&H34Jw2rpzqM%EA(m2aB6Kw7V --9XU@y3R9S%Kc|aZ+y-{_sg*LmsjSGrAd#1Qw(RpluPC0@$_-rZ9eZpVuuyYn%Eoe^b`I^wD$F2De(y -o`<@o*MC6B0v`5ou=+iajT|A6&OzcsLE^ZhK<}k&Ck(e}PC}Dwh7U(?A0;g4|mu7*aIV%$goLe`E@8V7#V5 -xbv$`Sg+z;hHHjNjnib4KHsX1KBwTsz*Tm|c7)52QZF0S8zUaxrdBI8LGfFqe_i(fsco1RLVOkyS&v8 -}pd(*({E}*~(MfMw%HH)TjLazPcp3+OpozHE@nF3YB7*jZf_>W`+{3)YkolQSs$DnD~jSUS$>ubhc@t|u*&llqIhi`u -XHw2R*qEwqN?AAFcnhN*}ojFkrxH$tjqm)vD+YFj@>=a1huR&1FTl9RH~ey76Gtt+vXe|wSahs;)OA5c?(fjkzy`1a~|Gi2Me=8A6c_9a&MYbUSH&MMB ->g_ZyeAS&WWNE`Hy)L8vbgJaU(>Gc#n?YVn03#S$U5C8Xuy$cz(~btKn}F-PKAgKbGK4CU+;I>_vk1_ -*2<~;-Ty?v6W|_?w4h^LLT{|FW{d{IYOX6VvO*YVrRQMGWiPUG=4wf5ICcJlo5`2n@Rpm#x$SHn<*f6 -HRO5Zwk-COzW8K#sUJEX;bFAimfp-v$pOw^9pWmfXcwnbne~Dq^*I0j-;U -)ll=!#u2zK6fUz~V+orggwA&_+lYF1KX-6F9Wq2O}>ZaO9#&2i%B9R*40-|O_qHR0k$*k@O&yVmzWpCRu~6THBW4Dp*Mii>kr~zz -WP@e4x&;~v4~$$%{k~o1-U1DMhsEuMlX4?1CtVnKQ1$|>3SuVE`<-(;bfdv8oDaye^*{F#$2wfh*GB^@GfQA6TwVjH-#g{V>53mTU3}3)fD!H;7s;n5?d`}frKr+ -V@Taz)Zp0wv;(SwvOkF1uH -0=R8X^eG-&l_xASUQ@f4zlq2vE0ovX)?s`uSzImy_qOGtRM`&PSLZ-ktk6#mPX}bO4EX%F^XbwqtBf=1uyX`F1B8W -7PQ~jj`MZW5&yHek)YcWsXX)BDPy%b;%Zu6o7=zQK}wg-H-TdjZxdLX2D8RBa*aYck$2={Tm|;++cq>CE16j|Nw;<{e57n>{ZCBk -^5Mq)^#9$A_m~8Z@G@2rZTMXICKpM4sd9n_VPbO)Na5R$ei&*BDHrm=?v(&?p_~CeiZmyD&W^@7&+4M -MB;&?OhjxqGE-OBtTRb|N#yG}~#5~X4C?w^rA91|bk+UOGpsv#$7LYWP9RvlY`E17)X7pd##%k9xzlpym`y6@Irp2MwHm>h-{YOtZJRZ8J9V6=w-psS1?R$dla -PzT!WDrrV{+=n`Kz?16p_-IQI{D5-=aS?Y?bhj6~!&O0$V~Q!f-=$-5owxc7ZDa_6)5N>lq$gsPP`j~h`X;@RvE{_lIR)*jRtMc{omT{_$N=6^J_q@13ihyCZd{(;cYjr^s%r7p-2) -zEuq=ESY+=fCxVD&4&0RscNIJe#22bGwxNl&gm<>d0(qV5_qe5_s&paPG%J^VSttScNNb<0zMvCiOzZEZ23Nc#kPtAZkv7B6)V9S>QR(ik>z(gK)=TKa%{mK&hw5 -^Fe95NniX`JXI8B;bYl{LlRiR?;adBNt9Se)b -Dk*B|h!~CM)3uRe}M@4#cw0!x<)FGKBgo(3OpOF?)v@aMf!u6B?p8|Drz!lZxdIRdhbI`%bX-^en+rr -t~}VNW(rB)1(R_eA(2In0h01*h_noe4CyVgxZPRt2$ruIRU7J;x9LfK6yFVAL`jU|It`jX>0Gs7oaOj -1&KJEOb41?bGnxV3-VEyI^6`e>!CatJ!>i>rTEj2-+QeQx|NcSQr6g%7{Fsi>k*{txof^&URLF -{&FK^1_9qjD#PUO6KR6*O8hZ|*u0Cm_U)vxI_lWfWX2uxH!)FuM888zgW0kG6*b#x&bJg^FD4^^?1DC -)$Gy$hNo*;Zv4`#L3uV@E6VW8CUkjvLdo26kCP6>D0Jy;RZrqH9|(!U{7Sf(D$3yPZxRJs8POBA6kdB -F4{AUR_Vc;}iZ9^tw!KyNhi0Tg-sS+~F9gawFF)8ItfZUxQLp9ZTe-l}qw=$&=*4Q^Id(VcWLY;B)NA -c%WDrZW=(klBbv|I)s+%my!TS3|^wlsCHtcR>-@CR4kjh6p~daq!F^{<cw^JxRYr-~!{>QQ9)%*S#(;a?%siE%O_E$En$k%oMUoK)F>< -BOZ)%%D&ZV`Y+HZAK4Nfng*aLs`l>v)eQ6+qQ^O~2P(HSytlT5FOdBbVMQwlWDi!PbWj++yy)j6!_6n -=CNFENI-#GFD5L!Pm0G;2@|YB^h-hhRrUj<&jpI?+l-2 -yBiBds)ZRWRClQpBbVYJHMQC-92YNIK4uJFv_Lz_BJ$|80~L&!@(Z>zECH>qW~Uu)sa7WZqmm<-!ovN -Z!_?{OrJ()8-dwgc8q<2iRVKoc`YQ=KVp -}!fEmlia48XM0YA-QLK}4}>+h)ugrG#Kpp3S_`47%`>U_ty*pHm#Gnol~7G -$y&Y8P6ooy>Iz&iyepqHgqnA76Na6x;t%te69~qrzQ^febY&uoyQ%M7}L@33l{#xo3cdsEf|zdcc?j#g7I;}fB -)%Rpfi-T0V+s9^D-!pIZUhXxCri4F{bpIUL}8SWQufIlVhE(+FCqVfhFN1;_vV_hA8S0l;u^fiN%6tV -aepv|{1ugX6pu`I&-dS1EU1mmu2GUeH4i;d?@%rI2IGW{d{ -B6J+XBdC{iX!{nqfDy_&uPft`YeM()kZ<$Ft^ujs64{&jj|SRa9a$C))ks5p(PlTKR;KHnhg|E%eF*M -F+aHM|#Xf97(LRpRiccH~}-=Zf>6qw^oCytkjs0iNm~g)*rBmbl7lq1!#|iV^=;b^k)$z1s>sG@@AXqB0i -Ft|p#{`CNP*bQ9T=J9F~x)X1x^u6eNJDkziBU?$xmEgl_A^$9MD)&8k_awo -@1=E9_y>4W~@(xYDBlL;UnEoK1kDCgLeT5_u#2idpW;_Om-gRzIO_LU>#W`1{E!h0ea^;xiHrM7<55xM#(-jp@=;^(N2@oOAW-i8#a2KJ;D``j@;@;#^y*j7hwa!ee#3|Mx(6Ai3yFgM`RBc%moHC -`MoO3-P7@G!Xt?Hp3NmCf?!|Jj4q7URZQzS8 -H&tSwPD0dk;`gb!>lf|!X)#T<~%GhUEx6Vc?EN0VC&t+b+XZSQ^pHs(s`|kbaO#IyLsWSz=IEatYmV3 -<#Mrvy;Ra3ucth~-uttCcBH#wno*)fpaGXr4sQYc;Y<9YTpwIK=bp9?p%E=gCT?ujMb;3J>(W8aE -g%$G-oL4DXGfW+51)%G!ZNB(49IvirhzXkp*E3_nnf~*Gb(Bjb&Vpx$-oFQv?}LXPx?NBJXAlU5c5bf -i~WuW;&keiTezY=VN;nb{&i*g$$%?F8=A2CrYip@{+$P2UN-s^)?;6|5Bo;(B)=RJM!TamW~SHG#ypG-!lYPrvIQ{6`PL!MbT)$>w89!98gtY7d4jQS -s{UN!tshN+|u_I9nE7hzS`IUPo$!**uh7$o4(me$IX3qb##J|TeT|LL2*y=~8<$$kT3@*82I;&RaQ-0 --;0&PH(@eM>-*&Ah>n*=aA>@N~)#rYx}b4d#~n`LTb;WI+kzNF6G*t;c*IhK0W)-_cey?St74RPVSSU -h5VkmfDmQ!IGQu4Xgvd)QBLJ(@EBDBkLVlUzgC4_(`Kr!-5Pmo)I*4Urer&CWSxZZ^%eJI7N9zCmMq^ -PsxUJ2-L9I#n&+7mm(u|lEGiv3IV_IRpF;U>O%xdn}Pip)z^YH;It)(XC@X%w>_w3;lIDc#St0PViHfq-wl+C2z83y -3OuVHOps=F53fs;8FVa_jjrN^d?C^C`=iub8j~tcT4Ab$bz@6aWAK2mt$eR#Pd}T -EDyo002oA001`t003}la4%nJZggdGZeeUMa%FRGY;|;LZ*DJgWpi(Ac4cg7VlQTIb#7!|V_|M&X=Gt^ -WpgfYdF5D5Z`(!?z4KQrltZLIArR!!3bE0|O%J_suRpCCAaJND*B -mt%kGn@#f8pCX>l;PAcgnS>d!$=|`z{A~Tda5^0%>t+djK3?F5VGb;*Rmw6mTXDlPjn_kL)=|zRK`1) -*1X7@~F+BzuK6&XCG%UGN{AE?eXAxuAPz(#-kKm3CA*nJPTEs-?>Lf{o5=#by}25mwhGqj -@laj4XB#iE?S3k*k{T5gT$TtVzR#aB@wC3T+5Z))|z;AtPWG-Zp8O5r^THsEUX!cWjp6XE-)H0b^K&x -J!2;*LUR{Hi$bCx`dHV*V%d)E4NM^evQ(V)v9&`nUW|E2AA523F}Zg&4W^HR6Y@RYbraPERRpb{#kZN -7m?NQiVcgS$z&2ma#LyJL{XQS(^?^(9GpxmMHvUd#_1{(jnuF7S55FU2)SLw4mQ7C{_){$a`xlfv!Bl -2|1cM?%iUa@!R2(N5an~n$#$tq5^ROZ^mRhr$VH*9BXd~;9oCbjf?IBT -AZS2${aKWjr6MdxKs_ucpNve4orTzECq(%X4;wd@VN>YiLKmk3lbc5x92yxNF#vbdo=8z_iKSqe`)5$ -^z+Om8ZHdByd!TZj(;3tU_UbiItf1weoSPP45m?^kU(*jGy=loQ_QYVQ)6O0A88Us>o4t5vI -;vpVoL{1-XoYl$=?0Z33}q-SKoxMtrEb>xCB!?aNK9T?hjKfe%!QHw--|hlRlEwtciJl5RrJ;vZ5HC` ->o;dVqtc!Vz4{Yoa-{{wS2i=Ua-5lwW0x1-Uo6Bgw9#9X+#2#LrD0!M0A(UKy7Itmg8DN4rVV2J*_=g -Dlv5!R2~7zThnq^^#x6PpUmc=qITO!Mdx1XfAu_15_BT%$*Vpfu2Rku)eGN`z$xHS{wzBwKTU4;GX|C -~56VwJMRiM#)hv-^|WKbUW;J03%&%R`P8E{NP93bhnNdqOIEhV(c!GFvMjkW&0DLb2Z(q3@2j;dyfs9 -AE;S-1)IZrgHVGT~c7Xyt6(U$DBLP`-@BNZgq8I -(wt)4L%p}-40LVMt4S65oeeREWFd`pp@|I#gGYht_3VL_+kHkAXyR5@)zHNbWA=dx89_YYVC?lVu>ZT -)*NFe~RTlxmLB{=$YoNn5aM^%%uOeFR9a8KNwW(pft~Mp*$Ke*lq0m^525#e-pK}y#(1QIi-1zASMx* -PSwJ$B7Tq`t};-Jnr4V)^TbXlGIL=n?H=4c+dd9rAR5)73P#Mt0w*f)n^i=&;79t)IoN9GG|Lr=tc@yVd^7(x!d&Rb$}f$ieq)d$oePET4R#$LNF -W?D#Px&BPLKq(mAx}e(MGbbd?#6*Uta -WCLM@3D$GTbpbX>HYqk*m@~WP1#zgb?aOV^cQwyKoo$<1ISGysPHwmcvCW2kFwHWuwn+h4%d4x{9?wA -r1GVd>OdF~)bG2Whd6(n~GjQvMou}dI8wO6V$JWx%?8C@}3g06&US+Uw-}V*nKlmPlrXpREOuHmnQpL -Rcc0(>{TFPj7d#t}K`^N`8>Vb8ymuosiGLTE-br6tgv8`$j^(&;4EDI$m!#Abw7 -5!-&(&ckc!JX>N37a& -BR4FLGsbZ)|mRX>V>Xa%FRGY<6XAX<{#Ma&LBNWMy(LaCxO!+j8r+6@AxNV0a!Pr4pN_(-%#Z);Vz+P -nr|gai-09JQO5C5^9QI0m?ec*LQ6IBmhcslGZPlNMPSDYj4SYqjbyGs;b1gm8v!)=^pKQyBlq^+Ozi5 -$a>5C^T(g=Z;SWe+`j+z?!(`v>?^r$r|hm3ny)J1Keb9v*>}95dsFs<_|HMqYrOoi28WTO+Q=p^UPWC -hooiuK^(rd4${Vd-Whu1j_}#2btNFUU_b>eQe8r9E&b;S!DNEjh#e1lL^Rk9i4`3{{*o(ULm)70Ep8U -h@KYqM>xc#BH|LO6Y?>`iGU)_Fqy!-m@_Q7A-fVa}8?)=y8>u4l5sVU|ohQHsW+PxMVp~*DAc-MancO -%{I>W$KHm$fYs3Us4aY;onzQ}lQmO!@3+6`kF`BzrH;+;Qdhl;PO(NK{75e-)k4=pFl@$T1ML-3g{eW -yLc)*|3#pk3zT?of)V0n|IHYfipb-ncm6vnGW)?b;XSlCLg`SvB!uOlZJ6t;@r9=3s?5Mvzyv@c{*n} -GC4xsFs&+LiR-OmCEmhE_V~^H9sct+z9&95pzql~AiwNcUbCapFYGAW9TJh4x1u^R?k&W!NQKPml=Ui -Rq_2Xf8wkTq%&k#IAD?DD6eNQzhBGW)0V^TpucqC|H7RSs -<3zP-1BXTuM_Ic+YI4D2vr;NdkgI91EVD3!LRvv_&ZzvR5*-^6A(Y(l5IKEWFji#L8=>(`kPfi&@Gsrzj@z7cLV90FWij%R_=#Rn}E?2c&^Gg0KtTjkuR`W -g8jhL<6)=G;cYUb>pJK@vXF^JeMm$O-|W;kz_%aqii*=k;jf`o0w_B6L4M)zmozCXmeLjw{xTu^)qgD -4_97|~iAr26pn3*QQ-k1J4}sJ#`;p=m1ONLHu%0WV7!5^4K;FGSwC&3OW;&x%6UvMq{npd+fywEyN4X -5Mws@Fwve;fW`FjXU{sID66PE2XM%4YD#hDpt5OdXb9kK}s30g0^6nYRvwiq -3(9p)X<^zftJKk#Oz%@P{{woeaS(40|M<&V5XeKf+-h}bP~M`_y -*&^+)MXQqe}BN8ny>{GGD=8?z%6t{EUP1IO1+vXjTiH9^HUF_8Gt^o`-W>XB-W1@vdlr4Y|2Ew%~_*t -1wh^8bx)8H{;T9#w(Q4;zvmI7yA44eUQNB09n08?#035azycGQi=32cQCm#JfUp6D6MD^Ju~;sd|Je1 -LAv<&h0#OzzGsKrxALX9DX0`-m9zff^o9G;Nbh0hFKm3 -*vVJ|ek*+vVIB}512V`zaD5HFf#EiTv+lgwegBlNXge$m_X&(}=qMCDbp;}}|~?^Z4`Z0w=r*6R}+1J -E@Fb!m0jZuv@9vOP`mIJ^4Kl~ONXRqRIlgBc`bq+U>2hMvuwtnd -Pd=3X8=0!6d8u~_SboLCV5Tb^hXMUA&_fK%<}%RJw%VJ%vKHrJ$;y5r$uKI!u`?bkhgw_TSV9$xl^mw -HuV0O%OxB`qK#GaH2)m!ZNaj|BC||YV~av(gCNzRrjWu#LLKAiK!2hI%1M;yyKUi$jPG=IN6~lIOYJ) -LU{6teq-l?U>#f#GzsU-8yN;Y3BYkFCpS-e;fXpPi8-lNQtd%GE -ecE~cS?8@2&BaY!>+j~z(0LuaDXg#C{FA(A63BCe8)k6+L}5|W@=ay$6`-2RvRN{KuU1gAsTM1SwPfz -8bBPnf=HJI1RT<%BGqE;*=3V*FjdkT=k9UEPUjQOu%}DR#t#>c~&1K0x)YE6TQ0y((FqLaCBVqPV77AltcXJjMNQ~_VEn?Y@JBdz2dmWo`t)Mb(Q_@WY#UCWLaTY82NPvuX1;V%`MJs4W$>GS2{O -BsZG0>7#@&(v}V+h@e8SQ>H>i7iVp0iZTN>H5I5QP0RxAe%oSTN_Iwo5Cq-qn)K7o@SO6&4~6<1#b7w -pkpB9_HB;vH+P}-GgSkNpKr24kh-D-AD9;GeiRn-$&$}x%^U%^|KtDO>ILOx7Z{ctNa2}R`9bQ#;pWp -9NKo37KFJ5>$^%LO)+?r6q?~MFo{c-#olQvpg0>O{4_Zb3_d-(=vwjlbJu&%pAUqazEQ8!cY~^XPT4i ->0l{FF{&_UE78Y#v6TF{Z2LquUk;0-PbQrmbqmOXQa6d5Y+t&DJr;^@yRz5L2A3yi+F1PcK_-Y)^4F>vqVSajM`J>*wTuY<{ -pF()zX}OdFYC$TO9V)#TG=IvCp^x -L#fkz}v;@E7ij&(FH5H3-_LNQHz=*F|{??@bM~qlRc9zH? -EOXT3_B4)yJ6@SzTiCs^`v}9-wS7jk#Ys0BaSNBfVIN{MtzUbD*TFcNO!f)sO1M2 -WEat5njFtX4vV-;le*bBc$2ai5jFM}%@7YvyjdHl4b-{IvHp1OD`Dl@xNT?;d*l2C+e@=nj_C&6dHBD -}HHxtvZPHVU-q1EvW6`_+&+AW3_gcbnyXz$X&9R7+7_DswH(P1e2lUvMK#1b~?5@k0PF<~;H%+m25z&E46((jyd?S49qtv^ -JlS9{L?vb+xMB6SZbHU!O3^Nho=W~5WZ;CLQ;+xD4XT|ngKjF?}K*k6R-EVoO9KQH00 -0080Q-4XQ=5KpUlRiW0Nx1z051Rl0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RWo&6;FLGsYZ -*p{HaxQRr}0a*033dzi&Bu>Fzkb*9cbEdwr4QRa40r}AGACQGCDGQ~NeY=A4U|A7eHjCSen^`3cca_0`Z9ND@PJr0BZYP -%tW(?RMHwj#j$>9_(p3m#kqHpCff8EXjB;E9ghC%YGCco_wLRVmSaJq)Gp`W<|SpNDI0DY*;+GwST2g -ggJA)1&sNR?58hBl;AF%NNDa}Dgx_zb1W^wPV!{`@t96YB*XSjfj$2wIMtYdv-1PsZ#eZmRmucxm2xo -_7x24d4o#uO5vSxX_&tC#tR+RWo&6;FLGsZb!l>CZDnqBb1rasUuq-2Vc?Yt~1Fkl7@%Ovl)s2Va>Fp!F{vaX~iANMOCLC=cQl8t8E34N$!Njdup`3G7;Ofo7e!7{s)3S63b -P)ul$g7QSdt@+}D(vKNhd3+V1Mc@6*X>++s|YJaLT`qx@kHA -ua2SHOkOkhRh2uQ5!g{zgCG4iNx3(Uq5Dm=trO*U%e6@CR3zI`HSfz{FR7%EX~*0}oGbPj83O++{BQq -#$J_A4!!V)Pb;;2fKhWS+&T^#Am9XQfXV)+u -xLq7!nIm2(w!piUIMv;qm7v!h2pMxVAI&5&G8`w -%dY5N0|(&2ket3~#&8xJxkdNN^ny!Eyccg7)ERhD$Fpr#UwD<}RuAdu98H!=AwCP|XS7xA@B^9GM`i5(<=NHjpaL;7lg}0FATAg4*H;nwkpwmZ0Y0_sxI7i9x2)DD@kE5))!ne -eIaY6zdB`M1@V=W5QL}1%d{dW(W=-TwGxb@71S6E~Q0I=^DYA81cY((+ee(Qcdp1$D0KVCC0OnhDk9- -3XIJ7B;k3=z%0lBaPF`P4zgY=LnHwnKn(<-+HwiVK&5MEugEVdE7uNt1)L?#8}czdkGVS#vZ?q15|rU -7lx$0V>IvYn!e%tvuZloz<&buMPvKE`@ERRkqw4k?F(;g@agh9+Neh2-N-*W)OL^ -wUj`oT9V7Q6mzmk1D+jk1{2$2OkJ6FgKZ|w$7G1#1toBUO)oW_QWei=R5BHIOkY`Qs7NasgMX70<0c2 -QjPh`q%53wU*Bj_Dv+}tvHDOsAI>3Fg|>E4O#i^TV($0^A0zd?cDi;6njbIKLWz5!560|XQR000O8`* -~JV8W&e%>I(n>Y$X5yF8}}laA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHFLGsbZ)|pDY-wUIa%FRGY<6XGE -^v9ZT5XTpHWL2sU%@&!#Ky^L+K+%Kh-Ii~_pRsSPFS5k?H&Q(C3swk322ShTjKP?^`}c)N2RAQdF(dE+QLosh -H(@jY()WxFONWhUqgS-8c}+FT$eDTbErn2(CqMQl`#78NZ5)tmv)EIRbzh|lVBa@-06l!486mE{7@F? -_wX#rMh=TjsY=-m!1}Qv`rXX>X+bQ@yO~D1j7P5&J;-fGG_pJInT -&kw9RS7x2u-1n$_5%#YzF*)%LW)&iPlnhnj6k39jV2EUky|qgb=eh9?&va-9l|ahMIjbsTzQ_anw~X~ -KIw5aHbz;rHXvBRw!5S0#kW`UzBzvwFo!_>-mL8kX1qagUvR*9ytXnswlB)>dG#3q#zxtiI4Mtq!W}P -I@oe>8P!c6%KM@rrmwFleA{uai=w6BQW);MC5^OK##2m3}te~9YQDlzO=_r1<8Bwb7W-uN!z6i*(NGC+2^JinQ;6W)b(CHQKU*yZt(E}+x_nrKsc -?dQE4T<(iTznYtpSFF5LU&YS01z`krO(sxa8jn@F#97lKwpl7HRCj6lfLb7k8vU2N`yLbA40ZzJB-X+ -rPcOEnmHU`QgnRX+@}mA)rN?wTg7}xE^F4Jm1+JU;tS^=SSM%2{V774aDJM85FruKkC0OES6Jek-;a}y96VYPxjy~p-&Gx?b? -^hU1sqHpT4`Y^Q?&|Vx$4d@tca9VbzH3ePFz||<;SRK?yAAqJq#~)Ykz3da(#TLw6TvKE+K;&dIzU#8$l{limakS){-tWLq%ur~!orEAi7S3?WguxV4|A% -cW(Ibo+-X2U~CVS90TmB#H-m-z2`t7&pI$ux(moq$hzMfqZ`3Fwa6>aRf0ButXA9Lr%!H+$O=U7Ld=N -kW+|4D`9Hh_RUa=jl}%_9*q9=6Rd%X9TZZS~B_2bGFQaJ6YdHhn@^1P5Wr*vXxrXT8D52#j+Y_Mc_4H -Ju0W6B%C_ssvzn6wDI>^uNL9wBWm}kU?#c2GNw?2ZVcsooIW2BmV_R0{|3Q8AO}4p=JPhwW}ZBU0q+p -357~vVL(AfKD4|NHA@@HrQ+Hoxf^rhBj2Cn^Qd;PjWjIhsLfa*Knk#@8o)4hYM-Ck>*x-Li- -8ntVZAd?UXG@)sW4U)-0kmxNJGzMw6?F8|TqT5F+B8-_&Sj7z>)JAOTvvBU&+q&mH8RIC{NdKte?%i= -7>q=%!z1^$!nw%VEjx!pqPfB-w2{{;}h$c{g-_J>5oorjX+oJT651lTRW&C3|TuA0ngU7Op$Ix5~UW| -mRusZ9Ov=ESAmr&|^EV91#bl5X-~;=GJHYZFUMe5$X2Th`!!Y`Rv0OxBqy$m`y08wCu)s1G=oKJt&)kZLF=2C^gG`9C -J!?A1@U7}$mKlwV?2f-iA`CjUh>PjUH_Grrqvj6TGFVCvHK#4Kiw8>h9;o#E}Wg=A4ya0=H&!`el(s5|s6$)Cx0$ --4z*50PB6BPwGb7fDJ9!y`8n7L&8_ft`xseP<5jpgC<8r4wKlDV2MF=(bt7|CiJI5F}kzU@}kL7dbMy -Glx%%z(Kgx=zA^D6bJ57gxN9u+qfH=_D7@$O1U?$_s -{I(->?x1Msthx(f|z9tDU7xSL`O6vh>dzUB@CFF7-N#poj4$iuKk$Rj9R$OUnQu?g}vnq&jGD-l -&M7kog7~Q)NWNcTU+V2o9pL5?~konI49RA9JW@0lQU~;AU4RR__8y>`;V4h!4l84A#u6wS%DKdz5(B?VNu}L{#dx8lfpRftC#YlQkkaIY60ium!Hk*JaYe< -OSjM)gi2X&Oudvd>qgHc0+7$iekdI5HLthU|18L^Dz1D^`WA$EzOtD#DlhZZnYVu{siOE+LuXL-DgGL -tg$VwC7Q@2~@1aHO9X7er+Ug$1gxbn8ANAJGvX5gS9+tSOZma;RO}m={%0U= -lLpQ8rLsmuRfZ!~RB292Q5W7XeNHeJ0#>IvaS0tOkgCT^P6NY_A1_qV%a0#(mp<6|s~_Ti?_v%`_EhP -LS$F@S7*Kajj_kOxztn+hkh7KLIE*u_ti)KTGBVamz$-P_&0bLfKLTCjy^UhqzA;1#8AZjMW7H_lf0e -XApWVH1QY-O00;p4c~(;Z00002000000000V0001RX>c!JX>N37a&BR4FL -iWjY;!MPUukY>bYEXCaCrj&P)h>@6aWAK2mt$eR#QrNIX~6`008#`000{R003}la4%nJZggdGZeeUMb -#!TLb1z?PZ)YxWd2N!xPUA2ThVOogQTCElsU;3vD(y-O6oiC|1X`|X+Dubx9XqleP23;XaZ+@pcLtgVHEHsFp3+jz_0(d@LvoO51mp+k4n4QEU!;i^$ -EFQ6kO*dgp|DY2m)$sp9~1e(MQbh;TNWpu~dg+~d7x9FZ?CYEIx33t6VZel$c0(7UI_AvhXtxhFrSjX -6pvl2k!JIcVnT`ouLE$G4ZgQJ%_bRUQc?$$Sd9tf?0$IbmSdt1NUJio5tCtO`K#-a&tF*<_f2{j&z6$ -4sXau(us35|EvbJj3s_gKq-#X?NVCG14w83Ihe;z>HbC&eU{Ta0-tjK$*b$8;=U` -3p8$VdV77pc{+=FTn{ZA31{DsPrXLkPANX)R|{Ly_mhMo94+AZm9aNW@h6qE7(6!O9KQH000080Q-4XQv-5B!j}R70D% -So03HAU0B~t=FJEbHbY*gGVQepTbZKmJFJW+SWNC79E^v9BR84Q&FbuuxR}k){#!?$w4+Dawz%D!Ww( -e8}24mB*iqt1iXcC4^LrbJ0e0^NBz(xw9`FL@npo%S!7p0Se5mBXUP -rr9sxiOA}%8Zvug&tQ>xp|C4}q+>tCLE(&*7|DvICkOIzP9|+aUSn5Lk*G+xE8SY-JQ$wec+aYIrUkz -ricO#IHG4H42$>`s1)5K7gI+kdgH*_nO|mJa3M!>Kxh%)LrcAzG%VCEtErGp@;pQ$pmkQ)Ji8{lR*MW -;L73_U&-0B-POz~7FYcV&RjVRNVy1J;h0B5ijVoTT<)4&QITu-N6T)__}_?3ROw$V8bGy2}!z%&)|3( -(~-1IbOfH*OEK6L~lp(Bgqw(w=fC(BqpF4t=erXFMd6N`{k=GSM9H;WZxHJQ6H?RY$#}%&Y5nH;sM@M -3PaAvH6WSgi7oiKla;%$Sg!=Q|r+SBAUCHH9S?^RpPh6?1!|EtB=RI@qo}&s~5iTRkf;h?zjT!C90%RiiQhRT -CnWmNYb-$4*wyE7)43keav#hH0xamKkI)N@$n$YQ05&MpcPB%F!R+gzV#@tU}V -Q0U#PRT-=Fc19E8B_r&lP9wbzH6)#=^MKEOS-3EUg=BwHL(@_AT#TWXr**b*Z=Vc8=7da^o`jo|}4Mg -bMA)o)Ns0|XQR000O8`*~JV4;ZV{dJ6VRSBVd -0kc8j@vd6eb-kEv=5d6N8V#XBNJcHQSLhyaozE;LRN-QFvpVfU>xZjXbW{d#10jd6B3;6i+?N4sUE2%U>Cp&{hbh6QCJ1yap?$Ce3e{ -*atEWN8ot(89eZ<#T{vec@9mbYnkKv^iAc3Kd~yOfv{V;;+G_Vju4^tj$`SHWxuc75RCU6Pi##R8}j9 -xRtOq~OZo{-1*vcWYvR4AS8JL7^eg7D^?AI#OjC!R4zt1E{TS&0;>UxUIW=nyY5s_AQ$el6l(+G5_`M --3@6V?~D^irr+J&zQ4V_3;v%3%dsG7aRX&1b0-p~EppR3wRH$qos^bSGIai7`L~mpckq{&-YkgI)T$E -&AQ5OgQcn++0(NBmfM%f?H{@fp4OM_$jR7=Jd^Vy3r6Ff>VquTOFC#K8x#C_q%vzl&L+aaP>&?f_YPH -$L(=#t+K^jyhRV_--{><$*+4I9@z4{FQh23a5J=<}Cu42KuG?v+U$f!K@F9Mr!=wKptS|OhYZ9=gN^8OhN0RC_{lNC>fLHcc -)=3$Eb8)|4>Et)qaUnbnLl?SCU7)c|;^}3Nj$o)iZzz}il~w2`sjU&hNDDe{SRYv7k{}D3q^>O*w5(v -3g&GZ7cft2a{&J$06VKF5lfev&NeZF2C<09?%X8lqxi=+o7idv8T$0hA(F~*RB3Eu=r$Xf+Y|{2`-!E -QhCR2!#`+@4`3JuOLlaTi0>&whx>fipdxV{*c*@0=3OjgT39v-)!CevxaP$JW(hXUvhACR)1yfy3}Gb2izJ|Pe0z -Ja(_GHj#afuR6|E*&#=deqi9IVCaqjHhmLB$rp<{vP67@Ys7K&AAuIf0UNQ@jp;Y0|XQR000O8`*~JV -wzP?{a|Qqa0TloMDF6TfaA|NaUukZ1WpZv|Y%g_mX>4;ZV{dJ6VRUI?X>4h9d0%v4XLBxad9_$=Z`(E -y{_bCKQ$9>W9VJfNv^C%k+b|3ThPBv=een#Hk!YKZBnl#B#|ZM@cSq`lw4Jwg%LhlM$cJ~&J$F1h2!d -aE$!n%Lurh@(EsDyoR4;Q86v9L@x9WKC_jIM?nybMxv->t)b?lWi1QPyGoQRXh(k&N{ -``VrG+e6K8DXtmGhCES&1r6HoVMa9ak*9W-DA6Yeeo=has75OGoXGD~m*Hz8&GbvfY4@Wodh6PKLDrb5r>jDv?(I}DXCSi5DPS60Dq5m-%BXSJow={whTo7OV;pXY3g2u=1F-8CpM9(xGE?Ok$v7sSLH)1eJF1!JD`GHk6%f(WLI$XiJ -K83iD?uR|yDo>qqS3-iQt#Rs9CSTu|RrgRCJ$ja|^E8Fmg?~W;jo)&KE*jHV4v|JXf+~uNdZl0Pd$2P -DTV?u7}NffWTr)Eh?n3-=_A8ReBh=u{u)@7Bm9%eI&c5Bqd`Q%mLve3VooVkX~+Gx9NB3;?M>Rg?k%_%eR+U4T77u -m&xU;;BM-P+)5#46;Ty>Q9BRdMX!YZ(I7`p!k9b;!1G6hbAFF-9+S#>CzW0x4$L#9@@DdoR2~us9Fx) -WCGlqRR7p(s_3*DFbF|#7xb8~MGYZYO7M0kZb>wMRt%GQKSS*k93iqfC(_+3?pT_&TTO8Zrk$M{)Df& -;Y<@~0?W;bDZF1I{$ko%`;-@DFxnr$<7WPI9DSV|28L~mDX!|$@h7MKPU=hVh1uES0Os>!LFoGinXhTYBP*YlZTwYZ~*r{=&#b5ML^et|x46`%rVI#*!@*u`Wht%&(a -HC)+xe{+X_V$49df`Fi-W{OlnVN}qY6S0LazHipgzbd+`J!zSyMam9eE8~VmxlXmqa!Kl+!yR!1urf+ -r+$fPV}Gv_5(tt{vG05DjkZo)pfyWaT#V`!?6+<`)`KrMq?f5*GqcHUj -kazqrVrlLz6&nzl?-?Yid~6EP=?Zpm&rDD@jm-UTPLu8#`6sitKJPTH&aR-&aivz2vZ -eL#ZusVQ5ujh?HlzawxXY6on^&90Hfg2vDde=xQ*$g>6Q_^d>K%y9lX2<#(j8jfXt5JGtTR_-ikb@(~ -l{IJ-X@UTWdi!HT?L~huAfsqj%ccymP$lsfV3xV9QDk8u-Z7}fX4sZtr0Qqgk=kUC#LBjJ*V3HRnXC|SN{4!ahTIovHiUEA?ZN9 -zudM(+Vx;|Z@D8{`%2cX98@%W%Hi7Wr;_#;8*`;cTq60fhDD}TO{<`%^_D -a|Lgc*I_YIbABV4c7iZ3oM(%{+z&v&YJ?w=0W7eS)4sSE(`TsTleuMXZp!4m02H{U~>p{D%rG6vQ+dc -XRP)h>@6aWAK2mt$eR#R*fJ+7Dl008m;0018V003}la4%nJZggdGZeeUMb#!TLb1!6JbY*mDZDlTSd0 -mdd4uUWcMDP0*lb#^aegKIa{S8uKwT;k{vP$@S%St@Br^)QwnKi~-Q^x8!Vh%G_7iEDY^q%`C#4`pbj -KWBm*pe}ZC`@z8qMO|%qJi(_YH(W@mToM5?!>!TZR~P`5aom^Me&C&psE_@7PpkfhEPTmaQOw>U08Li -T8T$^mrwr)Z8-`wyC#J*%PYqtwf)}G2T)4`1QY-O00;p4c~(=c!JX>N3 -7a&BR4FLiWjY;!MUWpHw3V_|e@Z*DGdd97F5ZW}icec!JbY#u5V@*3!)Pz5kzw>6N)LE<7Uf?z@IjwC -h|xh1)B1VjJc8FDX_EEO$KzepnIdS=cHo#%OW16xzK3c+uL2f1w&Zc%#&-WVnInmz%wSds(^w}&TTH6 -1$;OVfI&v9VYQFO7CZmXpDZY#8f7nf00k`=7mDwz6gme&4X|?(RnKXYkOl5us-Ah~NDE1AXoroWXCJj -zqOL_K(0I%TP!{gl?JUYt05x&|GCencx -TP(8d_U`?MyEiwF_Yd#NUqAj_(yyQHe}@I+$;})HfUHu&Ie0Id+Yy~lG5<;TIh|)fe+>zeLTRj|RD>$ -s#yoXU%^4T6|ITPiCwm2-dgy<=dJS(Qyl}5Qu5ECU)wqJx!X)_EqH|)6^N8f<&dLm&w_j#Kf+EDVvHN -*|yqD-MS5hHEFlU8$M16tU%t2~D%FKrfevSqF(#1aNBqHp5xSs+g#9t#Qauo~$V{d$N(OsTJ>%27oof -V&SsLY1sXG5m5F2Q&be@%l)RODVd*DgbeC!_A!Vo%3FsCo#kLlEE^tfWwCA3CcJL_rHr8%Z7aB$a}V& -^6^xGGJEJgqWB~2-jEE!OGacjX|!nx#(F~tkJ&>XOldTh)Rc+Fey?3=hG7d#R*zH3NC^8e}Xp)-7B^8 -ly=#fDPtIulLmPOcX1+_)px-Pn^PqsYcE`r%!G~t9*W;W-|~?Llb84Xj=}8Ev=PV3UxlytZ&( -iU3uJ*=fxIDw2AFr>D(ph9LEs=?8}LDB>(r3eU@go(FuhuHcC`s)Ss|2-mfWWYLc^eD-^!9Sgw>e+5(J#F@Jup -ikZ(z(IX5>Y2m7VvRnT2<3I@wYPEP86IY@K9G+yg>9tNUo(mcA-Zvhei8Ws6b0EcO&PI!z8UekxH{_a -a5|&DH-n98qE*hfwiG2aVBXO^P=fM@o&wJ#*x(H82KQiBSrFt*3yiT!P(kEh!fuC6HIt4?$AoSHa<`Nw-ZQyPEWwmf>>Iw@JDuV=(+&nw&g`rto^v-E?~0DD -38XNq6*0TEOQa^GT!~P4)cw{9Ebp{Cqg%)7hwUw5K~qv!g@aR`7g?dl5B3U13afi5sz#ya6iexHT5}k -s`G!r`hFybjAA%@{f|J3(@>6nhem;>eIYCUEA=xk=UP)AzchUVgE@2H_#X=J+G@oh$UWoc3Du2*Ui2 -W*HoVCV9H%TA6|m><)|t7Ee?6{r;&OLhKGmnB`v2Sex2-uYH^whKscHuvP`Cc!JX>N37a&BR4FLiWjY;!MUX>w&_bYFFHY+q<)Y;a|Ab1rasw -N_1!q&5(}_ph)RDFF>ZIqaz=RvT@!(nwKqNV1pJ3b~;eoB^BIraP-v|MyfGFoy1)Rg{Dac2S?tRnOE~ -by@_bm);5`dAr-y^syIxRtRZ9qb%!G1+B>{p`oF6b=O8R;g57Cu|`q?<#RCXupze&~^7SUPt#@qQ1(b;ilQI%JBeZyzfC)+Z3G1oUDPx -m>WZ_@LKZIXe9q2?{&^*IE*``zbuijQjS8sJi-RK<0F}4co+A(;DP)^-$dCTO32t7BvV(~LCetQdhoD -$3aEtThZbU^23-P;;4)c$hW4RPFJ0OWz2yN51kRFlIc-EbpAe7dxcP5-fJrI2ks{JbL98^!qSf9pQ_8 -}GXTto1nSaJX`&mgyopvD%GEs4A0NSGDWrM_VP^v|x4;fmoIpvXpg(>8S -E@^a2gEILF}x@b+YZpz)6T->dp#`g$fDqh+V5>%8D`0Y~bP%E~rs(jL8Svtf$|FCY6)0jT&q*H^yeL> -3Luulx82_|LUxeB~HL3Kt+^U3oH`t8HQoJs+ebpES%pv7_U-#U{5uYCoC2EnCMk&FujtM8Hw82R~vih -I%UGL4!ATMXER*v#F3ok##VIQ;96~}fDvD$OrZ~b5(dcYNeykqq@s^U*f=-<$SJLiJxL7CE$-10;i{D -UcC^CWmzLb-k7wbMrn=El`GUx1E2XJBuA5+`Z0xfGrkv1B8aVu9$OY?w+ -S=P2dH@{YFVnCl&TufYBY_AvW8YnGp(^DInH+yhaak8_nlR=(udUsM -qTBVDvo9fPMG_pIH$862{{H65$rFuKBMacbATC; -5pw7|5X9u4A9U|7sEc!JX>N37a&BR4FLiWjY;!MUX>)XSbZKmJUtw}*b1rasg;Pz -7+b|Hl`&SIkp$Rzdp{3A33Jd+Xl+r_?hY*Y+k8QP%Bu3-ixTXKSBRk$CyCJKCEX}+bzj-sN$nBvtfjc -&B$dSlV0JHaw7(Pz+JE}iLPf}gND`jFbKIPP$!B~wXyWS|Au?zD29ODnKy677w~0+&={r6Ug|YerfpGsa2wOB8deAvTeHI&1TC5tp)->9r3TNAz3(j+$py5B?r;sxTUq -&~>AW;rsF&}Aa+oBt?TqzcTzs<_-`v3^jWsR!L&pJ(%O6LCWZ3LvSCn)vT3RcVsRVDkXzirOvJ=%-tk -Er?cW6sqJLlYcsa&y>*7ri8-bIWo0s7fqYYL<6bxjyYt3}v7ewW&mYh_kKEs{J*%2Fs*mhcj8gJFv6b -*ZG@QdE9Zau_9L8otuSgC;RCGPz5Vq{ary$xMqDGb{GtB~YEi@)bX?2f#E9&o@TzkP;GbLx#|z7q##L -c0%jW#~+N@QrFlSmyJ{y7CUS1vGPZ9_d;!1+;mvMhs_h4RhT67K* -{r99Pl}Ml6T8(7%_(cPw@!KjOX1`(`J*~Vt!;kdCF$;kdD*IqG5i7KdD9BJ+04;(fBakQQ`kb_oo4xW -yw&11pfGqdkUYVgg>P7GzAP<*u8XA0*a+j#+K=6Ye(>G&zn3#4_arY43> -di)SDR{NgIWS?cF_Lzudz=5~D0KJ56BV-XZG6o{~&2drs$PX&>Vht$T@u$M}~eeZk}Q@CTKr3I=Wm7T -`&ymvS$$7A;{83p{%1kX++9J~q|d8q4b#?`DBSP$n$ewGI+rfX*28#D09W&lRI4zEv-cm&4Z5_sH9VU -VaCH=8BrMhRQ}-xT@d5IH+TegRNR0|XQR000O8`*~JVst49#4gvrGkput$9{>OVaA|NaUukZ1WpZv|Y -%g_mX>4;ZWo~0{WNB_^E^v8`l;3OHFc8Pz{Z}07i?t=DNgxzM_RuaJgRBi*_vDx$+j1i6NJf$g`R{j< -9XDYyOi%XR=YD;4wm8&ETgX+xa}$X6tx`Fw`1wuuPv&HTQmX^lQ!V5UI`c{xJA(J7#+cyo_1Ev%n-Xt -HvXkXz1jgz#g#{!5;0fD;5z^Z~@6Qh-AdM}@4}^|x`6u%Zn9K)>?c=hC#u*>xRu^0~#LcE1G@A|*pA~ -1*;flzuF1WU08U)Lir`PX4Uw&-gmMDwnQLYZPsCbxZf*DZXBwnN&^Ce8in`4xIrGy4SQ1B91W7QUGV4 -bjFmc`&jrZE2IbdG%+gpj8_&p&{*UgvR_Rw|7qY!0l#d)J!hwmLzI -J&-1ia@W^y429g_97lsAAr!GA0V%8LIGhu|7B?5qd6Z-)kd0Z>Z=1QY-O00;p4c~(>5CJmEA0ssIX1O -Nac0001RX>c!JX>N37a&BR4FLiWjY;!MVZgg^aaBpdDbaO6nd0kUYZ`&{oz57=XxwHXN=hQ<3Y(TfeI -$+3xA;1n@Q7ASY6O$!@l%4tCkCKu$TL344Og_F3iiY4hg3MBN><5T*Aa?{R$KOce3ciO(-Wgk!l0cz; -B^QzPtZppTgCTcmN&l;=YO#aY6Ppl_Zw()1^9J*rP@g68%L{yft#`PDyN{rVn+o)^SS&uHi)<{M0ig2 -?##->Uh4g9;UdyGfa>JA2d8S4y=EM$qBl@%;IAMahY5^ri4%-?&VPi%?@EwrIh?21klOzMO(%s|!X_d -UGgNgkLhS5d}7GXEX-aA=A?2#C<8Kz0{^vt*x1}`z=DZ17SN@q&2Ci5dFQORPv0%gDgGIpKHOmt_6G@ -yc9v4$hYLT~Vsaxb#?K6!!@nTR@s1cG{B<_Go@wF(0RHob8qLpJeb*d-oitX5{EmKc@2o@cJnr}64M` -@k7JSkqtz=+TB1oDiHpoxLo{bVcsS|Azz`#yLmH5IO^zky&d%!>=!)=ig+u#1@(B6H~<7e3~nMKK|H# --G12=3k43CH`Pu4H@0w?6X`Tt8m160Gi%r@6aWAK2mt$eR# -V42Ti@ph000FS001EX003}la4%nJZggdGZeeUMb#!TLb1!CTY-MwKb97~GE^v93SZ$BnxDo#DU%@&bB -Hua6X471*3%KZAnxKn2WDnT{2@nK2TB2-bWl<%m_4?ZW_dYYEUi`9~c0VK*H8UK}JTvo9dea!|MOkm8 -&({+j9*rA*rH%Cc3oGlwY`Q16ZoRJhCog^fd*>#lH5-we+N(QX>7|IT;>t$Wa;0pL@@mtRx>B9YTe(r -idqoe@?%v={l-o7BUUgDG)w{X)S=SJ`yqb$kudMXO&c(0V`MLN|Yg5lfE}fqQ^+q>ocB^m%n|f8tT>D -*T%qnkT&Uw>hvV^IuP?f~WR%)M>`c4&ND;kHewX9N#fJ<|;L6|yQE9EnaTGr~~ew~eWd8bhtiv(m&|D -h(6Nuf%SuT_2%te9=BY(hUT;Gdm|Cw~=7W2%dZz~3WrS&FR^g{dEVv6i<=NI1O{-UK7Uua!_`sdBIJ5 -IjUc_E!^HwrItH0%IB*jAw}Y#F;tSw)wW?zE|-r(=l4LQJ>1ettd` -#){gIQ%-ts6N8+l2rgdT=-kMtR+ICFEBVS;gv({uQn*jM!?wHmw&={vparuF@lI!2Q@i4s -Jv)f;$FsjQ^puy31AL#nts|YmGf^G3NJ;teiR$h%bj-5x6}e1+8PdQ4KMdIboRHo73~h?CO`n&tvTl8 -uO6hh3GXI96)!C_A@%}*nNt6EQ`M8y1L*-8LQVcyYtXB5@&O++qo!|^Fs*}a)W2n>GY$OP3)&BD+5AH -q;G>|MUi!ElK|sPexuMb0CxwNu!V>!>Xe)Z_=wD4(W@d2HFK@d7%B%?dEdB;r%#vd3P;`nCUt7;>Z!) -D)bp1w&(6}$tH}{i_FTlt!l6Q(?t=m$yd-3Q?iIf5r{p9Swada$Gae0#VIe8^ari9^KEkL7QKVLcC|D -(noiI6}?jL*<0R$KnZ7k_r;-U?WhT1cF@FLa%B=O8&Uc8o=d$z%%;C{(xLlzFRo| -Mzy|5g6ycXD1voJ?cO?3E)zhhW|}SR(9ctf$x6<@NQ|t6$K<(~4F?d8E!~8&fFkG5OoUV5d!F@m1#Nm -zm!*e5hfwx%lq8o2|4fcR_=k_wFY6=WeKf>FeWNa7HrhpH%t=<7oL6_!f=9T&21|$CLsav52VQ3N3L> -hY-9+jRuy1VgdwE8^S4IKvg?VCe~H$@LZ7QQgQ1CQJ|+nyPGhZh#YXe#{(l!5`)$u`=sC1TR0s0FsA8 -JF${v;j_jnzEQe<{tXflr(!(x1%JIMN1j89RyO<3%)p(3k7=`HlCpc8n0|C?qgKEJYZ9c4OyExee2NC -p6G__u8iTQI%%(W$nO;e;}IxKe?4=XcjQstNhxVG}Bb7E}C!0SfEe$*#+T2)2V9QqAZtLi{9$fRY}x^ -=SOK5&}S?dm+~WDu*`Br`(wBk{f?id;QTyd$C*nk)zbP%fb}r*2zm$W>90b7rhwku$MFfsalytH6|Ul -`-MOY59L_%~}iMK!JrW4ykDJY!=w)VxXkSS$h709F10CZ+`gtUb6 -MgzD^W8YNT&tZfCMhBEF#)o~3cC)fdZHO9C8Pb>u{y=CW+L{;*I4}eeB-EHnxj!83dT%oVw0mME2L?L -kXR+=+P?82(FcYR0zg=Blegez2TE)_5=EV~fuRm&B3AS#(h?rOJ(OrytlfzNhF9kI`-ym)|!&>s`iAQ_m;KFpUm+zs){7w11lO -T$Qi%!Ise*59yj{_N}}o9H?s0yqsm@nJ&4-6&M8TO;}ykS@z{$C9?l0^VfRtmRU9)?DTY#A>a--c*~~ -H2<{Dx3}B7-5+Pq&Y%DJ#Sbrk{7cXxZj}+Y!p(f8l4oZlL`6S(m%azK)2|qpNw~(R`P!fwlWynPp|u} -ovr$uJ4%UH_&4e_Q?FU+PSp+p2R0&yOU|ePI{{Hcu_;>jI>fP&~-gghLKfU^O#NPwAV~)eOwi6t0k77 -lI-;~7#+o9#2P(W9w>u=5_f+4z8xoktvwu-vhsObA%=O~7B*z~~G&s0+$YHPh(qyM)x@snzI(p-+wL_ -7dJVt!x;O*34+;{}6lj$G>spPF`{r=#9?@9{8Xw<>UjhiwrlCR>MY6)p3U+SE@Pt8bBr6%N3+)qwEyV -9=hp>>YDnq?+4iQWi(km;zYeFj!`%Y!4p_;z`}O+2Nppk%g8Prg6L*B-0OdcP}EQRcIQ@Jcqiwcz@mR -b2jZ76E%FMv2M&`@sqym46TViKYhl-A$uhtRCq_9U*yu6s`cU0=x8NOqb1lJ_1{ohme{G&w*b?8V0O$ -;fJ3_Z>wGQv(;X-E!`sVGPn_RXbVZ<~uPU?b989~(7HH1J;3o?7^OwGz(8q|m$mY=_&5eC4tDD1##rr -Dy2opaa5_7^%oH*A9W;U7=%&)+iL6eicRuVQXXTxPNTHm{ZN21qd1H*y7m>o<8{_gh~mjc+UjO^~3Jk -PnLa5dU(2x0Ti?pgmOYZ%cC!GjZb;$xpW4b0fynI16Vv)+B4;{Uy?*61r>vad7iEcT%MKTb}PNYYt)c -9>&WbKlN0rzB#I#902I$&SOEY4%mM%aAOHXWaA|NaUukZ1WpZv|Y%g_mX>4;ZW@&6?ba`-Pb1rasjZ#r-!Y~ki&#yT -0WSI-hM_~guDhk65E!$9}Mol%4CM8Ly>#yI{T5RWo%S&>X@4oxK<0z#=sf7Q@1W272n{qFWW`t5oNMc -P2_$T!aWSSZ4A<8o)&Oe#VSS+;{R&&L2FO_4dbekIMG9|q@dO|)&VfY${Ur{)jjo&8l2$UW6ijwDf?~ -rkH3=ix9FE2}l=nyV=Wc5hu6+9nDkG2UHfo{S&N> -QT=Kdlum`ut%k_vxFZRyO6Up+fWyXUC%3|iEpUuAoH?Q7WaSQ(9Jm4~yxxXWt6=f4WXc?b?F1(b5|=Q -k;o5bjq&)T_g*4(JS5A;$P)h>@6aWAK2mt$eR#Q^tJykCS001To000~S003}la4%nJZggdGZeeUMb#! -TLb1!FXX<}n8aCxm(-*4MC5PtVxaZmCO-52D#pq3W79mds9dvZS1o=C!)#B{3|jnp3L;zKe#ebL?!Vc{Vwrx -^Me)GFFSi&QxoKYM8wEok_zd6H&KHTT*8KWO_5Hw5rMWtqU8LZ-QS+pSo7UaZ>V%wo>H9=ALVVj~f@F -*~SUYem&TK#p0jm6T)Bgkt3zX3|rRm@nfae}O&A?57ddY#dgof^ -WCDU#e6lH!ibKA>6U*pO>=#~Zi*S{N5N1A%~!Ak6Y@?;>>ubO{78mNg~QC9rahO$D%4GFvHKQ?1HQ^7 -F8~v$i$o^Z8cTU6&*KdfxVSrY{^wHV^Qx$pDy03K$CTrMssLKB>>q9LCV9UN!;n$`W|$8m|{=<;^+;rjDtef9Qo^X2`= -+pC-FC~}fzt>vnlx}Xr!xtvIVKI4 -O1D@4KBKbyP_e@BZOHMJYz#>-Cy@O1$EPJt9;XQ2XhfNV0=cS&2kjR`BcmJ&^0=J{d^Ro+zGgypEc;! -F%N*$*`;A+6WMB~J6Jptv>RLEQQRN&W8DOJmx`1DSZsCLhn5?j!->R?lK!Nai<8n7GD>!KUtW3|3V%v0^Ew`KTIl}W9T6WBEd~{FAKc@L@ -U*}35F9=@h%*@d!C}`HocMY0%MIR`NoXJdT(tgou -*K5fN~95Q4`MknOXD;kJ`ZgH!uTxlF-W3?Zb~7~U9x&Kk}F5CPI;^ghEN0&D8t`+x$-`XKDCv(6_c@F -4p8CmKe{Dm$ZDLDL`vd*ffgJ2Z0qEWJ2MrXC@l`uNk7rofmcPhn59SN{X-8wZQ!x82aLR(E!X;~DKjm -L<(*`z~;cZY6%lbAdkp;1nG5Wwn|umx+Jyu3it^jBb#H?HUa26mz{`7}|uJLCsm?G>RQ$g6~*|&S93h -0)JQe!SVsJw&e|XR{-1@dul~Xud`*g@}kFarh@BmHSjkJEW10SFp{&<-y&|6?3|*XlcOM3owN@szOPy -8+`SGtmJOlzG6XayVkYw==CQc0$^BjO>z>H_i4sMhaL0n^l^~d|u~EO{C)dV-z#k8NYaNnHY-nIZF{} -?mI|+S`3{W!buzB6ues4W=8g5+746&qAcU_Cyn2;vhXEjK{cUpM-3cPRKr`B$0K$e~juQ-Y@UZQYcz` -gv4g-3$f6U1cl4^T@31QY-O00;p4c~(;qP-p$w1^@ud5&!@l0001RX>c!JX>N37a&BR4FLiWjY;!MYV -RL9@b1raswODO$+cpsX?q5NuA1VWifVFEec*w9U%^IU?Qlwdj!3bnprffEnsFGA%qv(I%-I02p2y;IDk>{gX&TH+*zT<~1-I!oYTgWN3V5ZlJkL4KztCFgF~r)++{A@6X@j -T*Unl9dF7M4`BC|g0*@VF#;dn!vWeM&*q0dj)mAVnO)+K{|v|87xSi>WG9>0vAeeYMmkSg;%%alex36 -^d}dXMh1iWMAI!at7(oLQ=h3T#=)T-rU`NxNk#UklADw4FT&ZOAbyWAFBMt7y9e-F_$=MsH-qJ;rP@{)!fv3T%1; -Or1wJT3HO0C%g&ud{O%^vPyEi#sX@TE%|6#cdcYky=1y6Ga6M31uu|pg<1q0GUdpGLv8^s2ePuM)-ddM_02jE!|!Y-35T05+57ryfJ1Bdh -2*Lj*U(xn6AOsIIxpOxN`0JQMOi47{FRo(@J4ICYENLW+`@k(YA{8>XLr*RsrMMl$2a9re$9;LlLJ6v -#u7s55T06;o^a4JRnpUtKC~!nxv_L3n-y_7J%7d5t_1 -LwMEc}2XqSi2Cx6p#o4zLfn1&sF~1GZ@roLcqnmw3*GI -Zf@+Ji!25ay?E^Ho%HOK3b81|nv)y+)Sl^tDcnR_Pk?(CUJ{`A5E=VI>{)@ -(F%u9y`?)2zD?-He)O8)#v#5rM)MGyt?M`)$PK_2>saxhM-W`0#j{f0~Q>99w|zqTv!CpR|h!}{z?$) -gB|qpt%qnB8B0c$>DjC4ZUsjYcmp?((GcJ*Qm6U41KB8*o0}PuWg9g;uO^AdtFLU&%sn@uwQvRE(nw% -iPkPw6@gUK?9dsXzAS&-|BYO&{d3T5|oDS?+%33{!Rh6MYW@Qx5b+ef&e=m|JB`Wyehq-d$h_j-;*qt -E>D(AhL`G1FJGRpcj!S?YnJf`yo`zVd;uyOxV~$=wR09g<+Q@dEr{7~SR4SpgXB7SjA2Cgc?pfWyZ!q -gUW@{VFoCC{`|7VriD02>0SS4&=P(9e+~|e5S>el-i~+O^KUx&15ir?1QY-O00; -p4c~(=`e-Wdi0RR9S0{{Rm0001RX>c!JX>N37a&BR4FLiWjY;!MZZfa#?bYF92V|8+6baG*Cb8v5RbS -`jtjge7n+b|Hv-}NbO?n?rhaJIKW3){$`(AlF-_i7aTJZqJ8-bqfHZ@;taWNm3-!iGDY^#A|vlqAV#X -wmh^&`~`&gxH!0*8j#T1}Lx^7`JSE=!xSB$b;?1P%E`G695gn;~4Z5g55b>K_oyQ -L=TK}!1!mtARfloDxe%9A7DWY2O+>c@)C@ktr#V(!8B1IAHl5u^%6~ZGKw?)0R>d-YmyvKmxNuu&Qy7 -^5)<@O(OG{l@_CQGC~m+8;Uq=UjqtVtCo|dJ6#KRQpjDD2N}YN>2BlPu&8%OBi71|k7E5@41)0p_lLA ->6kdI7^4)?~#Gs{%8&8Vk)XJSL#!MjUHYQqQXlgHcR67hk(n)1lUe}xlKaMKn(RM7|sdVOXJPUk;1nH0*Oo7>_#&&urX`_*g3oVs -4Mc3?e5&f^C>=UXz`?@739SfEMND2A*1IvvOZEdJ1>^4;ZY;R|0X>MmOaCyxd{cqdG^>_Uh2O37nl%|_>7&@UY<|Il -gz_A0_YgPybL6Ilx8j8p9NXuH0|9$V>7mpNayK6U80b=U-?%n(TIUOSA1(giC%xl@|sraRlP5rH}k8IoEQ6$yh-vrS>-e&a{x_hUWMeUtQ -jwoJR~63w2svyY3}oAaCb)n)wQ`h5)F-p&6xj$+G1YWC7XG!>0AR!t^#wVzH1eDu3|Q_&ju=T)*zCP(Cg*Mjgh$=G^ -LD_Tff;gtYw6acrS3Q4_TA|$Lx^F|(ey%#mzN=gaX4Ipx8j|@LE`3EGa$T7!~Gjhobq!e`;i#n+(tXY -<#oTo|d2NTh(%8I8{2m}Nz1C-Uc?;&R`2(P>VP^Nhz1bJQY&$PI%Gu}vWz$!`eElGi*$@4tqk~n#U4-72IR4Z$9hAFkT~HF5W6M24a=!@A&YS`$>^HIOvEdREe29)EG6rMY+^~|RNDRWbt0&x> -NX_seo8uv0Xg}BtT@lLQLKr@L73)KJWqlPJ! -m<7tI!&3_RqFypk$0A#I7)eA~6Ba}$M$#)hVnZpe=1Imo&ZR9X|pG%%+UEUM;YMfG`LiEHUudx;F8wq -W9bM-aMYH39(pEQ1Yo)H(QwypxuSzH}mzHLVqn*3}|)h6-`gp<-tR-0l(Zug?gGK|&kBthQxDrc_I0xjR14`0=yE$v4wpQFpQhp6&(YBf~3joxMo`# -ujNaSi+B9<$?^8&IJ-IiaeDk=dVK9o3^<8_Tvo@iZ`l+yUx)VFi=?^(2ge*}*ODxgM*!*Daw;7fj**t -_Y<%%~*V`+FfhN|<)F@k7+5unauaLl3bT6cpIYu1x?J}BJOw%%f@Z6{KTu}-*7^cO-8f?xAFh__PO`f -6YLf%}n2bx)Y_}Q^~gEX)T)R^)r`wB=h$fH4qP?AxH2oO`}dkfl>pw}R>>X32|trUY+V`yU9htPPc+L -sz5Yb~^FiZMMTIlZTO!KJ55$;0Zf;1|j)RZ>W>Vp_{D7NhI8#K-WK@tfZ1PYXHF -f1U5m6}TOy;c6F^Tp--^8K_UeeF5jy_UX6k}Xh0{&0;GbkQA=n+;VQ8@7@h1I?anAZH|+m57i}lw`d4 -swTSx0w%-g*29!@{u!A3Oo?YmcMGj4iQwZs&CQ`CW{btuVptCLB(Jr4^G4GpJY#ENI -6~5F8!y(0?pOBb-RCTdC -@PN75>s}m)ekNT0TIb;W$>Avs!A)$LcG%4f^KhaIb1bSscf^Dk=RJ@>$4$B(2MIQOhN)K#7R|+tfvHP -f0VM6=l1nW02Ny6bm^MwXe?S_HxVQ~EWtTq8rr9^Rjs2Tu*Rmk~Q8ke^ -)pk}Pn&=Yct&9z;hC$}QdXpeYX!(jNRQk?DkWF$=+m1z>g!$g`_~z>3YH24%VX%Hop^N!j9Ig03i*d{N4sZIJ{X$p~trWJiEBK`s8A`l}{9kg(04A2TB -taw^XAtwO#j8HQl$m2@9d@R%FP{0wC9-G=HehY~#n!M;jj!{JAyFpehUr&-d8)8z~ffS$@>3hK3AX -xcNlRt$;p*`6#y(h-KcS-Y%y6=~1KgIk^s(qNCoJ|v1+ReVadV^TIQp6c*`1BBwPb!^$}bv&Ekcmh%% -hme3Toq|F4hAw2+)&`Tk(=w{nB#*5){V|K{tm}iFR?~qD2Til90d!XvyHSD#3MP)o=4)LjSOXz-y+?u -RB3n5Yt6j-J;U&8whd*qKaB=UcZrFJhsTY1QD6)1^U%oej^yAkYeQ|M|ez0o)|yn~j}fkuA@f9HgB<_o%|58l44lMg$2oZA8ttnDiqnX&3N0;_l`aJ -I|8iFBCn*9b9C^n)|_W`s%i48=cve9MM$}+tfHNg>nfQ*;TBjKsHEyoi}2Gk42~TI_iz{KslORXj667 -65+tb4?^5nkP7F7vi=q&O<9sVxTqP}5y8=3QCvzc*Z~3VZNhN2fzbq-Caiai%)3Onyiqe_m5ni)!6e_ -X1%6`XQ@JCEp9Wk`mqS#_9jqMyS59Cz$yZ%uzM4t?(&W_g)C;FSPXBl$i)+d#Uk$c^*PlN-H@51zglA>#-;7j!i*GZ+Nk0cVNOoyE}*y`>oEHL0$&R9jYEyBte{+5@&aoKchbh4kmlQT|f!SWkgur>mDu}EWM+ -(2r_&aoIqKa$sm+G_fHT5%*<#*|mI2zm%xCj}N`BOXthmNy{bVU*r!M(Xsd2Xj~1`-RD}#u4&YMPMCx)EJ(aDhq+(PkshV|3vgJ`9<=NpWRwhdijl~P~7}UE -}MYj=%KJ-TpNeOqQz0L*|eC^bBOV8LZycn>4rLUzbleHHcVp -Ev~pmIrTl@MCAt@tGC5R(Mnq)o}o?(E9Tl<~&&feB<;U&spp?cHD*8P@7;_XPP;=lpn=R0U222B9RRA -3UXqZY|}7!+4~fd((a_KZO2rxa2|htSgXRTys}HPY`V{>MBdrY1K_1yo4 -_Ah;lF}ETkMx!S5Pq21)={rKZIt_(!?CXn(KnyTCByfG3iq~fCjl}<(FbUkb6M;QAMET8vH$iGx>?Uf -BvS(dOR)@ndGF$^pFMKae$i5x3FkSn?R`WOlX=A^Whn6PyjG=FX)N;s+!huLNipr=JHK$GWdDhg+1y9 -P^=I=kBtT9{vm$LrD<$47DrkUkK053tx42v&0k26CTF6&sW0s9M^0bKewa8M9+es88r<7ix~&k4$Zau -0g0R!7q0AMY`?5PI5IRQdW_Whx86M_8sh^9?iwBb#WEa{Of@($v)Jlkdi8%4nYKM$z4vRBJt?y>FMd|*Yxm-xLirGDD%ABWW`d<%k^ -3ot!P$dm#1PTMJ?CmtxQD%56iU_Rhhy2tPm|W?AorD(ZLh3-d5X*XxciPw?}2wW@VA&)7MGf$b*AyU6 -pkU$ct9qwRtwvPh?#erM_9pwn~~t->#BoWr1b}E-#l$pjF?NrbSgY*iEB&BZb#0cLyEbXf_13iN> -KwX>s-y-|7j?OYu7rB`5yn$2FW#x=t*qO!%p3iBk>&7cfrAW1E8*kcSrzAHv6M~AZQ5-m4g6)D%;hYZ --(12|^B(!G0Q>_1f8P|_kS@N>ilp9NyyLs?k~~jlxm0(|%zq|DvXnJHy8vo>2)b)a_3lm9w0!kW%?{s -P0utTBWmoCONMV2VW1_Ik6T}i6F!5DZMe(gH(z1@Kn`K(GiP -He&GF~UYmUX<&3iwq00ihreaRt)}Gth*hYk*>WHii`iv58=LfqZJjKV7{2{@qU>_k+vjGS0K&MmNy+F -$#H%&0-{RNpVkdY!buq<gTn-eQN -)Z&S&6w%7t&G+0Fb0^^-01sLgDDUxEVXwww&WI7};023~2prhglJC6)C@4(HdHyIjOB_X ->FyJEgdz~-jH&mtKS`9b8cJ|hB#6B&rF#j{B-IqqBW#Z>su#L;NWKs*)xNyxG;^L_YVK8Alfd~!8;cI -~VNG`GHIX$4qf!#*PiPXd^ZmNI}<%^Yj0@_;?v4ruco^Y!{l -NJ;2K9VIBNh4(6~ltFy9e#QEEIm*@X{fB7@|e`Vc7;v>k!lP{m2ec4Yua8yR* -ABsOs*-?y`NRYR5!gIRt?h%>1lk=`coA)4&$Z~rIT7pA4+7^4WneTtovVSFM5<)Ua>hc1t`yE|dbdZB>A@r -!<190F!pz|dph}ITXb-oad1b;n+*q|0&h1%emdei{)rFPwjzYVu$L6~g?FgxjT(1G#}8~}jJ6(9u-Wu -1XlO>$v;vx8UU`uVjasOe?hN%jM$Lub%`7O+1!?VUpZBzh49G{x+%_#mpZ06+^bL8r@@1#Kc`;J(B3Q -d}}v9ULq~J%&N!sdE>iQpT$(H{i#hhSFTtdQFbJ@)w}3CdQY)0`n1~!w`Wu4~R7IVcm?Y9tZ+s#1$iI!aM~d0fG+;6yrJ8M{p -Uh%T$X0q#!T)7+?JK;o|aqOi|f7Dr*lxYklgnTl9ZhGPP>q%R2~N8UxGA4aIZ4uDY(-!%=y2VzHws$|UAx2liB>g?RMbUw-R -RaOs>m(_PL@eQLA$V+HG$^*k$SwyOZ^@~cZ&J-h2-uqpX5Y*`wzIS^x?^AhrN84o;^+c{244Xq>$X9u -LTFI+&<_au7Y11)D@PGhPhR8!2;N+w{kB>LVsdS3O3EuRqxz7?QZx|d&=nz_B{$v<<6dV=u+Rd99=V5 -R1s$UV_*lz11nOI44SCG`+qlLI-$YkIa>W$27KB92 -1<78r1gM+JI7r@d8jF+w94onHhtmj=ib!yqE$kX3aF@mm8iv*H|Qi$FQ+^aTYgF8fHhSo}f7!3ZE+&1 -&#RVY+(v8z?hj-MQO}aS%rNW_)DZP;f~h!}MwaI~Hr%)G;)}QMf4H=r3^uVQy7ZlaExfn7mks)Aci`K%=raF@ -jSftF8~jy#>STi`CgNU=<)QaZf__&27c&Xt`lh)d@m7rmB`lERw-SvNu3=03wUU5|Xez)CvA<#si}Pv -EfIahI>p?~V68w1xeHmd6tMY>gC#Lot_s@W%QQ9X{RShF_XM;HLbJ^8B2LJv~^+Q0DgUD3OhkP(|WjH -Vq-Y&ZP2NNM{Kv)JClXL8@WHxYG_IG=oPL0f3%zMOqILWwbm0;UbY-Ib<`BJ?P-q;GIX*Ok@{x&iaq2 -d1zw5SYqq`rq#LBDN@9;Oc#u_p)mGvxFsfQC}beNav|oMyc4Q2tB20I4_)Fe0ZMGX$K0im=f`X1m*vu -XngT4B3;IAPYP?wJ)+dMH1Abs{|3_?puCy1SIRuo4 -q`r!6yTXB?IJq%6fWWG{r4(zwkYEwv3ksnqaN1&WPk1eTz##CuEb#NxQFc5fr+ol!&=7Eev9hHSU-+E2b(?&ij}1GVj)fz-esMs(TFrFYl5ybTygqqn9sV_ -GDgN9WKgpmejCl&VT_x#?{GP3PHe3%Eh(H9QYcF*nx1k)SmtM*^95v{_^6*SBKc_^duC|RRg_%h{&n9 -qMVw)PAZ?QE<6Tr6_p%Z;R--F_Faj1Y_d-7r~x>+A&yKnfGccZ?OORyYYHhLoXknQr{G92GD=n;GEbW -nMM9MS+BGc^5^|S85Y@_RjuWPYfKaM7YR-4}0|v1ODp)h67Y%#zsCg_u8l%i4!%YrRZ6U(^6`n_Jxz1 -6Sf{W8tQMLUW&)~&k2*X|~hSh+UFuy;4FJ7GfS!l36Eh#oqmldxS~R&hZt~4`T!02|{_){ -z;BeD)J006D$Rw5}l3#Pl&ReoGLjgDxAm~Jy*NhmG^;FqrL&=EPQ8!fdfa+?c57?F`@zd9SAj#9!2F4 -t1>vi8t@Fd;->Kt`J~^IC8?dDbw_WqjJ`jA{ml<=F5_>1c>VhPL;UgLzt2ft^)~Dk>9$@Q{hY}>6;&p -2zZCRMnzRXTdnA<1Im)veZ~&SI}k@kK-L?}>$%_Ah;M;aZ66=#gVTvu-f&^PAEmPj -M8osAJUJAf&mrp#|;+-%plHQDFbkhC3ljJ|s(h}JH+I$2AJ;0hbV1-iIE}j)zS0GHAnmAEk4(;Cr8g6 -Wp9WvYm0Vr{W4FLEERIICuY`z*KGnGI44ybMn&6N^BTfMk;>Z)mX%p7Ps9Zp?X~fLoOw&v;t^cND*7bir6~@=CRc@1*St^QG5R-!kSM+L-Q<#*ga8=`|iQHFk; -eXy34Rf0VMci}HIL#>tU2G-@?}v?)TLh#fTGD|M1@*{`L|S2eZsuC6hq-VauXuHWRF -WZRgI_Ri)&$Ln-}Jup-KLU#iW6fs%=tjGBync$2bD8@dh5GWd>c7(`BCesuU?To~!H2QxW4r`)q11o2 -dQQb?-^%rrT&HUgG7^5>1GfwO%wdnF|`H3oRdfj3wS#0`y>-cvXTLMIV>L3iDB1S}D$_8*z_d^{KY@J -y_mC(qf29LL4^Y9efJQX&5jpbvjQb?7d#g0Rru3xMDG6~vamE30Fje|=0Ja}cjiWF#n!S-$OE;C-qbd3Yz^&m%|BlTh=l?%KHw4Jz -`?5R=?+A0uV@&%M<`&@dWXT1PJC!fD^*lb|Dj-6CG7Tkb7XmBo$dIuf0w+YgR*_3Wm{-8oW%wD>`_F} -sDQXY>rNeNB8;g(47E8{B8DC$MGEp(=4Ewc#92APx)5=Lo3kz|-pK_LX<|T}QYrdw&}rB(n>GpWgWeP -nvNIx*?&b1`DiJZX0AyydNX1xX3v^$-p#-ylR;+Hbd6y@(O7CkIT$dLQREsHmoIY0bND~yPNYRxE-D* -c32I@$|IbQn!#$lw^ShT4d!^)yuakHFI)h?)tdfCwRz=!3U!idVr09-*Lws{VI2UZ%(Wc^c}R24Yjy6 -#x{2e=0uHv}W>??ou3 -8^a(6cElxT~Z<^0H;=_pkApq6M_toLv-~(-e`}6YR>4L!n11Is+k-gFF|Iy8R9x-dvuKT=EoU0Y_d6i -etPzT`2ey2Luj{#3Azivk@+H+AWp2vo~Y7};UEyo9mz8KbV8l!=z+w4 -FhEjDPR1c`uDKj%DZ#{;utiVrM*GiFKU4TTro75TB-N-6W=E|!D(=OCxKD7vZ%8N2F_3nCkL!45ux_^ -_+j+?GRb$%R!-p^X9bkeHnJ5@~xy_N1#R@_8tSR?W#{ -tJ3o1VMS7pURb5VQbhBs8&HJrV2{*6#?{uG6jY;X)3%K3}_vA92h~4^j&Db=8`^F7##&8<@Mz4wF@Fm% ->8vH3pmuu`bD?g7(G|npe26Rf&c0dDja|(m||gOA98DHq;az+WFFjBBqEHt#aSxVWY=I%2zQ2SseBi=PLGXuYqO`l=aWQ0}8GQnh?!!QdM??vusT5IA%#VgYmB@3i# -1B1trs&qDt-SElXaT&meR`f$DxZx@+K6Pa)Nr3x0(G7>D?ZBWA*3stG#yy?nEDR*aRC>!a!5DOW!R~1 -G5A2k)PmZOMR2<9sm!db4q*%SD@aIv290Y*(6h`V1%xI2;hty4QMqa767&&=r5RH0|Xr1>qhGY&QL(` -Vv{_Vjr-rUro4j84i2Qd0kC1>V}CUu|bXIL|>rm$-Wt|Wsw*iW~Hhd16HQ5y=xP@0B#CVdyCz6GHl?l -siID`Vhx&aoln)aBGvUuc~&wFYhKPFwIH8^nf&%GcIQc^dsoEsy5ZfvS;VD@vX$9%H*c7}{3L()5<~Q -sY%thDBM4EQv4Bpd@ri1D8|urR-cGPUXW{G@|ojmR{02GG=o560nPzLUFc_v6W(o8eV9Uh#$nQ8SWhwJg2r&_8q=IhzrqqGddqbbbP2Hv9@D1lMV%~hDpdSFPJdUJ5vWcb#|T -@6>ZqF~aGpzwk$FrA|5nFH_wcyNQHUSc;bv7_JE;A!RzVo-dUR?q`mC|97wX8Se>f}xxE}NKXq{pGNe -MOt&w#el=v}svD4qzT_9@xY4xK%m6;(=(55 -4vOSRVnk_C}HSNq{{i((mB*ySg~s}hS1`y5lh&x?QH08!?K>G;!>@2JW6`BPQirk_3`yQ-?uXA(at*- -T}dL^;ZJTFM3s6rpX&hrU7XX?ruYFku-e`~1l!>RQF24S6M1!PEM-amzIyAgejE$V -{Xuk)|8#Vz{b3@UUsn#q-uAu1$(-uC1N_)7rOhZ26uAKKf*xR)@7P4GTvf};eW$NJAHbN)uIT>rFFZ- -QSTX>+H1s(S$QWpRJ8Fzkf)D~^7PwBNbwB6IZ7x@xAaRZ){g;oxPNP(Xs4tvpI=5F(sgHGbW~Mkeout -nDgwRGd366k56zO>Hj#5Q{^UB?X)tK>bf?keOt-o=`35P+x0R~o7_jO+rPH&ozM?fiF>;zU^R8+1kuY -XR3||{8GSM{>>jsaG_inv2d1MCEFTT+Aqesk9GaL44pIa9_v9v?^AFu1^W6jIcsd=Eyma^*EB -H*$r|Qfj$uymQuzxMbJ;LN&mTH7ZDYyu^#QkX$k#^@dr2xg`U&Z}M>|@aBqH>G$eMgmoz6=d(yvkh0) -}bDMv`m}=1qE#%0s7m()T!N4)ZhfOnl4eCegksml&cG<%*d@ZJ;_I))*@@-uQQ+nL -Oo;MUe8d>4pUmJ2da6U?bc_07tf*NfXD`0=>Fg&Vz>?JAOM0Pxdt<6jxsaKM#cryj>zPM;NsC^9uumE -S)$Q9=&|LUDHRBst8P(SpQ~E&+f^w&E6?IjLrzNa+I;F42+|Tc3C1oC)N{5lTwG+JnjaGyNkXq`h2&d -4=)l$xOp(4Pqub-djL$y?oaY0an63JcjYM1osoOVX12K-E&V6hIh;I5IoNo+|89dsETGN -J?vr9$FIGeZqx_nQ-#!P5X-_2vw&p3&8FxOx$UJ1Sh$i6!oEY3O#Q%|1S&0YcG(ntXnn_2*Kn8vVUKB -XJ3lCeG`1=yYr`2q5ePh5g(H8nEdHij=T7G=t=~7)mfNKtF^)K@^{j(~l^$$HF=FfXTv|{N8u!hXj&Z -rm5c3J3R@2P@wcgoQdb+OYs)~MueaQo{1OY&*Cdr1W!ZJo?$F6c!JX>N37a&BR4FLiWjY;!MdZ)9a`b1rast&>eo!!Q -tq_ddnQt5ymA{n@kc5#}jOW$nCM6{v>Ia^}-9!%=g}t#+>s{ue9ly42FUi -V&zcRS!8a<6!Vd7%98VIA$YNdy9d~j!7)!f5x1H82*$#_sLw@0%lu(#vz4wr*i3TD__LZt|6>irp4El -&dWzBKtmIp?DQyDoiSz|=23w_x?6d)!5jW@KZTYz1H@EfQEd4jP_1`T0bm&{Ewx{ -2j=jl0IMV7RSD=t(GhT^|d#(8PVlZ9*RyFSlN670yobiKw7jmL^D1CF*Xm-b+OvB>{WO9KQH000080Q --4XQ)WGdPksad0Ei0!03ZMW0B~t=FJEbHbY*gGVQepTbZKmJFK}UFYhh<;Zf7oVd8JlOZ{s!)z3W#HS -`?4~hro8xi+~gjc9SA#zL5A91QH{SWF``+l2j7+*LO%tGG#k^Ss$F39L~IX^JYli^->v9527?uwmRZ| -p_NU;MHAIZb_6=cTiSuvtN}7wT>GD)MbDH5H5pt0RCjL0+n8;S9;e;g-f$^cyCUnMZz1wFJ@0A$2BMO -)oBp-Q6=*rA67+!;#w=f16FAmAl)UDk^oqRUH%9r%DXQS#fh*`h7(KbT->n@v8seEw{NUOs{yf;6!c@ -30pfF1cA0@bq=OZ^#z%>|FF~iQ4lIqwobl7Uzaa~TwDz5vMZS$U)O%&NOA>*f0y=VjG%B>}NE?5V7o< ->nrK~2gHl&|@iuFm_d*+`K@1V4L=*<74Q%<5^T5nrJn -wSlch9i6A&bgo5k&YPzr3oYP$hbc7Ch@L{G;-cD)E4XZkerckC&r>7vL@UXP(hDgux?fmYz*ie*iXk^ -q&n%iaNnhpGZZ5|KO_O#P*UivreUnd?Aue3PFNI}K_N}WS`ASAG)K)0*O-TPCV|-KjOrG5)H~O(BHl4 -6E?BFfn8PvL)Z|jUv6fd46EhjPtVyv~yMk;OGAfV`XH9CUkY~Cm4FcXfM!0r@%w|+y$Ql9rKEc0AzVQ -k{2uUm-)+Y~`9f4aujwvdi%ZXCmH7K2Pc>t5_ok@N6ql1ah(}`I>Y?7G9^hI)!bgWNMtxK_{X&MFa_I -z>iInJ?Zu$}U5Y|;c<&t!{MchVmR;Zprm(GI#io8(djeynb>xiu-Udb@yWF%xW=C1^~F1*EhXbMujwW -uC0uAVB8g>+|*5kF)jJtOBmN(1wW;Tym@Bu%$NDOQdm`G82^pPZwBsO%_IVt7&$yiD;aQMKX(%zHrbY -%EMss-9LYyT;5#V#z%b5>CQ&K5MBo@;fF1KXG-9BN#Yy1b6Cp(X+cH(rsLIONIPa}D@)fgqagRdV8N! -6%&P|-=@fM+sIhW;=-sIrZ{U2qluv{EIPoBSi`TMk<-47iEf+w*`qV_C)DX=(Hyb;jZPfL)hnU!J7Z< -ljFFn&5su{uQPKf~kYX6@(UbJOsRkjklIt|*-DYR`teb#^YGn?2uB9+UVhxOUj)%ks>6`Wu`U98xF5^ -}qvKRG!DvL@65>65+9AhFOWOxTt?`fGl3(N+z4$?yWgDfXfHe{lN8*Wc5bf#BA#Lo&A>MU&aY9Ra~}W -gbG`@ugwZP#Ub>?y(bC68ZcBxa}%zgX8`*IKB|86Dx@zH1T1z6()T2AT-{0D>n^r1urb=mP>)oXhc5!&A?=U;Jf9_kE^wr0Sv8R)Pga{!~s5-iRhW&{E)oh>#LsghJu^{?L@DciE!1%23nVFnV*1Qz6c -vmQ7x_Iw&$#p!dV0P&t$(`;8uZCp|n{_Xt`&BJ^&2|H`vY?=;6#s`VNj8Ul<)2%v+~npXifEfHn{B7_ -Qij)=3~Iy-OI0^%iVtchaJ_xxK0baJIJONh`KQq~>o^y=RxkD;+vdnhW~94LY^!4kLx)7GnGxTsq5NQ -8S!qB2qTgsuZ^33sdMp=IBqu9j|t@FaDMQ92>0PY8u1dgWsZ?Y&(=Ve3XCWv_0Fic&~faxpj(jDS(H# -I+>4dr0!>btXomkq~)KNzZe51Iy8Io<ZxAA?paP<1SX{-l;hZpeiwHAG`qdeK|r -4y1?Mbi+SilyCk{AS>It?v7y>(bYX_=$Zl81qIA}Phj3N-#-QGCa~xl{6o^iy`f6(gKbok<)bYICdbSy6EzQ!6w;=;F*d-8Dv0a`EHe>YH>FB}Z2-sI?z}tk1abBD;$40>yUuTJXTP!Yu0?^`RQXV7%Mp#4_-SP16wZF$Wqld5kUXWP_#H#*40U0T1U(e@U7CC@ -wI9PpZ0cQ}qC6=w?kWb_vCE%Mp-v+s9<7z3A{^YGz$JA#yOAmCxNjy2HqPGnR*^Gg(jb;t4s>`jupcf -ujAa@nfln=EdP92AFeh`T7K<;Uk53N|+0gMB0oLSfU||ut%y}YgH^n%GLpx=<>#ODR?t%J<{4*aRi@Zs!3~NA -bAc*H_ekrb8TLhE@!EUNY20MJ&^I*sZK|<(!JexwZ*Wk6a~HvujW`-5U8Ml}_IE0mko?KwfZTc6HC(0 -qMj3{MU)U;Kg>Z)Oz(;lfGTmDlnHdQy{m?F%?Z>{_>?Q_fkl&%UpKbnW3aBqr>DGW;y~GTOrOSPTP*p`*il*ssrWp@Y0>`I54;ZaBF8@a%FRGb#h~6b1rasjg!xc8!-&V@ADL5dfCv_4-nX%wuOc6(o0Vv7-y`>ia -NHDoTWqQyVrJ-`O~ILeHlFR_xof??l~MG4Uzl-=okYhc%Uw=;V~hby~8zpAxTZsmxGa_(y!=kU=_a~G -^2zQcPLlwK6{U%yeCY?nq)Q&(`@rQ;oNhcn$j@q3l-h;Uhc;kLN7PDiWnfzxz==;a`l52QC)g9B~7gT#5S-+(cw -dC-(ISkpIJAq8>24P0im4ns}HtGnXZOTL?R4u?rUOR-*uabQlwS^Hc&4HT;P=Fc<+`g5SXtm5}6nd+W -+r*j%x4l|{qV^U1ku@6-Z;DtA$Whxhl;w?+B0B=)Ozvt3@pkl^j2oQLRUX|AJLdb|6p^0N`FYPBeJA+ -1)EsPSK!8R(Xdv@RgMUl1vwwncIVokxYK``sP7O?n`Y -Pvqu}`M(>$BmQrn*^vfTmbrHKNoz#jK5nIC_6{rk?;Eb(z65*OX7+x|9t}2uA$fP>xj1!(cNVoGFPV2 -%3XDIYL&tj@`s(WAI$i&A`72wodBE^5Y?2{$#I%iwyr@S)gi+mtl7GVafY%lK ->%%wZ?{k)>WH3!v{&(n=<_Oq=l6=LNZ4W5~4PNM`YffLZGmKt|n_|kcG> -u`K+$1UecOy>Xmc3xZg%}H -C-QGl|vJ6kNmU=aj?fSgfw(kJ&)Du5J8w4VKXCoPXDMNLUGQN6w`^oAFyu6x5SyVkft1d;zqEAU9A)5 -?NLGp%xob+JJ+2>6L3Qyx*NO_k?hK%}~IW#z`81&hD0o3Ylu^l^qh-k-ts*~P%I@C3sMsy!%Yn?9s&r -B*FLc(^aar+!U>THUhbhVRE=U-vp{)J=`jq%S=t=LaFApCQc_YqkbbW1=o|c5Nz5A2vDP6ku5i#@1R0 -$2_hwjr%=K6NGlkxWJb7Q>FzmiyUR`xTqC%{ILRQPX>%rabRw4sN1NVDo5Vk)qB_>r^da;R`OdE4dW` -j3r0&>h?P2;0!gmTp1b)VAB)b`i0BhT!~IrjMD&zuhKmr#M-mcy9-J$fl -Boco89Rz)DqDKq{nZ)KB%x8T)i~eX(R02h3{4i{cou2bc*Q-I!%eJ+_)=@wz=U@`589JIrHL9~J~Q(5 -RAD7id)B!SG{c3~D5G(ysFee_MHln_5D^A_r88BMA$ppPLAn*$S8GSCb|89P~GNz8r7 -sLC<&Fe_w;_DkrHFJ|kQ#-bX<=NDO?{S}ad>>W7$t5?ocxx&kKHyazU5w$;fl+iFi}AaKZ$b1rgot{5 -P!WnO>BWifah4+MB;Ily^#{^FP)h> -@6aWAK2mt$eR#VBC0E@{C002rS001EX003}la4%nJZggdGZeeUMb#!TLb1!psVsLVAV`X!5E^v9B8f$ -OcIP$xH1y98y_MIc#1BX2X=L5P;wj11T6QpSuhiu@|5*>3RlUh=;8wCCDH!~z9QdW}g8fa~a!!`Ykvp1*rLr&q7lg -ZuG@R~27X!abcg!t_r+O53 -gjw@K%$cr*T6(ZCEaGz^h-Qc5T-gR0$CvLr37i?DM-jA*tZeQD8gP6(yZY|p{n2kUoSUz}c?uq(0EJHb8;@qS -|ajZmyKn)Csz*Tlhh2x69M>kLRanR{ -)$Hf8{5Gt)*DAEx#Ab!V#SQnFGlaHcD*HBDQX4?D#+N3n$Vn%beL7!5hJN48ip3S5Y*10_X`Z9o_WYH -@8kuAUs$`t?-VJ)5e>PDDDZM0vQ2>Z3HiwUMHBWRE0?1NE;#1s(N7eO7B5&twD8wR>X0ExZx@9KtbEm -A`K4srohXb$j*9>h9?5oXatBAe$P4}QCX30dBrZ8jWBy@!Q1Glz;t66oU0dq{xlWv{*DSk8PT*xB|u) -=Age8Jmhs9o);ZZ4(7qkfHoQ)?{5>HHWRUZAD^z1iBc-%;#UE02zrsDyq53`Zuz`Un-~c5r8gNDAm#r -RH0pWx^sxi}ijj@E#j1}YxU1|kWX4{P0ShJpb)Ra*)RNR?K{+!j)@V#5ud(d3LxuK^Qo^H|9CfQeEIi%bh;T -O%AjrF6qxw1p#q@QnelBJZdwID!>6pT$;?u`K^pOEv8bh%!|ETdNx}MRB3w%*M>S7uJl-HrbgN^}tE# -{mk2(5j(-FKW1~?T55S$?r5rIVd18qSOw(Xl`J0Zbbo)Uv+#u;ozjVaTtwKm2R85ZI%M -<#!0@|hA{W+v0d6O_T@>J$sH8E2PY7Kq2Ehy#b?&vOb(C`?Pas+{N5ckccJH`2D9D?cz*xizqV(bJ75 -iCG1gGPmt&6}F+wrOuNRjMgK+VHI*7PnyTso`X+B;b2o#qw$fELg>HVBt;1q7O%mLdVP5D(m+L<(#m`pcNmxzoc-TAwRVLDwAx^vOPBnzvZ0*%gJt!&~^N(4@8aOOM -M?9vJuJ9zuZ8Kx=o39dJ(pGj!8#BsGT%gMS#8qaz!h&PXqL=vZ5K;)^UV{(k;H@iGYW#Q3?;?a=GTR6 -6KQg)onvXAn3Je18P#y5-ZOUIIpqs5dJzUffIFOJ%R)%dY`}h{vw2MjETKL9bst+25B%0^c4XiL280; -EmR~?L5lcJ&%XQc7G9r$EfH4PPw}!P3|gPT2|$717OYU}&K50Vn=Fh0q;0lHM!66@-`ecKp;vLVASPT@Bg^Grz>%I25FZCIC8FlQN -lm8?4-wZcxb46hV}8*MyKKl>u2|B%OA>> -xNwQlr7N(jfvdZl!`cqV$wI%x*tqplGL$>rh?SzP(NpJsfPzc5nl#w{Mh2p}Hm!&?Fi^+@CIY8(*=XG -*l=M?h8RBLM>}D`N+8z&eTDwV5-z9IBS#k_&Avk9AhC;Xb2KTLn16TvMP@{DNI5CE2>U?866&adq|UCcF0QpQXd5Gki$AKs+aKIA}27I|xYu!68#Kfi3%_7J%N5rlkNTw+-^3!4L>8u;Dw=2MWh9;0|+V;50%QAAv -$>AZ`f?1OFu0Zd_O%W`$(+$!62{nW-wL*;T6)9xpVj-&iP)V~noGhnxebwatC<$qQXSOlN8O3zbTr@6 -;}D>k{(pG=Z3PLs6_(P|<0rlOU*dJw>oN9oubf%tH@^XGyHUmo$U<WUV?$>9>cu9KE3#GjiM$K>dPuW(|QgYv`lFi=q0;=e}k8paqJ>n -@#5ZA+-`g{&3Z6z4s>0@&cdw;=4N=;>jW4b6q4xy4?MOoP$5HgnraO{nVVaFgH;^{Eb&t)75?qNEIwSVH?|xSNbAtpn?8m+*&j+JX!7lo?4PBv0nEoFDi(j)1DK^iptk&P$|ww%3UnMeVD -Z9Nmy(3cI)JYsWg>7w*4nGonOSv=2$1-TEs$hWZHEg(G_gEKfsPz4kuIwCrtGKUoX}$^`|X=Vc9P#^I -@tN!TXJ$i4l<8Zbc~}pi~d3e-7dr0?!7RK2RizBYi$RXB4dukLq<8ZV`ve0VjS(Cr&~Yen{ebE}IBw4 -b4bZ4FG|kNIQr8#Ht&b&cRf$7y4|p=J-lWDL(p9#8%Bo;Ab7EtWbH=wJb01{?LzCZvHqPoI((Mub`33{)yD22a4Yn5zh9yT3 -Zh7Bw$Z0z|sv}YZp9H_xrI%@Fy>#2o}&rt)I5yfNlnFKRgZg}bD_(7z72TEEVzC(vcUPl_%u2&uNl_z -|}n{v;_ARTzu0XL9#GtCGjd+xe%xC1+atSd*Jq6JIIfRHV^+24WdJyWVZm)V?QUXRQl{9uZzL~NC)A4 -`hz5k#S5dJu!lQ~bE`{>!&E!%w(+Siq0N97zykB}4d--7b1N9EXah*aPc2+%P+Mb66p`1HqY7U{c?6f -2}bcQXeUAiQ#J0JZN70QuTsBU?_*;J_;8V85Aj?dVga=K=3V$MmKq -$yEdV}YrQlYZ!eNtz@@|P=C@6aWAK2mt$eR#T5fn -iJaz008bC0018V003}la4%nJZggdGZeeUMb#!TLb1!sdZE#;?X>u-bdBs`lliRit|E|9RktY*sO0jj@ -^l?*Va`h?kO>LiRU*gVOJRFDwCFCfA1;D#gJ@((8-37phNL`zmwB?thh{a+bzkT84zE;{Yp>?HZ(*2N -DXsc9d!iQ3Ax3b!XpUNaPx4ZDuihW(kQp^gi_AFDC6%V$Q8|K&757NH1JiCp<;+|K0E415S4>j-(#OK -u^W*0KEF}nmYxK%o4SGAR@;$_UP54HH7!>8{m_%d@GYFQ_#1kM_0lE%u?BztMHz*AJsK4yAvwGguzDk -C&o1+9{;urKIal%^Hmi!@!#X6ZFh_|u|!dOeTXMm1HwnqPyu7gCE1L_5rZboPY(O;)*Ksvuh^&Gmuho --b0RxkjZ-GMRqP+I?L6DOFDlE&0&Q{uKMBfW4~ax7~2gn$SDTea)aVmiyT?s_iD8f@%H$Rp~$0_ -Q;#W~Jcp+SK66aSb~pE}*V=()jItUvdP12N(-!5aR@Sv#TqBL9K-m6}TH9i%EbxBc43z+*&O+jTNRd9 -Q7Ri)cZnZjv;^vvr47~;3=PC+lxzt(tyo7k3D43<=YYZ5Tg|nP>8S<>NALFWn>C(iG|bXB^;vio;qam -ruKLW&jlT3NSG--W?tfn^H^Keq|35CZ}DJD2HwMj5<~w%e7rKwc%I3LgK_UD499h7kfP&(>v|ar;2m; -PeB_xk*V)}*VvZd}3vk>MaNq4~6FRKUQSy1W7Wyh*hI3cDfbbh$8h`epS1y|xyOg9=$NLT+u&K8vSRz -O6-9zd*`w+ho+s?GVE0fVg${L)BdqFDX3};BhlW)cRo=C3RAjvgewDgUMxXsAujyb}z=tWht} -qJjCD?`J;~-5_K{FkYN{PY>uSd!w=>W$y{mb_Fn_?6gJ9M?$JKX)FP9`p*50$NEbTP1%z&6ximWS_DlK>4UEU0qLq~|&MvP|MeYH@t$D&hZgMLjZ5+FRR$C(+DKH9h@0w<`{=K(JGOtdzlA|OTPmiNaJ@F^S^k`DV47@O~S1A(VvNIKhqz-8e}zR636_ErPp*odOh -Ng>w|p{ZC4&cY-eX~uJ!#hz1t1FO!U#R7+~aNHfb@`qizXA32eq)<1~X2U2@bmCl31^^WzNCvcmc@ku --O?B!6w?grGMx@j2ipx~+hRMPBF!%VuAN@A`3415Njl3i(N+=tVaj34$D9A(RVBiG9V -EPi6O`xj{t(PUE^*EWszLeFi$2WMb-_jlu-#}S`BRx>yoPp=i@ZD#VX<1tX)2YHV2(6Z?V5}z%X>a!`AeK6s2+uDoMeBS<9h@>TOl7-#k`jF@&kI~<8r>}P~a|70C<~ -b8+m_BHeYA#b2E?W;Kgy|45K+<;>0)$etRSK$~j`v@u3Ao0AR~$al*G^JUn*UnGo9pECyRJ4#cssCh8 -K;VhCiXM5hy7G4;vYj{H3Tk!RqdL1!Kfek~qp)SX`D6x=;?f*pYwZC*7DswcPP#)stJ@iZrBCm!d0f&AfBwLh3D?~E9Yt?Lb_zhAAHE4c6hjXwma>qcTDe%HF@vHUi -HpPyz(NK1o=FsP%R=F$+NDmcV0fTF>!Af#K{Zdwif}dsX@8O=fn>dKj9R2yl@(YqpR#4Ss&i2CcoLe{ -8y;z@LT)z4?efIqP&0Fltm!Fc==f8RS+u!~E%Rl__Pk%mjg%!5|^fyjnC}>BAaAk#w;nam!3*uGFF44 -eF*3taeKm0K7lF+E(cjv9#pV0nL9PVj;>%`uQ3NK0owGk%DO32sv+EuLfCI!cxqe~{Ymo82OU3Luaj) -`T`l~IHiFSnqK-R*IjE_vo>#Ndt|@}Sb;3NmRqkn0Aw&dj+DGrlWvJr*&PBu50vu;t-4)I7U|0B|z02 -=TIkLdRYoz&;g00_p|Y2u%u3L*05wf_5*auf&j)-0Xy3NStOa- -Z@}erbc%YX-e7w`c{JdH4>&Fvz!k=4wq#ESgp2XKREc|XJ)-;5oc#(ENq$taidwN2@DV9){BKSF%bER`HTl -nKPqk9kWrJR$WnjX0V^y1=76V5NjOu!bqa;I-Fgd7(L&y~F_|71M4WlUlAJ`6EpVZN?N5coE7g1Yw^> -$750ZO5Gn7Tc92IWy!Fm(INC(_>aPmsI7aQR`%BjO>U|2?8d=p|XN|8e>DdfGEow*G_5j8i@CSUr3;u --^R>Xw2~m`-aL%0j@6aWAK2mt$eR#QBwb)5_f007D& -001BW003}la4%nJZggdGZeeUMb#!TLb1!vnaA9L>X>MmOaCz-oZFAhV5&nL^0;R?YS)D1%?${l*a>i} -sjHZe0vE&(#9Sw~mad(J#1Pg$YPHFnvySo72d9sr9gF92s#5w|t#bRHd1rr3pJ1cW(r7%p3oLiAGuG6 -)=5+5)!QQy)o;DHeazS%E7U*vjK(J!?;j7eGINS&r^JQy?ASW --<4i27RTv=mjmj!Crh!c?<5yhfX!;=P2(!mt7x|Vgm1b|YcC8BbB!05rl{3HH@cRkcGs`{YB<^exZ#d?z3R)}kh3M8>YyGF=1RmC7N+*qUEq7+A#&kV{x0P%^G%zKt>bkQ -psJhLDThR*xbYZr<@9H>8EFbU_Qekfwy_PIAFB*atl-EV*4DL7Zxm7~}0Dyj(cBaT2z|KXDGjvJ8WRA -ZmX9_+#L9l1l~?a1ghI7nzS1e2A_$M+}#;$cP>LYj*&7Mhr|%t5DZ|CGm@n8n*s)w3N<;%;xOSNhETV -sk8B&3xTe)(ufi9N2H#EAuqfp*W8TsXpWlHMM@-E-DwR -&o0x<$7V(^kE1@A2C}|ozqAFlnwE$5>YL(vN7%%GV9o5d0s?A0fNvXwB-hgRe!8)42;G&(px=gDAM?4 -2r7z<0LeJE%OV0&Q>^3B-W~&}MSlf!@G64B7jN{){z^Ebj+{d(TB -67L1QE5%(l;?)SkG*~89ljU6G*fdq?h9IH9$^!4V>M3!R3uni6RQ%K;Z<%ENUkC&^U45?NGppWG)PUY -L)7~CfnN$nPyn_zm{hrSdZj>eoCJAxWrRLhX;qa6*GlJ^3#D`o`?CF;$Yu41s)R5CI~19^Hmrm?Te@< -Ssnqng21U{0(4~d&k$U#*8T*R;dsqej#U81{!^5bq5&&Kj30Xany`xG%Fh@0%8F&-w25f^Q0B9UaNVv -6{J%7#)pZW?3{zZhCUbR?NIoD1%IKYc-qYgM0fD1#Q2vp;j4;A!H*Rm)}*l;VbqR>f3V6e<;IJ5qJFNZ=c~X?n@j?t6XZ26@9-ksCp%h4vvSdL!u -}}4$pJK*dIk=}tU_k7A;sa$~i7bLN?eK+NgJ<@v -vJsnw&@Sk^=5g`Z;T)o6=8Q};$FfM~iJXfEwFd``mynhbt}nqu*g_yUM_&F|F=)~XCkO_Fck+49HPSU -NR-lf`U|p$wymL5+5EmCzv)b5*dlqW(YFMk>B??6DC8n8D6pcebQPm3%BO1i#se#s`{TO27CHfKZm}B -8o*D&_V4z_YM0)(~==&uE`P4C4AEZaHEMR&x3PI{6>a -%NBjB6|Mh&~=(jJemz`X?oeM$r&TMbx7OCLLmULyqB)$`|day4PbpmU|Dn_EOtVszH4@T{FKcZvL^k6!(P%_-X~eh*4E-Tuu>XeJj&ClDHdjR>l`V@{6q!Ml -IV212(;GuDJ672qdod{S-KP^iLo4;6>BIu~Z+z%I_N^+uwwU7~3GpnX^vJK2%qWiVbfA7hiwkL8yTNk -Q@dU4QT@{yzTp)#^>M>;MZ2YwW+y#wnVaM+6gE^+)C$ApS^uoXUP?mIA1MCr8L%v|(Lzvx{<@6GK++7 -5qnR1KLsl`tr<_@=Pygl?EppG3PEXz2}l@Z~19X|5T(mvq>jf}q@7u8-dl9zI0BW7KKAy__Nf_D~MSp -7STEir<6`(LWH4%-5gFSyoz3B?xElQ&TewNmIX>LjLkb?ejB9W)k+TKDomG|)#d-Zt60IrJ}uCOIu`A -K!~r@7iXP)6M;4f>Uuld8&GOH^usUplhxRuu{Lk1%KxS`d}t3`VjQr&^Dn%qBG%*rfWUO1s|AT-mlXI^=pwG0NYBKgU3&wJ(*;Jj#^jT#EBBRKi=TD4{|$Va#bij#z4N%oY -Wr|5Q@OqE}75N22+F9RcRZyFi`lR;DG~_T4hy=)k2yb970oyR4!$T}tUB@1 -KS7zM04+d63KDT^R5{6w?IaTM>fkuQSuvg}sZKCZ0=dX}MKMzjq=$(P^@Ev!GJ8l!ZPg@4M_l7%k_S> -$GcQi?O+F^@S^7PX6NX?~S+bGdc|Gtuje5aeZT2X(arVl^e%~=x#i+UNK&Od*VvoH%egDJB5rj~Y<>H(1#D=eA$_zjcZXs=%mc(cB -6q-!T{?pBf^W`RkhtPKxx#Af{KmNZ4514;Zb#iQTE^v8$Ro`#iHVl6EU%{y_HgfT`*&aFrDKK0&AV84-U9({5f`j6tYi&BwmE;@_!~XY?vV -1?fV1CF+l=$%@`B5?3fqVoCg_%|%dgH##yU|-~p6WsSfZy0WejPEyqVu$cxF6UH@{PM88gkke(_!7xc -F=J?eYm-OfA{cfQ4|~O;0wkzBd$Z+;}%O^Ho~ET%XbHy&un;X3gFLlpc(~^7|scm$t<78IC=-S7x1R~{Q9xBv_oH3nFEvS*O7!rDeEHx{+$9SrPA+;+%s|#IypfkUUkM|1NR?4)BvA)oKAM -eT6jX(>J5T^3S`H~3%7w@KTia|jude-UFQnNP@jcL_G@lUEvwp*mn!zbsbIz%bK&M^F;rI~cvR}1LW?= -(WI9gwqoNCc-43=mil97Xn$Pk>TH@;9G+=l`ixkwDf9a2PGMi$rE2hV=_2n*_s)s+eUbXp=9HpI6t{p -!iqYo^SCvW1uK6uW+kSm0iW9N}M#lPubhe=CZ -*!$a6&S&sAAd5^aq1PUTx0W$_Da0TADY(d%953L>OlcDke$#fupS>r4Y>j-Ka^x;k{;BTV>6-vKD1>p -4d1r-_gV>t@gVnKP88>rV)B!HnIcxqKjq>(t)@hm9U!=R)?G@ZPcrb25yO0!~`Uk0H#5J0ewQv`@me^{6Aen-G`5ryPW7q -F4Xd>&>IQxYNIfT8Gw{wZ#~@bOQ6P)h>@6aWAK2mt$eR#RGmvL_-1004sx001EX003}la4%nJZggdGZeeUMb#!T -Lb1!yja&&cJY-MhCE^v8`S8Z?GMhyP0U%{y#A`iCO4I3I1bB7>vi(*+DG%2>BE7Y?@*|*Af!rjTPvF* -Q)yt7{HC}{%(v1A^PR4}OknmLmReWO{55eLU#>C#WI>jkb-EN5v)W)OKZ8((e|LD?2$!$Z$e+7iOABi=R -qnf3j{F;b_y?;IH27c%}!cOPe2;ykv3n0K#LVYo7H&%A_gH%Le;h!G+{ -~iY?DLL`{3?UMtjEPpxgCi_dC -rqvXdaO?DMEmhJ7Nf7o?5`ui#nSIDA5aOb8Yu&8YUGEf{n8tC~8gp6hr#^^fBP%zP3fLQLbkWegqS^` -4r{3AlkT%#r#do(aX6Gp+o^iFTADut_?&+j6_BOgkm7HWVC3@$&ATZ}~yX&-EJJ@a%`= -^ali_Q&;m7InpXXetmR{Rjxw7eygMXH~=|notzYPMDwqB?PMTL(6tLEUOcl?s}Dc-_yh1jvV%;GW6Jx -@4tHy;*wF40%9u^#-7!r?3$Qnn+6I6)wEYC;+0=;1o+1`&NC7Kx@JfM}rLB<5N?3nnD=I6yjr7OXSLm+{rDbaKY*Ei$LmAGTOnNo;xSbJTJ&bfI;`795#iO(M_GienBBMna?5i^o&O}#Ya7Oei4{Ft_ -NQ~>p93+=~*~_+%Kv7>|&B78Won>*AGTENbCJz4cJJQE6dc8Ial&@m~)j2bZ7`-2AO1oi9BTRUG->X; -?VOWlsaxi_K?v4$O>r&lZT#o&rI59v9$a*@o8jU*=Yw2ko86M&f0KBhgpZv@ZM?n_Q8qjW={8I9q7ZqivBpErtRlrPrLp`^1Ajjt!}Iy -~PhiJFa&R}Gw`aB^2LJ#Q7ytkq0001RX>c!JX>N37a&BR4FLiWjY;!MnXk}$=E^v9RSX -*z~HWYsMui#V+l>kL;`sg7Ax-Hv^wd>Gz9k3t}XoKXp;JLiy;MA`0Tm=PeBMV`xdzH=d?D -Eg!Cx>gys?wi8Mw$W_eD)!K-JK1d5gEU+Auoa?WYgq}MEutt|EM!f4s&=!1Rg1N1YbI(gtE?7U^NnC` -#cTUGUD0u+h4Kr2rziW~>^jux-ux&Fld^YxSGn!~$PR7r@4x5e-?IA&eo2}8vaky~*=6~?Xv$WBw&IS -Xky&qKrTylDH*F&eUdew%-tbzS*m<|fmC$Xk3ZX}%w0YUATl}x&sz=09Hhr4fAR8l8!>deuEJSC~b%O -qEv;|(2e<8f5c9F&B>?q7@VR*?6Uo75Vzy0Ioo8R&u-n{<(#k-f^zq)2uEWS+H(`2z&V3YvxD<7_GBg -`xMEzZgJJjZ24#kFQ+jF%iw)g;RWgAc^t|H3-e-pdkXn=Lo2=ACA>e^-j4Eis%E-%ZQF4HDy>9A9JjM -do!Z=u0WqEC)cO$@5r?YMl-WmW06#0H3ne&Io;*TB4FmO9n3zcJZA3);7XgjvoLW+)lDVs27)vOF&JQ -kD>s$@nJLzK+OGSXIiKj{*QW%+KmtRQNr}98SDw9p`6hk3lI=S0-)kYgjHNC1xd;^6QR^c>_xw+MPnc -$0?p)oWtgDVl*;&Xt0d5XwQG99l*&TWxYXi^pmDGUKkLYvF+_TZJywwY9LK|w?ZqJq#31hw%z)#0IZ3 -A+WLY8B04onl!IBNxKj5AM#S4zx=y5@Dm?q~y;h%+JJXFi3YS~6Y8Gt3EFSB7^5;FP-nk9n`yxCcx^* -~|E^K}ajfoVAqiE5v7XYj;2kXRia!<@=3Y`N(O&JHAv&S$uz0(;|xNq{fYhAX2n#PK@Xd!>gbQF1$zY -s5!$oI9Xsf1rh=j006bUueuowR}uNpCY&=(_JM^96gCrcIo^%N&di?n?Mv=q69A;aRLjbkfFX4IT8lu -M24IpceFpSEv)7Z_930hB%A>U|!fp4RyoS{sAWekSc25&L3{vc-N{Ogru|z(XW#I^~?43s{xQKNVb4zJ_&~Ujly&P>v8(c$v -NyB>hffZ7c*|V7fK^QewTZmHJC3W>i)W4LS}xTgr0BnX>1X{#)xN55vk<=;kp_ET-6oY(~36 -QawV~s7>0ss0A>QgJm%6^xCkMU{5T|`xO|CHGAKMrSRtI?O4PuV3P`dSOlGdioTt~HLZ7I!7ZJ^Xlko -`G(t7hc<1;a=<|d+nQ#MmQ8LJ_L=z8k6ZV{4epYx;cB2{w5ecQ{REVX}R^f6=WI!&;KD2VGcucn>1Pw -47Rsq`KOW&i8hWeP`R_uVQD*0WPSe)^zS@KvOyqg -pK5F-xyhoadiiW9ysil7f=BV;0z^zhMj4(j;BXWuPY5?C0|y-*HO{-RY~zfXQ5Jnw-{DbiUTr6G6~A< -c*vMT92DXaYuCqTWtYla*gUcRLN5`v&5x}dC(0H;T=|p18`!HJz@zkxV`0UlGI<5(gMDD(;R&h#%BpH -Xz!m`k)t{OUEk1rUJm#9+gX7rHq`laT&M+dLHR;csxXuPygy$k3*Q95b<>ZRs87!&TX)QUAwT0=CuzT#jYjQhPI-miFZ_kbhWMKBhy2jQd@y;1;8wP -SMAv`Qd<>TFsXtfg>xrO6B@juGLI!hRNV^8=mU9ZM%esBB -nse}f$DCeKzMZPi+`{~>e)TRxrS?ff#tqRh0+}}##wTzCBrj97oVcGhXiqqYu?3#rQ7Uhkt|A*fZ*?W -)mvhJ0jK@M85j(Vc1oso>l5SVavtmXyg0&$mCMe2_E?Rx9x%3{NeFsag#`<{AC1Fa8`P5-8*E8}VxzZ -IdzDy2uHjvjY@ml$Fnotw=r-#yjM@GhMcv6L!aW;=nEY3P=jTG1>O(h_L -7%6BHox1K$Z;S4IN*#z4_c-2)g;{m>~b@ekHsq8FC88<>&19QWJjv$}wtxc&z6@><MtBUtcb -8d390EP69y;zVA~s;UFZq(ZrKi1Bn+8Vz@44w!p~Dbm?@6`1IBVqe7U|B;Rk_0ZcwR&IAa-N3YaECIw -!B3z#!yz|_L3B&VKJhRonF1dLCfhzA(8nhgO44HLQB+<7#9;PI^WfePf -C(7)TUk3<}-XVBc*I)FXWWv01*$6)rWBIw-Sz4{5v<7@6aWAK2mt$ -eR#QPhT(9B-001cq000{R003}la4%nJZggdGZeeUMc4KodVqtn=VR9~Td97AUkDNFVzW1-NSVapOhA3 -C0Uge=gyV58tMYFlIMwSOmW5w8xZMtXj>$l1V8oKFB4&eeWm+SG>SA|7qwDm;l{a&d3rm?ys{@K7c5p -nBIIG>Y$jTc%mnUnk8NWO`hMwo&M<++8eqW40&q7$wf&;BjynyY*M1Qu%HjAYN$FyCH3?fOS!b;X?Jnwqn{-YY5ht(I}Y0p1v|4H|%{%EbX -;pLWWN1Tg2_jf^-QmRm`NUg*dz# -QVMkdp2`x98PO6rvs*M}2YI(;Uh-0ewpskzMdz0jsGIuU?uDuX7muFW*@={4iKFA<@e7$v^c~N!Tx4M -q`@rD3c^pQDEEk96Z_b!Exk~nWOa{E^x=nfVS -toBlnc(HLmi7;&I%sfe%6Z5Js#-5a6ort8dtTy7%Ojd*z7N@Sb6Z4 -@9~Y%=|Am=53c^uGwk-6i?OF0a=KB-f!v1o}47N-;?}2u~3XG02hHgSwwIP+pEdc=ps9n!W{+F;<`jv--svTTTcG_ayN_@!Wx*gkWZ%%NMvkv66WwQK+4yzVVBi45q>fjC6=pWJMpI8{Crv7xRXC?~L*k$)3G&%6%auQg&O8j|8azz(dRkG7RWW)B2*U==-J6G1T`-HNQsF3~ -frK&I39s#Qil0|p%!)s|Jj4@MVhYNraAFS=j<4MpUL?;!;Gh1Ns52_ROdrrx$e)Gyl1t0&b6zcW=Nw! -6iA;e@PcUh9@+x!nuO9KQH000080Q-4XQ|r5509_RT0E|Td02lxO0B~t=FJ -EbHbY*gGVQepUV{=}ddCgpVbK5wQ|KFbi%S_5tB2jT>@Ag(#-bu!B9(A)x%J$A&mF+MP2}+nyq -=q1+sHWz-Uv~o_!G~mfDqD4`nu$aL4WQAFUv~qRt94$KEX@}SnJtc_{+X*_&C8|ziQiK>w?7MEzg0yF -&!KIhy3Y#nTP0L^loa`jmAf@iXQstZQk7%&VO`2R6UA`4nmogqJUR{_!Qx>|Lfxo~9fdoEJr243VfxnW|tVRb)xLm2oVxEAbU35lgjB`7Xp2j@eSiB82(xr=zO*y2vA;>It -p*7ZFB!N<;lNkqS1VP%y_R&&ncCZBPG*!RKXFF5hKIKBiw6VwIQTeO|uKt1PBPbzc0;Q`vs8(VSHJ_s -@E)Gd-4mmdGroC5B}l!u)>~Q5hC_UWPc~un_BYLSK-g+7nkooybmwV-<?CS^f6q<=de}d!MfPBzZY`{cQ60+u# -5C?2qp}Xbpp(ePsTnh$^5G`*?Y2L8?V>5I`1rtzgHHmz36YL29jH4pvkPt)1$)6vW{zgi-Pu**b&0+O|{lgHtvM -q+8k0sCjAN(PD#8ngw{z->c^r)yYuwGxGlShVB?k3a{R${ANI%S%?~Y>xcOl1lM;DolU&3#doFRWz_N -#8UZ1u;a7Y)UfNA`X`T -uFTt!Q<5(=JbLBJ1^db%vjwOL^6nd&}NYT12+qjnx8AZq5>$gmxhc>|ilN{|WM`(wpKwvk1ip#rfDFC -^}qVq|H_$R@DgpnbHLV0x`o(Y$8UX(N8q^={3l&}1r=0^c+}Ir;t>wFX-y%Fqmi%l6P1y?S@yjTwFY< --^7QgjW~m=boM@Y$vbMGUUYqtmg*#_xAkdEANQjz5wYgWwa9IGLLP0239YegEhbo*irfLr4ayhir4Vt -c2+aVNWwL=yFIGuNEOP}sw$BWL<@E+`T~~3S0YWuXo!g_I5+UWLlTCcaLll8ZoKO$!_H`#(f@_cE8jX -rAO0JSY63DLopM?RxSsmGvw+t;1$fZuNaMj;&~?f(lyL}vfu}xBdfX*DYSVp0@&VSbK%vkSfG;5B2}l -DzBk_Qa<4(WrQbtR*5**fy8@C11pmN+Plpz80WN19WC>W}!sdumGQR^|tB~7qEBOjpyz@cD%^PKF>5a -~zcH8?Bz0xb<^^MYs5^1mCGvE><=u!zHk>mhha=yVX2TWdvX=Vd -ASql!dN9tk&huY%mV14K!*Yucm*2384X{$tFkrfr;tN5~%UBde!yEJ|AhFk;%M$cpli%?F -wEo+=HLzt}A#jKNZs|-*@Q`NPhC@G$=pY6-*zsZ;-CJZd-VD_i?@NAcQkqiuX@C9rwp3V#9<*DDA ->9_89MLXIEUY}_$|hzgnSd+=LLm%wo~^+tm}3R+yJBI`6|v)iMt%Ap+Jp;0ZvNcvQ}Vlt79R%xTw(yc -LB*~2P^I1r6z26GvsG1W>m8iRua2gsb)TH=RX4)BS*l$k7nMr>P9sA=>PH5V*3XfUeAR)Wn!1_TXTUV -@0YEY(ZZlpzX|WnSzMe*%JsZ-LX2IMJKJK4t%ih!-6xh(3l{fF4UyRsmpLmB9lg8`K0u)Tjs|tiA}6O -Z#?G4f4?F%&8|A5P=GBk9?yh|Ne)w{P!QO&S&fwL<8h#(92VVoyrWhO>a8{qsI4d$>HnZ92iTx=TOt1 -DwAZ8lmKtUVSCirAfyN^AjC>54rWB=l}dLwAekenvo4JH$TjfGZfLV9=$mk+XzSXF8_*Qw*;1>wvOqGiHT@nGdl)Svf^Lq*Xi^26m*t~{mj!!P-d -o2{t>tj%o7Nb*BtniQO>pmkO3g;BQsXUld1uZRX`CUX -_WjYfwelODq4HckFlk=P$({o?}W4XVkUj$}Qyyh*eE1Z7D)+l@o12wGmO%ypRi-O@9Z8_@mKg#D1;j@ -Icu{bt@P|52p}r`pqyc)P=324)D=34O(}fwpJs&edNuYMm>YCJCnK}f>OW})VVMbrA1NutbRKb8-h?U -Q|-q#+I}OmV7FacuGQQ|2TQ&Ygxb<{=NRW7iDzim5laA*8O9{YD^Ly0wiF-;7JldjIWdITVea)aidv{9=9fWnD}aJD(@ub{Hd#x3sel2*hMacHf#Jc!?Mx?;&~x|RD;X0htoO9>J3D@?j>p=9<7{iy(NZ6H;~uqM_H5KMjV -K3gx3|wq9cH<__u%70&mFULE;@32G;%%3SAPN29wi|Hzr-*^*}q(T-QZ@EF@-O -beM1u)s6}C&8exMpthdcAa{!&#%tjh7_c`fx!Lrqieo*vo1KHQE`9{r}wR1oBLog7Bnp{dRQH#>e -XgHnHSjdgAq2D)%D)p2(&L1Uxe@JPBnA&$-D%jx+kWCKy1c6YMWdcjS9ENlX!01yS;8F)Sf5c8tX$DO -Kltf2wE#>q7gfpcrYFk@uX^Pz)Ndj#!FP!wEF*Pzva^E`xkcO;$tR5>AdCF7PT4g%7Yw3D%ZJch?#%^ -|Z#Qt&0{O4nK^0z@y#bK>6Wx%jvjz$vD~>5t*Uwskkthlo)c~{iMVy;7^+myZWy#FlS2(R9qTYd7lN_VBJEI -KtAgR-;~df*lSrRn}CWaN;2nj&?T6oGA8$SPe6DwW`FPDK@%ln5>qy -{SSqtZ?_G1A;V=rh?u3V)&Nbo*fqN6gb@7vp-V>5!lO`5flvjAdOt{Iw3#_TAp@ -X_jLA41`0W*=qq>L=@DgjKIY12~`E=p||K7SD`6FTj_#v14e#X&=L___cn>>&h)jB_@^Mx^F20!hm& -$qf^g>t_uxFjc~LSuDN|R0(yhZa}OW(Mu>NPdSsK6Y$;A$dOpr1cyD?F;Lk -6a_ywGI2%#XDvOTLJ&DO---cR!_#^{H3tw4Z5MmC7F)(aHm -ExfN}{F0iu6WF^s-jDk+T_muPE43A>NdhlRPeVL*&DRT)Z_z1r0Bc}Lp9syUtXz5r+6|F;ic(T4-VAz -H|S_sg#?JXy!#H&^dGbs`pkmi&d7k4~6wkP5M9mUs*YP;2ZZ@3Jz#A9w4lOTxTYZwhn8CXx8R|}`9uK-TkVj-3WBLKS -#P(6&57E}EVi+%P6@5Ef;MUGET{`#N)@$8w4d~ls(Sat~lMb}1`V?g?%*o`5^;B*I6-;~ucQXdW>fk0 -%jiQDLcTt}T`Cb5JWDiF*i#i=;X^^8sM_&VuEYR<||K_}XY=}c_NCZ6`lJnh`u#whVQwnb4RvZKI&uY -rRnH#caKrtO6O$el>*4Q+0?PAdwt^{P=>eebC^f~lOeTI(27H@luSeQ))o%|+w>tbEg@=`p))cE_-yw -?U&W-f(xwL(H!)FcC^`Z;d%eMcX!Wn++@nqg~5Sx(ta7G*>5akWse}`SCGbiVP_Ybc$thQTU@^&>ppf -YtOi5EEw54l=A$uqj&A`0G$I?wx15%g9+OBspFYU%(Tb#&}CRd9+YJW6E^tkj@txwp2$u;ckQN8)AY9 -jFKjD-9CfbvUcl-uJZcfX_GJ$PPSCKkm0fz4RLfGq!$JHE&-aHGcOo3k$PUV(Q|e_7u5=@M3B&ztv!6 -TA5H&f7i15?7xk0huK$-AQL@&*svlu+;r_TlS?XAAfiEN&09ZIzLGa4R!Cav^o5PtrvX1% -b1XKh>aiu9+d=xNja&p#Z&svI@mmyrVXHnKoVN>pnU?^2{MoH4}%n9A+gTm12!2Q{U120wK2owBIq_X?-2t0l5>q5a@*^qAvT3z(tk7yBx2- -T1%t8iQt+!jLw!^Eoi;d9@bB7kGtf#(`)-Bna)=8~Iy$CF=Y;!J^1B0|2?hEkib`bhelsCF*gesTpS7 -n{3Ye5V@8Kcddiq6T}=8d$)pyC%sg+t%X1Tj8pQrERxvjV{2BW;Egt;6*UxVY$xKqXg~1o -#sfJ6WA3t-+{5{3I@~}T@Qlkg5mQHb|F%B>EC}3yf=Y2&^A()eAovK{se{NMr+4uLvEne{xxvpj>|>1 -SP)iV*!2Kfz6JNn(^AfrXn=F%S`@WF_kROZbViCgof?p>yiQ_AO-Z#){Cg?QK*|T2H)Eg{wN?=5wy2; -1i`3vTJ*eoLjjSPAPDqt=?F`AD(5$UaDGrmRxQgV_HTroORvK`XE`#Z?VHgR%78WpcJ1)%Drub*YtTP -$A`raq-Exz2X#!U>k&M0<^e0Pgc&jwskaRvL-LcPJP1M9q+CBt-BO)`?U(gEpJgu37GH3lo*YsdTi#h -^uUIl|vBq}j+GaL%RL=wpC=U~Uius>^E2Uf36wIgP(j=r-W$t=|WsmDS4V5*N@gvAac{Gp@`DBq -S8JumMpotQo3tP;lkVJ#D^?|FndJZ#H6Y26Llwn^Dt-G)Td%!Uek`TCnLIcFA?5nh8AliG**OG+nj=# -f6`LaXX^4Tv(0Y{}-BEVwiw`z(bN10uTG1(1iPVYDiyTTroPl-ws76SmjNsp_XTMO*HTbdW}z#~s3n!O}PH{fV%ghTye+k3DfXF8qEdFx{uGlf5 -uh6lrp!ySijbyB_Fc5A-lu>9je7npyd}2?>gNBdcnA;!V+%KGhJUFUZb`sEZeBQ<;GjgrRBt4^JC(s=ukFI&ER4*uXvu* -BhHt1zt5!z?<9>1Pl|7nB;)^gVJBBXRB2b<16EW*Z2@}^8`Lw^HEY03#-bLA(PTw+JgXV&*MK^jb{SG -^AShtNk>sGkBhJC~pQzxk{{+$=g;Pf^Lh-qinDMd5zk%h5g2cYzID9Tn -V*5(?QMl;j%T6)xb!N#z&gq;v`Y7ofZ+j{t>LO3q2gWyujR(RZe(S<|t<8m~@%2KAV&wXP?i{ZQ}40a -38F!-1P1>n|CWm0EpG7a2U#;*j_D`gmN(CcGBm2!aPH^^pg%Xkz2Ve4?g9N9o%GZl+j&hhhHGf|k)$Z -wJIM^aQ_@pQ@tciJG2Q8R@{#VqbVjIJW64f0>$cPNy@k?_xR<7oC9Tsdk=TlazYAo6|HhOoh^lwM|Ex -PMUda>mY&remI}Zm18a+MY6Lje(DW#R@=qiG_?4j;-4iHGNOmJYiY2}=kyJ#CV@oaX}r9nNPl=bHkWX -pV1GWu&UTLBxzLn6%cG5n9<6wAq`hg`i`MKmcIL&Q$Hdr6Ozsyd}C9J#cc`%Y<9HL -9WbD)u-K=HDPCE2MG4BbA5FCWRdf;AMPvp`%ZL5jo3EyS7Jma!O9KQH000080Q-4XQ){t}Psu -UXv?{XOhd?B_#_*zk6r)K`tM5(q4aSedPl8QWg6E{#moQJ -v=!aBRQ}Sb3rM#folw8$i#wL1YnU-Oc78MgP71mXn$uLi4Z1?&T0{^~ClY|vl?7o_+?@21ljP9e7RvS -H%$|&ZAp&vnK1?Qt+5bX&HRW^PCpR8? -lyClG8f|e5jEzBHLJoTP7qZ0=TUN&$@EH0bVYO71#WIWI3mqq41Q&KpAu(F3Zkh~#`k`bU%^C*qCB{Bj6dPMg5lSG_*L-g8GiCWqDh=lxTVYb)+pTXYAIM -KHK7K8ng@q?#wac*Ej5URQcb!q=QhnDCLvQz66cJj3D|m&QaDR85u}XQpSi+&#%fTdulC-E9%2aTWqL7T>$ryu -c8)-6@5C~sLY0@Vhq8V+O@9-0J7BL`^G8wH9>@Fq1X -rH{l%ZZf44ARKz1mXD86}61(UvL;27G}GPpS49T|<#7%{5oV6s&*1d~3qI7-gXTSFb33TDnAcI#sx%_ -#YP-~}73)U@o%r^I{ap>&dNnKEm^VJA#=m7W=unQdTW?Y9uTSX6SCRvXR#Z!(&SloT`hN^v969zj8*5 -)AHB-lZi;s}2dr4ZtOR`?b||(2o<~-OaMc0v=wD&cqQopi0kvLrX;tJ*(oz)Wl!)MXgeiL?Mi8HLpKf -@S#Fji$&De<#IggTy5Y+!7GGF6NWK{@1#->vP3W_h+G*4sW`c1pg}N)1izr_wkgHM4Yk>o_)cYF)a=&WWGk$L82ZNlhKe7P`u>ij&}o?c -3>~}9Yvo%Gthe3vS$t%%+!rCF19DM-151ECyA2aMkw{*Pm|-pgf21biv$*17KgIO9Q5lB=ZGJ~;2q0XRU#gIiW9#12ZfG)of#)z2?LP9?0?&$&p~QM2=W2)uS1fM*)w42rRb@DXUKdX%VI)XrOAVQ@ -%Gwx8JBJ?Gs+#sJ8l*dc06P%I-LMuN^jeNE0b8B?LpJ;d*vpcg`QFhU9yiJ^g#KkQ*mVU-yU*6N}yM6 -3fF{J}i_^WG{*ggR9>)TOBqX>;}Di6lPatVCgDZ=4`N06=5SEV -e|+}8{VgmerW6ny1KXb1t+$F?-9owuQD`bS;Cr8nPxtfqQ{e -?c*BcU+Sz6cgCHzOdfQ=DAnKqgno*s@QZ)ex@E}jRoRJjn$%1^G(mVx$uOaYc{40~{kNkCqcGld3m;S -!(#&~q+oOnI-qxzDq{J^=N_XobDbQ79P|@y16YqvfC4H#8mXu0PYPX3FaQmG>=8vOUoNvx3NlBp2jw_ -~&=x@4ezut4I+K*Mirje-t#DWhIxXii3dH9@JdkY}ES8QGZUWD^&3t99IiB)Sc{wpi1n=GRnXSM%G|! -0!)EIQ7AEGIPd_tPH71)o*R)80IE -SiQu+aHxx|QJAixOXnG524{@P_zcm07rxQzZo{TZpa8aAu3G!E7>hxtH6}Xn=zZn`K1=)-60!*d^H34 -`5J?#~Xy0S5?G6HyStAP5iHG}9k6cCKrq#)3Y?dFc>;A;d;3ZgnFC6ws@2#5ttn{_&x#qGhK|4l4+!s>v`UrY)AJJ@2t*szBI8! -8lP0x&dg=WVhAW$1l0@`h}uJ~yX6?s8o= -O97_Ow4w)U(;iI8^?-}aAhG(U=p|G9_UcE|(SwwVO>Np}Fm#wE&ez9J -^9!4n%JsUmBG*v=-Vf7vzmrg=$Db62e@7^spZ(Epbc5?Kky$RW~<&0 -9TiGH|Rq!0U-8n?zd68YTIF57dng~6p;%8_`~!X7UW{pY&H?Hk#f92cR=3V(;Vfp{@Jw@pfzo{JPMEI -;dA1Pqtlb)v*XjCvy@=7R=sp3w9GxtV=ZoiOkoCy@>5J&a$&8k8PMwK4(GGWEWNt~6ylo?&-D -0Xr|-?_DD(5h$;smD7tZN<8VfG@s+xh&3E#;V;c-LE9B$ssObe{#$?Gq~?i+fXoBFZSW2QYQ>V`Rdkg -q${7FvsSYX??$IScyPMg?u(>4mlonorjV=zCPiVreRnz{RF^6_ze7fe?L6g)!fodAPw=FQJ332PjrMO -w{tD{8v>(DrUHcImM63<1NJhti)!hdtMvB5EK8?@ymHi{EY$ZVl_r(umcKPNY@!w#Cr%gtRR3pu@lo` -I;wM3VzSFF%`)4P|NjY?_@dEIbuJq1zi<2m$Jt_%T3}HcvRSMQCDs88gE}_PsKKgPwPVES5BF`W%_pnja-Q$w&ojW+O*JySt$<}*D8}O~?*ve -DCc`2}kwhpA!m}CE{d{ssoSGqT?Dlf;RuQm*HXDMR+hWRUv@%~i7pwJt~mp8mOYfw-?o6xM$UTZ^mDA -E#DgwIQ! -%rSEn&1LKTYhZVSQR$$k|A%y0O?@Rd1sv@WAZJ2fhZJHTVU7P+|r^1bnh=YJRLKEy@RO&r$gc{#-g8ErsNB4FPcUpCZ(&++{D1TjupO4a>!iy(EtORWv_i;U)H1o --T&N!fKF`ZvH5E|}WV(J+>(lCBs8TznEm>8vCskpdAX4QN~SL2q?&SMXgCgq_skSR2@ke>5eha@`}m*{{8kCPIuQr(np -rqbv0S9ehSwlw(Yc0ou26Ii<1=xBu45|1$0Aga3y#rSs;0K~wtR|E#9|6~#>OC%=NN0W>h-x0x-N`~y -%+0|XQR000O8`*~JVb1@;TK^Oo4j#mHxBLDyZaA|NaUukZ1WpZv|Y%g|Wb1!yfa&u{KZewq5baHQOE^ -v9hJZp2@Hj>}JOl`6{GE882_B~B`KvUR0ZQ5+JZ!HPrB1Sxsq@_)bX20#ELII{BCs -j|ff&}eiw`h^CQB>5(<%Ze+OR~7sD{4HDD@>U61vqIKgE2||_ebdOcOOoX1NbK;ERr2~;)YtY_Dto22 -eJ85nZa=)}+o}@lw8>k=k2bB`v2Nc02xi_dju`yj{ofBSv-59Wp8xObH-BHzqv!QrU%U`Sr>|ajyv^4 -Y_wG8`>bq}+GOfPL8yrC2eBX31r@Yd4@A}5}ez$MT)lW6FSRE~oGg;~J;w$w+g7$Q5J*s!?u05rT74tRlGpF}2Q%MX-DFMPZ8 -QD>XrT1Nbza=`O;!p+o>Hi$%J@J6{DAZ+N@DeWB5BqKl -qW3mclH*rDuyQtMeVtG7!<{eYbsGZzQxMVZN{eexbVda>asM1Y7bAG84$PKp>|KUM$)1*K9 -3ir3r-#9RL2vyS}Yi*Y=#j)VnQb*aZ-hH;Pr_hO?ag^y9Z_&`UD`@Cms>qb;>8ZEPc<*c(}MJuHcn9` -aRQfp+j7*!DFLXxnv7vSRdC;l)N&oc_Y|OZJ2T1yd3#>ZHu1Nvz;i*7EngXyHvw_$|yhjl?^m#|mz5% -@v5{zj)DQ1F=@X<~7LwX-Hd`Fiz!&XbBJEqy!QI!AL-I(y|^h>5Bop@qsOPdY!W5T69m6Won>>Q$dJ}zUHdP8(uE$TVip@QGR`$`F&xk{%s0aR-pD4$uV* -vOO}SB@1fx)`DiEc=^0$!etHDp&OVM|eo6+>(A4!r;yrtUfc`_jfgZBmpMBJK(9f3>Xl&TM5p}86U?+ -!>S}D5>S7Js;;jLzWB@0r%1N+Tey4O4nxE*r$6^upn#D8Ytn<2kY4B@c#o++NU#TM6p-%v}GTd<5B-; -s78P*)a{tJD`7oCxdWz$Aj%a-O|6Z)<402;LFV&t+egL^q=P7#b;%VJ)!7iY>Z -U0M_WdB~x(VZGnOrdI+wT$!gLp;AHrE8Qir_wvOzwwcJp^3Z&9v5Uv%AFnR*QJg{Iuw(;-4RBug!)jK -MHmx}Ci$KS~A70N3)PK%e;(K3TI&~Vz}=e!a#9Z-2>AgiLwvW4Q+X2q;s@bCvy4~BS|4lSXLBNBE)4+ -W$Y{`ZEz)mj7;e6YA))}Vw{56c-Ha8%^Ss^Nv$hyqQKAU_<$(X5~q@L7V84F&)@Xg=uoOzKgLMIrC92 -IopkF|2%zj@puxtx@bb3=QI=kdpNVfP6JFS6U@~K%WaVn@KbWAFOr{i5OJI*9Pb@8jd2gTtgqkI+-cq8P4XaVksZJlk+k%Vvd(I*K-R!kO1E+cnfkQ!)Eos1HNP_kz=i{aMdD;NiSzHfoI;~QHhZfn -ioPWiEQ~H*v31`W1rGlE9S)H?WXHAtiua96EAvz -F4?_|q|(TL_?l_>*7q`S>qA$!F}2`>+lcKMtd^pjX=uebtP>A+P%GV!`3lu|RG=M{{7hGj4tz|^0OO= -pd?me0e1;OWqPIj0pd0R*UdY6vrAQv*F23C-4GG#n*{)M7b1n(@iUqeZ83VNkPnj+F|wBI&YdDpcI=nN|ash2E6EJvY6 -a;}3sJ2)i`9%C8>K%_3Q#43vH#?m+c15zo^p^*<0%mOZd7gl2(=8PheBIDe2`TLU6BLpF5(GHJXEdaaSk~JMoLg1NQNQrFwa -QIHHzb%A@0aZT#H*wTH5F_ZYnTQX2srVY`Z|bw^RBJ+F@*xA%3Gg#+l!ryazW0os*e{@}A5OfI|5AL( -MJfo*$5@7aYW6Cu*)K1)($QZP-z(~}IR6=ckJ$NEO2w@&h}6bQ+ -Hg4DN!59bWN!s{7L)^U1oc26X^l*F44p{;WrJ8?5Xu1s;;M-z -xB0EcmXM=2;I=jZthRd*8j?tPN)4?bVWhwnM-E<(C+X(n#FbuW_&W1tYnOjm_*7o88@@lQcsQ>iFMDC6l(Mbaf@~&<(rCqXO*Bmc` -K#Oc^>mcX#3WzyU5AaWj}1-g-5|0}`+B(IfIkaJ88&B(QkaU)9dN7=~i!o_jiYGkWufJnl42Q45(FJ{ -opODfdv{idNPbJd)c>&&gAx>@v;>{*Jcf7G?*buNAP=fH*MZC`3M#BX~ws-0^NJ%Rz9oCdu-$r2QJ)L -(?rJlxQUA!Ni& -TQWNnE0ifjhi#`YMp)NN_H1-)g{HxTLhFbTBok&xFhtnUSX-sM^Q7eATt|j#L=AEluY1|s7XIualZtFrK46 -{$ho9}K^@!ImP%P8-dYgB4bKN4h8c+)5)b6IA=|9U!zK(WuX=#9J>_ib+8qD}1d31+>vGZZ4b>3xZbD{8k_j(6*7Lnn4$I5wFxTk9OYKQ>f4HP%B??(GpD`>5{2Utt -L-%j%eLHl2v#4UIld?D?1X#H>Kn{`D|-xh6x%(21tkySAj -kNi=xHDfgy~|01)v&^g?#A0%u0ULszTV<`~F$k%gXGzQGKMCF%;d#~RUiIuA8YtrcaFn`|Q5h=Vpj7q -`r-`$Un^Plo%WyIQ{28!Gx9E)4Tq#)D}YWTvT3N -gla0$(JRuzlYj>w`u?Ve+!CWHoaTc1-{+j@l{&G=+Ua}iPZ~OJIsZp8WPQsH`lxTUSc4%>mp6(l_iUmPvqrhI@3XF$tMNVLkkAYY^szs6>A7 -kKnC)?64Ru7u$I5>`!m~3T#y|p!PUfMBH>s~teSyUeW{A~Kc>^j`D54J)WLa -iq#X!7QveEbJ^+%uZmrJRG#yo?74H|M#s8bnM7Cv5SM&L=e1s;y)66Tiy9UkvHNz5qNmz`C(4laZA}! -)AWrJ&iZi=BApiD)j&I{IA-sA-tJdy)8^N@NXFYwYb#P+VMz^`@Z?D{Y&hse6FSJ`F#U|7fUfMBWU!L -Avc*w9{;B`N%aunwDITe9@$dlu_6eGjobTlp>4nd2)ANSmlsdnHkaiF@-b_4wTJci$L -id+)i8ZKa_c)3mP^kPp -Zb=W4a46gwd{YXw99BjfoQ@TkRSw<@cUoV7iSb;~M7xMXw3gVk1qh&AgFjR+4lDGFQHtMcYwLP -@ag@W`Q`+9y3lFdRqfL}ZRJ#+mIx+!IQKQQ0aA2p4Evj?cmlv&`*O<+=1}JKzP!sB|%L$WL7U*F0&ph -F=tq*fPqBq7sXN!=$VcjW}#i+YOJ1B*t8HYj~v1Go6v-F*Go-3D`#2TKs*)ui^$ChQl#Y1baMg>ucUo -?ckgP6_ny|I1tNeyxgaXT(=g#vB0-a{PxzL&J+20oYe7eGHz33Kf>4`E88q9ZQg=4=lxsZci&bWU0g^ -nKTgW|5p6OlE8JXp(AhmY$mM0WQD%oIvbc17Y2r)Y)a2yw2qS2kK6Apbo_~P?RQK7xWao^-^p$9D@6T -9}YMv{Wj*u(LV8lz+?A<=YSW-b9&0B@g|=?W(ZoZPFZXB5Fv=~qe;Co<5_q?t0VKV;!eOqiXm3 -r5nzZ}X70BY%)#3cfA47q@Df5p1VHEg@z@vkeS1m++`(rwRh)awpVv_NChv4eU8|2?lj;QJpD$_~9Oq -3aa-{+N9Kpv?gUe3ceAVoPY3pGhr8dG^x8f|&Zud?`ti)*Rz&h74wJLL5%#K<@%_leOx((>A#RcMV|q -DtbW(8=DNEVf$uY+gJjco`n2x?Cpp?VB6!Z~V2jmc+-S*P;0T#=Y<*5yWBQ -X1WBtZ%of)`18*~pdX2+GY8dzoJ|vo_^<<}^WKbFwkJuj5E;ambPE(VQgmGjcTbvFteepPYplTrgQ1X -$2>rrvvr}uyUp=>?dc)Q`0w*Q+(jedoEM73!-@Og=4`%@x$gu(<-oK8e5G5{$1K(5CMtLbLje(qH97m -{$kohPL6MpF_E;EiegoV9^JUMYS*rINQdLZ1v@^CHKv&KO2eD#x8XCdHCWp^sOe2Y3F+AkjRKRjqX;5wmm-va-;qmu%D -W@E~mdujK=KT2tNEjv^yZ -1X-^fBh^5#|Up)0H6Z+aVED#xwjIG&X14ACnZ!%yc!3Fq#^DI3~p$shQVIyWdWNN9={fWld-Dg*{VcX -aXV(iz1z+mx^;bZXpvL91f;Y(`8;O|3=jt^$%vPV}!0IRGuTM3n*)J&ts<@9vTgtR~+<4KNMHEgZrp{ -g90>k5F_>N3v7tqHnIRhRS37G`+mS5<-U6--;D-}9Cj!2>$N;R*+2w0A(cCO0Qj9bcmF`az -U`5#i0Y8mq`La`q)B8(okcZ@Q}tvT=y+6f4|5*sHyx7lo-Y1E>Nec$FRoHj_~ -`23Ds^ -*%C;`L37OY5ZlcX&+Gg*(L|!C^O29Mnp(Emd8y|Ae)I3(;TE1bpo -SvLKk&j-kP20Q(M1Fpe_CI=kMd2$Ze!2R(kFTBdKoIQvA*CK>>u09UR%Fu4S-j;O(iHgKtjW-Pi{fim -i_Sj-8X+t4ioVQJj1Azt$mmw;q&wJmv4cDt~HpMTJUnzoU{lhi!KW=fj~XlDjU94KxbTRF|dh;2joY< -q%ASJ_TwaLy%Y_AdSP=oJZ1Cbpt?Gjr~w2zL6v7T0r=ilQwzIQV{PSi{NIV~zZ9#|TyEZQV*ImfC|B~U)8Y?P9~d3;ct2fz;^Hv1+|g*@T)34J%HI{`OU?_J -{iYyZqgiA{FYZskDBI_7 -M0yRq3SWu%WK&c`ZZV9qxW`2l&!hTYk$M{HPyoHAZnv@%bMH5(vs^cZ|n9lwS$lf7d!6&rtm)Iw&PfN -FVs$RXZX$ye!y{5+lr;Apj+un9^jGGv+}p3;$5C%b*ZN{b3{jLAS^DmMX-UBgD+6xims;xJs67F&<~k -DhsazUXuhg3{;-{Y$nF0DP)h>@6aWAK2mt$eR#RJ|zX2x(003kX000*N003}la4%nWWo~3|axY(BX>M -tBUtcb8d97DXkJ~m7z3W#H%At1ZXpjwGneM3sN9D9RkQ6 -F|Ka%MPu^X845rfJftF3WGJEsYh*i3PyEfhInDgk5Rjp!`F#*3DyWV5)OL~CJM;Y>rm< -}JGOWLv4TBzdDWqNvuXl7?XWlU;3kU5Yh{!UO|LrBF@Nd%4ymB*G3Rrqw&xC4E;)4=OV%m4iOQNr -^qupTWRoQ+t{0Z_yy|^#DbEqJGu8{ac1HJ}^7x!2!}>;>_4wVmtdqVTAKlI+$s=)TwrxB>AV-C-YTbK -do!Hi@s6{Pr3AnBMw$w<^^!6CV;TlpqgJ?JaK7h)JTd8~E>i`6Ad&VJx!E^!o -+tt;({POJ|KyhG926plP#rO>DTG`M7JOoa8zFa1ON>nM_M+(OzG#RU$W27zN8s&Zlnh;yp!iEG4V};ue -ZAxgg^Pvu6aH=WEGkg{9P=Wvc>of5~W`~T?S{mE2#70ZScOb*4RVPN8&wH2Q2y2J9Ie;)Ott^Dq^-H7 -c2A*^dq3&N#{^2iw1pLf$Wn&8vq^Z0$e+zppeRoh9soP*V2pY>mlAm`p`b0ASCSidJ>5bLs%*oMD&?k7eO|fVq1-iG((dV2)DTQY0in!6po6K~qqoC@f>7DdOrDmc3&^BHYXlDKzAq+;C^dY_~ -ylgEkwSVj%!u70y$e?N_+QkvNdW165eJhU`8Zk@_Q8J>5L8^W)xmEgiU8lPqC; -5H;sFqz!BXg9GG0f_|Bcr+maxyUtR7D12FasvD3Gw@G#i&P&WE*I)0W(6H%)Vjr@Py&n!;Be3{799wT -3Z>43Px=SYKH$jKiIg6oM2&P)|F+PR~%rR1k+&`=NK#Es8NL}$H^!`(ZpNfqT-!q)&*O;wDyBF3t7EwO8)6%mvxh#RE?&oAA!XxqPz{7DsjxRKaQGi?_?d5bfGMHf=*3=B -*J%o$hIUu?RBpC_EPaj?~!&v7s{<9w}pCB@e{jn>NKIRkC2(_f5XeI=20Q?@<+}<1Rar?nPS2q9% -amt-%3bZxWQJQm5-iW9T;WWP_^%&YWGh=483{jedU4o?mdztf4EQ3-|A2z18=GJ2C)(*3pSsCyN`2_n -FQ8JK6A3`V9-$Q323kcWOl4^Jgzkmv1&XrVwgYoVne|TD)zX*|cT@*VB!0oZq{V#o@^>*!SDSTb)Vj# -9~19=kK@7a?nUq@trB`-9XkhdOgUgGiQkYO-I(7`5qZwH=2hXmUTObd+XdB-#O*nJAtOUoadmaGtSR& -na`oag0bNzV_vG_AxCECbmV*7oX#8m5ti4uD-YlQCrXy?Ujo7>UX)~^{OOInY4qvfKZy -K)P)h>@6aWAK2mt$eR#T-cbkP4R0017n000#L003}la4%nWWo~3|axY|Qb98cVE^vA6ef@XaHnQmN{w -r{mcTXf&W}0;S@w9cjZtS)`P3@f6Y45%yE7Kw*Gp0x_N%>=Q`@g^W06+i)B{^}^-aS|6v@uCwFc=I5g -Tc(;fp}UhZp(CbQHv*^KK^_N|N8>}_oX;WOZa#p^Q{M455%){BJ)ZnVwoqh6!nD^dy9Ai|EMoR@rx|0 -w8+Ji=u^?h0zLJqH~1?+xGk1q9^Z<*sKv6903xl#G|i-tHxs$2MVgCAF<)e9oKK{5s=RPsOi$FJ9~&ogN;Xh~pRH>G9G2;pyS=5j=e__KyB8emXqb9|{R%f@b8+qD1@w$ -rMQ_6QtLPlnw&Zf)T0~a*|Hd3G^$UE#sLKv*JpYIdn%XWI0bO9LNgLO8`oi&eJ-s=}oIs(biV4*V{UU -H)m(myW;Fj0KqR2Y~OlU)c~#{9G<25@&420hK?PATJr29Yrlhd?#J;Yx9<)g1KYu*Ols21^OdIG51h1pZ!R@kGK?P53 -{*tuKoFA(A_trFoofjTX0~=`x>;&Y)zC5Tgt+9@`k72`qwt4F$Zx4(MKsId))P02k>pahBG%k=O$&WH -k&9pm-%|bO{kA(7Bw%b18B;fzgl4TY7=Bi&5PAEc#p|5_n@0Bg^reGf2RIsDk3N=qu>v9M~X1yr9WqC -XuxwaadPd03o3^p!d4I2y_~|dnT@NBr+uYt=)Q+!o=QE4L*s!p!Q9pxrIt3PUOlkOU9_P1W -62si3$#;=XF1F$cqJ;DD_{8H4i;1AiIZRua5;P&sR5QTjODP_I_TSt7AOkz2Y -^;B7g%QqBapN7zQrQZ>EE9pjGz8^@D!xc4?vizhCwGfkCY5XvL?mEA?s%#htEr3*GCG>MLo|Rgg|cT9 -*l%|0J9~Y&EpX)Arq8WTLOO5vY0DugrZKLKHzUJza5%~r*W2{AZXs~r<1yQI)u)<-;WnO5t}zBus%rO -;^xh%yr~b5t>@*!261}3@SYO4)t5O=MS2`;(LZV(#bPj|rb^SRo>ihZ6z_WT_-#@4Mj&F#q3F%i+

Wx=2OD|;*#H2`M4ua4B_?s -c**7)Vo;eWxZ`ThRk3+P=@MKZriOW;6|YyF<}6-d~7BGAD@h096 -USNJ2@D`YJPCE_wxbH$)bWaD3c(%aAJ66b5=W}bQ1S}jL|aG4FMi&8cKH7e_K}!}`#?VCw_cIKSoexDo@5f -}4#MD={$|Bs>$kg5)+iqE(WP=J>t&gf`iJnabXJ{@H7!shfHmubsckjW-9~`qOl+e2??LisC*L_V2Q5 -REi!!Zb^H*GTjKhH`qI9*tbOWhmc}3Ewb3n!CTpQGm3+R`rd0mv373)4&y}XbaSAOy1!87s2P<$zVZH -k0pG_8lKpU~HEP28RUo8%yULe*irMBk6#`<*z_Xr?d^%M`UCUezvm`Uw6NwTex~p -+?oj@01I_2Y>D#o}9ioc36~0C^Q(+4UA^Ql0iI&DGn+$(P>iTIHVoK6#Y`-0wog==&C_2)M33iJvcgr -=U)z9oB%@4_fAg_UK{~BrHqg{006I^Wv~D3=+!?4Z?<0#`qB2_b${@!*oJo}r_aRT+y1N9N%YC9ulKQ~zFu?h5`g|=Rc!sr5~udhEDyxJMR=_@e*tybv4|LGA5-#N{azI_ -f_@C?`(#_9S(<^lvmiQBCi;kgM`hd;o`Hpra-R!q8HBG1z}*Ltl|J$fJ`%`&Q96>{#q3N68^Flf(pt9 -%c1wd&G;i3WgqoMzU0)F@z$6zxr2f4a;v=gk-}!i -{wCX9Kq3;)nFUZdwWQLpqd#io&uD+>enQfonSBNm(|97;EN7V;uz@m4H14AP6W`7LG|=$7DSs#;J@|Qr^Uqn&t*u$%q@YK_w-tWyKZ%gh1e`Vp&d5Bwob1(;Lsq0^e(D2 -c`opf=N1^O5#k$hEYZcEmKBC$icEz!E3A$Ps*Y~lTlWui%j)LW0KYc$rzUUjR_@%g%`~*Xlk;Xs6gCA -14W=cXbdfiCCu9*SK^#;CGcz=gAuN|Q$c-$AhFJ2lpR@lx(*_M3Q%EL?L^CJM`{4?^w9zbRRsVF{-fO2pS&{b=Qb;?D3W;q7vUvgm8sZ8tI`vNM1AtilYX`1 -pH|TYN6j;#BR#n#PIYfCX3{YR23i>b-GArgY-{l&r-`Fq7D%1DkU9%}B<;)7Gi%o-1Sy)f-MKmU2fk81B5po)zwEbjq)Z+X7V>t#=K;!Aq8RY3D;pB|OQ -k9xi^zZWw35$NsjKDq1!My04s>8J2`e8QimGJ(CP`Sd+KDoqh|LaT+7mnR2mQ);zhZxf3Jumnt7RS6% -l5`T>6@xO|uD|_^4BZ-Q$)!ZIDI(^8BWkS=B3O&U~#zuEV-hjM5FEoq;ezalCkPfDR_(A5Ug;O9spN| -)~dAq!tL$Sl}eiqO1@uy#Y<^u2@$`}anp2RU0{FgW()w)u8IzpWTzj9$l^=EkI&vW4Cw4wXY^r%1O3G -74l$Dh@sjVCVWStc_zKF{@|`kFdp`aEsMw2Z5T#ZoWSqZ&UpLg;#q9iop@y2HdVQ2a7UfxdX2Mj%eiq -m4OierS#F4Dcr)epr_3e9jG~ecFlH;MT -`u?+e)ZgEKX0ug!N~gUS@s&gwtj`!Tv!b|MF2qGDOPt -@gyfUB}#B5Tk(e}1ls?xHmc|8K$p!0mb?HFW?%vX9|vmEhRu1plQ5GM*J$z4gt*-PJcTwfZ(trJQI>s12Pvg(nBP3CA2_sSyDLkt!9a-nbu_6MfBcU -!xIY5m%M;*H~}Rm?_WYlnw%E{dao;FIe$4MCDT57Iali*~bNI0#wjk>qlvBZ4_C3(zXZ1^(~!W>=D1v>^B=1oVjgKTDRlv76_hH(eWG@4NHV -nNOxouI{+(c|dx=bwHi`cGk^4Rlj&VQ&QcsnxCblIcpG8({Av(?b*=X;b1(KxU)SbYr0(2Js7N>R}5rw9i#_w_thyRM*|p}{l&>t&q2%3{=(_oHoGyxiY5Am>OV3=WjEkm2Y-OpKpBl?1TA -fn%BusN--)z2Xlche#3OH{?u$_%zU2INDZH3<0Jkb|)n%)3CVS84TXn!Ng*%)Es3{O)##^h~e!0pD+Ode9&LMivryVgh8`ubpngM6f -E)($*Ue7cC5G~Sp(%V@jn3po3r2@Zd3$= -NRE0<2)lCNijh)QdHu%-qTg}@4&)F^L;)cRG}+7tvGa_)wq*5eyIJ61Y(S>pxGTk_aduD^Pz+w5TZ&Z -}PFq?FtZhl2bJ(UZAxFx=#%lxnX%TclFs!09Bly1qJ=3RSnUT?@r2?3^nk4L51}`3nIMU%B6hPh9y7L -;2;b=MReqGkk_tr*Snx=n$7D|1*ZDH4u7;>^c5{BSfIABgc<9Ku+6gd_dJH6B?Q -=dvX604o_NUMiv+_zli@UX$#a3~v{K8=}VMJ2OaT*$S3b=CTqqRDXKoDv;>(y#HPIiJ~V1XuG8a)~`-)X(mQ_ -dp86o~}QJFA*u5AaCuaDV*l@Fz$0vrkSe;GkCl!PMFK*H*+H9$f+G%eq)lz=#g9d&FLR$s?%wr~yRrB -j4ch%L*Q2R#JSWcfogdQZU=+W(&uCSu@|69hy1cY`2W+s7KiBS=ITOZW29-x}^ZaJ1T0~nB0wW>u&nZ -Zn%`N*SfWuh;Wn%kmAgtIn6ewYQ3AUz8HV;mtMq7Wlr5JwDSbLF_x1Ho=-Yx3nRG5v>9gt+Nas^c3Cx -!ShT39uGMtjSFy<`zg^Gb+wS0z2Y9EpeMQ}WOaF?y($F#P;w^l8UeR?4A_3zmwIR!G&o%KQ}-68!3ermyyTh=m?HMSk;;LCJ=t* -r&xuRWkvWAk$t0>^I9N($E@b;Bo@P%Et?r5(EVukg&|4IZD!qS3L@w2L9xTOavDgbF6L-Ez}6onPqm1 -LuflNvO2?OX9P|Q+N-$4!!iq@upjQ)T^+jB-ixEG!vr`*K5%Q>hWBB6-4?vnn_tBi>$kuN8+mRmW>n} -tJd3*(Qf(7QR`s>hMo-ylcg#4MZ=jx9V$-$H6qOe4V=5EWCQxV^lT6SWV~aX67{_e!23FtUrC|}nZrD -tP)(qo`qHSH?qGnI8HEN1B`D2*02So)yD-Ey_<5d8imBeh(Oso3MO8I9ZX22Bqo&*g+0wOM!^=7nX;k -uT-Z8|Cf^Y=d^jYB>T??>YRqR}T*xb=z29@on*A14`pA;o -8|8oq|2TZmEmL_+*K -gC?I4LEDB&7m62zH3D}bHqAV7&tZ%ne#y2YwksfQ6bLeZ#0>#$DEUqdaK)*PDD<}0})UXGKqnn-s{(J -Ue+23?kHdHXcsxiJrLL8(2F(zCZqV;hSXIW)AEC}0RVT_(AM=M!dfY7{gHXfrgHy)p%zxi~@#uS1rn; -`1zG|PzWrQE^RloJZ08QDE)%vU&9#-Mwp0K8wxY&sNEHBa=<%om;S=+&#e{PxWdlOesK33OEC(y4-gq -VbgMrn>_Eq)@)`I8+D=`1s6ZgAR-g{O>xKWfxwUl!>wpy)qcVA5K|x#K!~0^S9b!jW`cQkLdL7HZ~y`uV>X}jw~-#2S610K@J&4;XSJ&041j-t&zT#^AJ`6uGyLexo##3Rw;h@ucZd` -x?2qG5@XTOA$35tVl`J#4Gs}weI5XWh&iITSTE4f`;KXQNL3T+SC;hmbQM@AgSH-n<7*-Z*s(FUd#zm -GUbO6SjYh`|`U~xdsKT{G{3C&PB=Hu^fp$W3|ZH7{~pO!$XSX!@-^Y5n-%xz2TBcTY8rn1T*I4~zD7| -@X24prO+GGCKk4;}hcRFDb?u<)zgf#9zHSX^U55%TS>(t4>9DPB`VhjtD$De45~HRI!63`Tb>qXbT$R -MLRIudoz0|&3ZO37Fch7EMycpqYWQAh06ngKp3>O<; -P|=0MWPRDfKJ2St1aT|oV)Sy)=S75>P;-!KSjSadu!5>I&|(Y!+PIm8lc_CItqPEb`)zlElSk~ysn0n --Po{waq#{fFr+Cr#uu{}wRPL!U0jz!PePREBe<%SA3>y!qj%pU8XInjOevlc#Ml&?^Jy`f2hVbdhGyJ -DTpZcK}KI0prbiG_3a)N_Anu(hc^jwbqP5XngGO8Qm5>_% -j6MBfhZlR+o<-!Qa7<+I+(uP^AiwZ7y?6o@!L;cswWs8~PbI=&k1%jL8o6M!m{zOcE9Ua=FH`==o`qL+_lOwz$u -DWndTpBrZK5UtjR4nFhqv@Z&x$xPK2+;w`YNncS!a>g7_Pu;sUo;s%-qS?!tq6n$t)eF$Mjk9W->ZEJ -SMBk*@(lp4j&z>R0X^fXvH}TleMoyCPu+#nk3Z`S{r2YoS$^#ryY9CMVwgeR0VmaA$t8n(F9QB*muh8 -byuxw5JvZ9o}y=g#%|9MiWf1ax0g4tIH3rdT)@ChPyrRnC60fj{$djBrl`yTD*5#3Lbl#dh6OD(G54b -S(=8zDiTYkt5q2SZTcmkk7aocpWhlm(W?;z-NNVRfc0n$ya-5~-3T)r%gXnC;WHat)$mBjY&;|{q)2Y -8nb1guGl~;l~?7T5mBn2wl02GnNAsAMFY=RDhIU!vOk0#U_uq~^qPvvl1PPM_7^Y}m<9iJZXJV_H7E7 -cC93$m69Q`_E#DYL!J?w~0uXtf7~e`pv$D`JW{I&q2LdwwX+InPYeCs -Y}-*VdQn+jL-q>is04o?lU`_3=AzUQo4DrK(n?u7-4X4z4W2!>fNz|67b5(Bl)nvyS21L{dDlh#5CGd -?^v);rstRlideHu -PG0$hhN*T*USF^a!du*ABIy=P(NAvhgr5Ouo8X$ -Spz(Pr+(+>vbvCW~a?TjM!C4DkX$d=I%}S>c-`kE;TsHYX_g^x6b$Ov9K1N2eg|t2vXRX#f?tbk^Qx+S#6t6yMROqUlf&k%s7Cw+L@!QMI|tP -LOj_mP9cou8KLNSdVtR_eac34Zdshavs|7h}MKkt@d&Qj5=h1M7UUf9wHLfsbnBBcdKD(2D4;9D`|U1S -nUo)&zlr?MkKt?42TdbZd>HulvVGDVsr9&@G(`_`&SxYy1lM9TF= -TzSy2CbrUKd@?5(h5_-k7ljSo@|jOFyzPtNd9cj!&zvqk`Kus{Acw)T4~fAOGq|dB=WSy*!s7fkSZq! -=oSsWgqYqs{lQbywoX*#^hM?)>3h~sqw*?OhCD4(VzzV$xWZ*Ztto_nTn6Qgu5|&@hs9mkFd~Jx6_>a -3aWdF`f8DF-SD{?8YvnG1`1Gh1Y2j6}Ao9MrLZ0#@2#Tp)nW@acst>w2brBx0TmeuQ}^LyXGOjI%~Zoo=@ -Y-tD))oq)H?BqhPK7C`+!O5F)?{sjo%G@g*yqMVcp~GW~4o-rS`zFJxyfN?BeJ^VFeR}`ojcxk7ly@@ -tgnGF$@d`B&61I1-saYqPpD_aWHT6D)CV55`}b>~@%-mCV@__05PA0CF>XrJUK -e3x_t?=2BMDr_MCeF|apO>T?CMEdbDeseH6Z-mf8Ulv`=>NPl-Y|mxgJ~R~$|htERBI@0h3Nl -z3G8~G-ltS(y1xk}o6Zir2{yxp)n!>7-n6aZ;u9+ih(4`yf -ciuIP9*{STUA3RCmfR43}YuyyfvT-4d|)7sSl4g9oFXvp9M7n4Bx`FJ}!_dkHjH-pe=BHQ5MVDg#}o7 -jt5iQI==(FpabiOIp!uk$JnSPJC19M2Sv%gJpF#>EB5`S;OSbnVYLo(e=qjK`G@v+7)|{ulQ(2MuP)% -Pi2l!mHpE-k(i`u*or*`-7tjc0P##x{EXAOCEVgA0Gx{nMG!>Y;a_9yTk2>V -bHeR>jU&Uw!_6cOE~~*LKTrm*|G*DP2NBhe6KpKhWc@B0ge{84xP`U|*Ub1Qk(NVkAd`5g|>UWCo+alWMMiPZ&z7%E2>=U{j`aY__fvl_%&bz2z&F`4W> -eS_zjPXvdg={T&uVjxYdAOWl~Vn@p)Pv$v=>FosjckKF4poga01(UsrgXzyC>Z=n=d3CVWQUo- -BqR)prPHyi@UZ*$37gpCBG%bw?dvl^4@hc>xtLG~VW9%h#W&@Vd>=y -_=Rd*bQ~3y!bfVA(SD0JWlW^&(c|&4N-xbz*P?733i*;7?2pF%0+KnD6LaZVv;%fWA}R&5zxb9Ck`-F -RVR2xwkxb?Rv5?0<(SFU2Vgi9ARfwHg~Gs_#!nT2vD1`#09QtP1kAkv67@cJZQ|q3{_zRvghR11sx}Z -c*QTS^7j;^zxnStM`5C-@yjE#!&UnZKK}0HJNnow0l&>lJ@P)O)jDj-$M -qV>lx*b$-m7u5|wJ|1_({2kkjbp6)EGn5!tWv-au<>qqepNf+2(HeR(xNU&SXYbTTG+b3baP(-PZ1>} -HyK8mdwpFI^Ro`WA73X8+w~J2*#ckD&PK1?9Ka^w>z;f8g=Uy-PvDhvZ*b{VoDfi7+L>KB9zTO23C}a -w~R9r4zMpTPu|UGbf~z{iFZtAFr!7pA0}D>%V%PypG^+7#TtUcGjBl99PbOLdLpb<85ncWJob@b>iEf -_c%)mnZ!K==y6NwV`5`T!E4i6hQ*}QSm_Nu06(n+VhiE>=OS`p~1)9tEYbs)qz^-oop+nxtuuw}kqGvuO>PdA`{dpN;%+vP?eM!LXgjA{Q3YRvq24g?eja9% -@1@oSg49Fa8;?Y>XFEt1*8BvD7*l{(Cqp-02J>aKiR+6!)&^`@q%}wx!p2Gm}PoD6VNkjUVXlJJQdt( -fV*rskw#%#ZPm~mWGA4W3tWa7%ja%*x?|P3zZpDg=A0WrH3rTg`XCYV`KnV^tj_ -Za4<~Q2#7OvsiCunSdeWLG}=}a(4wd+jJJ2kL$885deu*lGJd{ydV28UNCgjQy0&eF#^Pg-Ffv(hk`> -E@LhAkRTHqVit|d2|!fFw*0eV)%Zu3Y8kZmXN^kCyLX=<$yi9zK~Q>tRFM1y#emQM1UkU`{EOxIi1#0 -{VV%eam?B@9Jo#duP|xoO!fg_cmVOfRzb#^ix!R+BN7xb?3>Mzcsfh537YG8D&UJi+9#2YC4<#ap333 -fgLn#5w~O2G-NY(K9*~Di&%7mX?@dy1g4PB#E$#{k!R?N==%_x1yL#mL*T0M?gxQc+WW4qCt`6*+bmf -1wd7l@!R;`ZaasL70?FNYucJZ$AC^{O54i-BtifO%M#!^y6#pG6HzOL%9o|a&7bHDjEoQ19loP6;bi6 -A;&E`cg2=6yQvK_lEKd%0jM*hz2Bp%vGT79*G83R)V@2;O#aH94D>Xw{#kx}ScqY@eu+ZjE#+PgHr*i -4lX7HdKiKPt+zm3baJt=5rt}EqE#ja9Sw!Q@CqLMq>_S`P`#(9HmZ`NL5Rl|D0=tsMxzuFxBaCH2lS# -(aqA(BCB0{-r05mRV(tRK{0m~)iBab-fYZs_>CoBpF)48!VLcEi}?kM$fLg-Tx;346|pPOsT~rga|`) -z=AYMGh{Fc4yr?uEu_+KSZNgheoG%VgFXB2i9lpyJOikx|H0Zq1h^jwvA1$@tW;Phly3=FoFVZn$kaL -yVKIF8rz$p7hN>E2q_H5ba}S1L36k{8AHhtPj6y}sJ)y#J2>IBIRMDkve*_Bp`w-)s9>-@ej(>k)LxA -PHnEPJ1=B@@3JgNG?MtoG2bDUsl^IEyWv#57WF#**hkJ8t0Qnu_ZRbFn&{5?BbFq5c0qcRl>xl3o@M` -qv(G137d5+P^kN6tj9q!sA^$(>CtsXu8;>oJXvC<6KK`SKEJ*>?uqtk<x%|f!Ki}I@=9iXU4H-IyO%#WMdBpkc-hhvI;lar&GlQ -fk*X64WmF_7f>^}mjQ)=Hlp_?)|FUmofZ6S7zwLr%ioh$f)Nu^MK@7&)ld1`C?CtEltx#Qtzuv@;Or#uUxWIW-9=}M4sT`nchLI?ZqRxtrE$lTIW)O$Pj6OPO-%E4wDVw7u7XAZWCGSbie-0 -+QE+e#DPSE>H##NyFFK~YEprYJfT?kvXfoj&#ero~5-a(U#4h#=d!lgdgcszP_&l%eXSxi`jX&4A)@b -!|Bc0W||$ba8~6l8PhA>Wp8aWI10{loyU)4iGCmT-p80kPJFwyceB)dP7ay0DDK6@Jsw>)#F5ioJ-Qq -!sUrooma9sZ=z}|^LIH7{-QE?HEsGZJ6g1yeW=MMC^p>CQ}sR6lCFzFCdCZ8EG6i8WIRTZGajpBCzd~ -3{~u6G0|XQR000O8`*~JV1`i|L^ZNh*@+$-Y7ytkOaA|NaUv_0~WN&gWaCvZHa&u{JXD)Dg?0tQI+s4 -u0|N0aNeJMekV)T-vtz!8q+j63-Ecq;@X&*%;K@yZ;fdB)5lG!Bv>^Cp_js;1{>T|jKg>5Vn*qz;-ot ->SXotG!U=F`n!l#i3_YA={qlg&TjlZ_|AG#}5?IG8PFQBlI%-fXe)1fIXjXNw}ax~_t7)CqRBwstpnw -zmHq1n;7G8l3lnx1(?8NfA@wcX2UI$}-8bASr|ExQK^~;HrqSDjs)(NfF0EJ_$zGQE?S_gDMZAY!S@j -qJ#?hu!@olS_U`~baA%8veh*JD)UKo7ZouS9|uuc=A$G6h`~4?&8KlzMHRLXTiJor++(oefTb(IVZx7$U`VJl(ygWKSI{!yEcyn}qf>7T8*n{BR!Ta;0SKl8Wybs=e|Nh{5z1+pq|nCTS0Z|pzjXJZ3bL#=}e_6%sdii-? -b0xXqiHY@UIbd9|n6)`-7($LQh?kD~#lj#iV%8qt}+p3_yL%d?q)Yw2%*Qghg`wdLWmX&1%@}>7z|p^%P5^Q=YG)*o=4*`jwy~`eAXHa -U}qT4le7xvGw3DO-lN{{4FLRj(A|(fj}w^eC>=06mQN3|@gRuP7!{>9Pvn}DvjxbF6soBR{eZ=cl|!!E37!S5ZVUc(AAZ(ixYsy`P|2B0dN -+fSf~axLZ(5r#fZJ0eJtBZJ!UVtWbi@i3L;@T2Y>wNqxx>lvTTKOybqnNg#I#2qaNj=Au7D7=j1|4R*!vAm|26EJ1)z<7 -(9FP@6x^bHtBWlH85yFdn1KGs>sT3^q2tjVtQ)SrySb-Q3u8KO28b%7oaVVw&Db8(3V&o_Y}JfSVDFV -N}L8_nqct73A=)&@-|jOC%XQhvt!3dcpT41A)z1G8^0>;*SyTTO_Al=XcN?Vt&(L<|#8B-Zrc~gE&r& -H{rGjSeMUVL8k$Jsosn3*^S_VAsRu6;0{>I=2ejc$2Qwy?rm~l;^5d4{YG$zCmw@jys)x?lhGDl2POg -iS6t+$#oN4y37k0Q6tRZY%!2s7rrA1h3CH1*1K_@&C+hKA;$reOi*rO2!iMy?x-j}^Uw)E5UWE%6E-L2p)Y$ZEe -J6pll*4~%9!T0B{uq5pDFhDLu$8B-e4xZ1d>FDY&bL54+5@v1`!*acfds$q)fTqQk&P#cNR^nY8-4yX -e?)LZMnAQ&ktG{0axNjCh@@stOfxpH^vvOiw{c)0j35^=L-By8Hz^dlV0&(T#erm;qr@FM( -QMNy21{SMoQ%mDDfa!VFKZfYHXqhJI31d7cVzlX*6xpR&eX5zWLq=s0dEAi4&s=k#O>TlE!y+^CAho) -CnDEU>F(Db2bZ3jr^Oh~9^=;_^x#DF -jo*!ZS3niuoNJ-zv<_gb*906tW?|q2C&0ekq0kDFZJp6}a3Wb!W5-z%bxQ96-v$N-XMDk?X!iiocbLF -dzrZyzNv`Gv3B$mm_${$02U6HEUib95A9*7-e*oKXUY46{v_rCp7AhyOd(R6g<&ac2WQr(2n21WDeS+ -1G#pFPG!OH~*0W6l?!m%(Gyd8EWyEWKa&x?87<>|f7VO6RoS!NF63@MD-0p13^6OFLgWDjK=-^OV^!! -45_lF$!QHY!>9a6-Zk5n~_3-#$nT{VKnS>EJy{BRWiQi@+QB?0!_1^Ei0&^)4d;9z*A1nA|MR=Vd@5l -02m0emYT`y4dqkw1%-9kUB+esDKM`06?K2p(3XRir89a`3A8VuzZ0Z#Ff#*;rt3aX-M-!ffj^-)5AydF$i-|Mo$W&^wiW0u~Y}-K<6 -VAad4{3`OuJUB!<6ONW4}!9W(&-Ii@q7bB}xR6CT~G8VH%N;`yM9z$m -4N3yF8j%vwfyir|Mdy>aW^N7yj}qXRTIkHHK?dXn`_k-w!UbDeV# -P2bk|I;4_p3JGj2HX|F`JM2WKdb(FYzy`8t5Z=I8mVJe^Mo>bE$3_{HeTF*I|Ss -js9M;~%1HRzvTAFU4coklyF;*GW#l`D=BMQ4yt#;UDSZG#l~v8DFo^_rh%knaxW;(jf`SP!NmJQ^A1_ -Ghm$zi67Sss)eZ@y@{$QHA>do6v_@&cAx<#0USIRFAB^#ozDa(Hw~9HvX2C(M=7$kFVm7SmEp -@nVpl$n}LYw;2&y2&gL^35Pm<$m8ZVE64wqc7Jopst5Bkk`E6Od1Il5tl=C&Ufc;j}NmdsTTZfj;0F-MG -^7u*~|q$9;WuZSWUs(GdN~ln-xsm6g_}-cRHbO`CT>wVkBhKz++y6LHj!X$Y`;^Usu!A0R17ljokjFV -s)o_`K9La!yj4hz@0`nF=7I6O5MQUe>Z=^^20TDP`%1i^#s=JZ33qcToAQ~{$>_J6&qk7jW~6R#arKk4z>0X9-^Mh(CI6nKaB47V#A-pj#|v3 -5Uc$s4iwP*NQg;EDMU-9fOT8W~h^4(WNp(hiL1Sp1C8K;CYpC;?O+&boAIK{j2Y!+P-k?t}(|mM8I4z -Q^1i0mS{ILS|&JA2icAmgV=p*plNHz}DB4YxvDZ0LQnxW;Ym^N+aZ=)Gcgso1spNX*3a`Im+MsTWHDfx|;9i@Jx&-0>K(26~4z#va-v^ -9HFI{i(5UuJCB4?7C~{c$`%LQH+r`=$hKouA -G%M{{+>~3DCL -qk8>fq2aDsg{RE14(Y5q%iLc{;&Mq(~zcDanC}qa@>$VG&0+WV{i7Oh_Asc8PJ%*HH;i_f{x1>r>;7;SkkK -jACfp1rB6r%^epcc5oF}ViqLwLl{r{<21$V$ReHyf^2szp!z+)Cdn%-+JAWRH1wV -<=T+%C{uI~3md;PvJRe{AlVYN>dw16ogPZBIiaU||H@gq`4A|0V(0eBIzqE@e*rAZYUbYG+ITGx8f4C -Kgh2s4i+4WZGqF88R@aS2dutRCV+WloZj2J$M-SXMb-%#in?!afWaG*&WxB3>A7chIPXm?2DJKQ2cRl -*21jF%Veohx9P`LpONd4PJDE7Fw{7fdXs2#a06|hV^=M70DAGP0@Ok%<5nfA+5%o6@&>iU9EebY;nOmSj0TCR#yiyTT`g%ohK#G7tau9RCj+0i4 -@#%&yUTt>$Fcd=m+69rGxym~5ZsnD3b%!8Xap3#Oaps3VX~V^nb&$ZWBJz*aI|>o0RsOv!m -t|P7p8UvO5mXE5~u-0KBur?QpR6sQrSFNqmOuzn$U2BjBFO$@~p;9Myk*hrej2=fea0WShDCzCJSv&j -H{0p6z;_J^+Ia~Z-S$Y4Zb#m=r&KrCEjvieL1rL`YNNlOuE-flTlT|#*m|p@_eYwm^|GTYXQa-&f*>; -1-=ko;4h$t{#tnL7}9-TY^#d8BHB?c+?2qXd7sxdg~4#amq1Zr&;4e=x(oPX%GdZ1x$ZD7q^Y4^ifG? -q!x-hM17JjI%zJ>K7CoIXZmE`la0Gb53LY?9Ji3-wa_4uUTyTK*FOyj& -Zo>|=8eSAlXDGjn5Gqzai#OhPIZ5!w7+wP8b*Z;(5^*hgV9)V;Bb;%Ac2voUiGkgMnyoFB$S2rzGLt3 -rJY4v0{}_`GNbpX)we$Ug0<&OHl2tXFbP0kS_TG2q{2I5WH3b{A{d`n>$S41zFXBg)|OF`QvSoSVJR$ -LP6~!rx9S_`Eg-_=J+Vc2z6gz?o7eMDc@3tJ5*&3azK>lO?*xXGwaes!XHe<|v!ZD@8zEdKMRagxPPs -`0RPhW*diS2Rss{-+GzRA{0hYxCQ{I5S`fa8o`$4z-i^8M$0ht*5Jh5Z8?ia=)=U)ssA93E%$}?Mqzi2XW46*&kS`1%aLnwuQzy&y5TUcsobqwfJmZ3?5!W -T&ArA&X{7^4sDGaN2ERjo#TXhLc%U$$WOPSSmk>6!(SHLGz^A43=WY}(*TKa|$tZQvSpS1UU%BJ(s-A -15>%(nirr~1`1#jXk3?WBi -R+^ywg4(u8bdhz2n(_zLnG>?W_DYO*KISa+ty9WcD6SZ@vvN1qH=rW2zKPa0gDXA*o6nP>Uw|s>jtsz -t&-xZ=m!1OjHw{gFI35-XnkF2Cle(LD&tr(;U?FrQp*a`bwb%w)eKxl5$R$a3Lv#8?XKgVo_m-onBGe -IQ-}|`RgCywQJpIW<2fl7WYZyem(dvuM(NCZmyW5tl+0?56!&~TVMT+ycD8|21(_95q@Ia;t7@T3_%86pzscuaNB)G -bRF?jC~&XtKzqEeR=9?r~k=-zZTplsOV~c?}Fr&((Xu10pur*(5$7s-!}vNni%^br8uOW(tRU}FQr -WHGoxr;h~hzLH5o`V~e3G)9gPmWSmjo~Z5;c9B_dgLCOHBP;=hsl9}ctgDo657TIY9UK*l8KMZ$L$7yGz(I7-qa!w6E_(^@B`ss)m}6ni{WQRX4U^yE@vZ%1*5>0TUnH7GG+V{L^q -fXF2Xs24tvB|b*w8vTD^Zrpgx{~Uj+VW`a3`pj_y%_0d{$c8^<+AY$LQmjF62(hjx9!y9hGP-jXv-h@eCKB?jiz9geF>Y -g6%9o=yM;M#6?DLO8h>Zp*h*E418r2uFt3A7~>Np_!8?jKxK-v4?noM)}xHydrV+@{7IkU!9Q5dkK0L -@FBqS+f@>^L6<9cwa3)ddBKXwbk6Z&GEwT0t?t$8@KpVagTUX#DECkgIW+;iAE(8E})J)9C -|vD8(=p-(1nvTtC#g_iJ@lYQd$Ntr0tW!RKX5>7m^wC35?wjWU`u -o=zqx!m`bhXwNssZTN4;9a%M>(LEZ!H0q*cjh1z?dQu;W1aOgDuB%nXm!Yb+b*Wo~A&7z0+cSG=RK`N -qrXi4GqXMS=jMnzN0EWS-94%UgEcqmef0`%2I)rWF)L?t&a0EEJ`??vYU*-#HaKnIVC}Nc=I -I<{I#ZXYI10S>X46E)fW$yN$|NQ4Kws*eR-8LJ-RfE2&Xc;ld_Hk)-eA?XB=OqT!Vcx=p!x#_}mYt4) -C<4_&3KitrX}K@?InZmHb--Soc4sIp&)I%H@?r934-07oN!170I8+L>E73L^-{t;iJ5_idc^4>-PE=uXUeNAlWIRv0CS~d7D#6 -<~nkAZastB6-u&*)djDMTK{xILPFX9j34C5`ibBPKuhMQztW=f3{xxtN#O@YwGdh_VUJD8VKN9R0xa8 -h9Do@iY~Hjdngg5gw91|LQG5Lqy{}`r<1>14mmU{A(3JaglN7*S6wE7O=FbqOy-FQ(H`Q`l6wUZx?$u -RMOq};M(S>>$E`ZO#JLPjrjB@H2@dl>!lB?@NUe(E$~lh>uIF}xH3XSa>j=>D-SF|6VuoqO#~MR;SyT -hS~3yro}GAApMoCQgn?8rxc)?lvAbv?Lmjb2F6#yGtj1m)93?`iH%5yk1do4w0}Xu>%c^hE@OBNq6G2 -HG5lvQ5X%>XMwA;Y2FD_lv$T~VyGa7P(Qft+`3)sBQ8r90arcDWFiAD`^sHV8-G-Jv0q6JN~fWa-l=C -WC3_(@IUQKhj>4Rr}=Ebmpe_FsWyB+jPS`C+y&1=Hk|2`|}~xx;63R?o~HS=hc}|>cp+Di6s;Lpn){4=UZI -3-a$6t8{RHuX7JvWYA8-yXxvf>Q>!?Alyx#)q;yTGY$G)nw8}wyp2cJpAsH0C#Btvw-8441WRp#rc34 -`8&)xf3^)`66&;71f#F@la2{xKlcAPH4#0Ko#J7(p2TsnM%-wDBoopYWhJyABKb~_#S2AeANgo{$>Ha -bZ@&S$bY(h8#=tPx?=Do+7CNdCxy{S*bdH1UkQ{!r}8-tTysJ_1lKA2uswPkCQpx}q*nh-5Y -mEHxDOb{Xrto#nfZruiT_|s;-+LiP7z_^>x{ogKt}8(;B~OpOeb61~@?F00XQNC&!YmVSUeTt&yzmW#sSVjRq27L;?Xw0b21kCMja$Pkp9$gMDSG+ApQkr>_^rXF`#p(JoRUP27ZDafkI5fIm=@5bn>0! -e27@#ztHGe7tv1ituCs*xRo7Sm<5P2h_rU-=2|vnsifM92jE2DgJKGryFi3@hgE%DXYZaeQMCX?`Yw~ -fI!%7H?RoS3|quatRKudv$@TlBoB;ko`Nq$WMGq?{5R0O@rHr@7vkfP9|;Y_+v-sv5;TRik--69JLkS -@y^dK$|RuF~nTMi~keQ%G4{iiF&)~84lKWJXH=}>Mn}=tB!ka#T*DUuYTJb9>2||QJ@!$%SW8 -7wRW13XG@}7T3vYWV?^k6kKBrY2oq}&kVIOOvWbhWud=eAvffKEg-N^U~vbPN{bQ(CAV?LKv6=YOWZ=R2)*yk -N7q8@L&26zCqD)N`;XBjkQPWpz@7~TOycHNWf#TGO$sZ<&UKbd6U-_GJWlLxh=jFvx21MGgwp?j@e9x -4`o=6~J?2}&jsb@$dLy5RP1IjarloD75Tq!zTQ{*@O -?=Aug0*9Iih6Q=VBq@@A)ws6t7Gry>hl5NSJE!32cyt6z$2ph3Ea6a86>3?ErQayD$Tqppbq+&^uxXA -Yd5{ED0dXBkF)u5fB2AH8+55dY~0t9E&To-GPYlC3WfOHkT1bx1I}MXbkmcuNSce -h)j7vyA*OUXWc+HuB{`+Vjlpl6q6;wq3AT4@!goH<2_WYwVZVO55QPA`0~py*IG}a??fh=gyN5tO+sb --(=sZe`1`C{ku>s%w8UpP2W#TAS37$7X@iH!8JGF?LXrnbRyqqG*;r4>+a7?Vu(;rC71yW(OZ4^ol_V -k%DsN803vnUB>M6yi%1e)1nRJ*E?oy(urZbd7Movc^T1v~C@rR%sr#BxaL3&dfvIo85H~3;)-lUTcH| -ec-Q}G2yETjlpb=e`Bb-KivN~FW;blKZ1dL$26Te^%~Gm2pN& -bH5*75DS|}l`Yu!|@phh#qF}`Iz1~CGTeYd~w8jU=W`5h7KAm?3CZdz0uTiYCG{zQ?WBZJ^JEoyhQ)t -TNCaS0MFgfb`6>KycKvDkz@d< -N6McoAq(Dj+`7(wxpKi_Td;VrY|kFpI;udUGXmu(_tlmW6f=$!rNY|^jg>yY%3ah=1C2tLR%MYOLxJC -=p%=$!5u(^9ONM4qrRAwBn#**2^C3t}{FlDBTyv`!)n(OnKEJvaA|wH06||0`yGcIXsDh_EDNH**s^; -jljl!hzv^8hgBgK*3wQ%>5j&?#Qd(O^-I;%m?N&tf=0U7|MQz*q3eo{L_eNPEOnR1Lo#+Y=BI&DXb(k -Sc;Yiyxu2AMtD~(+*3bqum=5OHluavpS8Blt3WFv{ --0Hewn-cM1?e|e%)8tt#IJS9IJ|KDzm!)Pwh2+dk*b(gHkh8zdYQW^tbGt7tB9k@#guPHh --MzNx6Xo|L0<(#!JqAE7saSW&rr5(&nj2$1-bzJF8tKk@~q?0|$F(F)T{@2;*39Pdsf#c}EQ~-$;(A6 -}^W_}i=yce`~w^4bH1PnhAq4<*)0RG--{pww=HFG(7h_=rY6-CoFnev$S*NO`(E8~=iUR+1;(8)t0y5 -MHlSh4S;m@)j&lZG@6qPR}R;iq2zV4T_)qT9C}ed+*+_i#Y4l7Yyp0_+VQnH2Pnb#M_FwakV{FkXZ4M -s#z4rOCL*p9@~PSvKt>GQPZYoM<|KVho*{pP650>{e1oB+Jv#DO5lB)J{Y$T;arHczZ29rAOu8-yRD= -c$|-_=*sZX9OLYin30QP`tuD=R_(W1-GIXd!9M2t!kV}YUt?H3O6JsAE*nBOdI4qv!)T~N0vWSPj7^7 -7sz&UKz>dVQ{B~R3o@8hn()uyePikVm9G$6zXx!Z4!}W(sQ3>No4uvSZR$@0wd6n8m<&L| -x6%zI074xs2mZO#1dKHW-w-3|w4@Qgwe%i}D*8n}dmFhgI%L>*)OOt$`?Uc)Q3_xvF5ldfw{FD)8$?% -aGJXU_T+D>U$-JH7(m)>e6q6cTAEQNbv6uvimG-BM;FvE|N9kL-WVRHLY1!Pm|y4BQr=+u1@ -n>WY$;P2Jd-{_eA<9$LGsJF=&&w<2>yA-O$G&PCXZJE_1p7m0CID-7FOK=K`MQ1*6xVE`;qYLn=B`!Z -p<2)|+*H!%$3*UYJahkr&#|sg!%FN5e+8C(s(Gi~=0!akVdMY)x -ZH@KVdpU0-Gb!W3GCXy{cq1N5dE$)9sTL5M`NBGVws=r -)&K<#{X@qfAD{fLJdx^W!&cw+p@aM6dbmnv{6Z+8teut7nThLBR}><7O#-9M29uN9Wskj&pcMF^7Bi; -AmoUkCVX9^g1`}!kFMfY$J0kl0%~a~RnME$E~=WXsNC5Jwzs!8cXl>+;S)UB+1Y;S^w88=Gq^fE^Ff< -5$dX_&8{uA?ulgy>*@jiPW@d}Yy!L{f9hVs3q0z&}NvFEoFPl#1jdGst=4YLA$m!cUR@qhInS1UEYsz -?d-YELPyPn0I)-t^{xP;ZyP~LEQ?PBTJV_kugMyaVNZVI8tfw8CDh&q_-ahq&x(avjgyCJ$kBP8KJwT -2_5%@N6LTN?txF%lz$VAOjV@`nSvlS)`T>T_0vt%l>}YaJn8cm*NR%NX_fypka@D>^CV+S8>$G0+PRM -yRUMRqMh(u?$!G0^vTERzl>rEfV)BT`QAeH>*S{Qp(|YlZe&!(xA# -s@EziVt=WiUi!I+0JgjBJQq>GcD#gC(*>LyW9FkDv$n~MOagVR74&7AI=t&NMCv+X>7p@UwA*&DB3vZ -?Bk&R>pfpb46sKW`y}9Y2qkyGs(>l)3BXGKExd!Q+Z$6j!@awUi9Q!v?qX?y!+6+D~_X4_gmXc)H4k( -5{bA=8nX8f>fK}gn&?*sK3gg#F%Z<(wGFOz*Feun=z{LK8J_!|GAP!i43#qx88(UPo&GN0MtO= -(Y1{;F3?Q{`eze!u>Rmx87y+Ng(A%FU3az*Swq`1-yqPeBvVhow)Dy`414c;KsKdQ}$V^^O&*RbRRGB -cL1AUOllYV;|R$ti{cSVK3CEXMiWo5;!Kl!ce -1m0kbuBPLwb+Sd|<5g#bydK|4eloN9wW-f4TFK7TH&J`HVj7Had&5HtJB@y+G_bg2(*TG$ru>bms@Md^Uy%sSwa`>+M}R7tUbDDV~$e*FvKvD?GgSM< -3G?vH2VNV)ovNu+xX6J9b{34EtS_7nv9-{t|7vlg4b0tx=CYXA$j$T&1E+HLO;JfcwCB{XsJ82@;Kr;^eQ%i5;Q;Z94 -}$;=A5#4X9}#aP&X4L4+hWA4_;zg6X9Z7&abYBi69*2cO^R)CR7O!Yh)$6P2xMpV^yMEJwib8@~$gvq -l5$?@GM2|QchkC9LZ8{++9TqFK0glitxtYy(B6jRc9enk;l#(YD+%yZsgI!9kqJJD{1p93VF#$ -9NAqjb51z&~4K1S}mAM?pfYucTZjeAfftrl&VhFZZ@5P$&59fOSng#8Ixa+8fPp -4ai1Y&zK)0UD;=pp#_~s$(sefj<%3e#dP{}_Ep?$mMaVXh!*Up}#6-2i&&S=*$L -uj?E_vBlhw`R7&qzmN^>t2G>;B%-65}psW7T!?wJ)s!aQ}hmd?VO=L67kvRy6Nvtga}bYX#~!A+Xi_p -_A`?&+NxYHk8fN)P()i(DwkzwO{9(AR?fvR2%pdloS5!w -!Sv8ZY)4Fbo@bQgmzm#3&Mz%vL(-&0XQR9{3Bvwe^=*!r0cMZEOD~e*8v -YGS>*xs(aahnIw%sbTGJz5D})iqu7+b4y1P!&7*?ev<-v3BF@T34^fuh&1p(i?tc%@l*R9Mzb)*!I3$ -)4+X}D|nswWePvVA!>sA14e`$d0j7^&X}lHon`MFhhHp{50%?!TA_Bx1UF$509sB&Re2m~J8|kBYo3H -!&UvY*y%J@)-2GoLFV1!u2TCCDR~-eqpM7#0x_yk*z=8KQ?& -L>Obh0bzu;%yfN&QUc*NVK7hBaZtcKmdk1kFU@9mpakN)i}9UX(R!V<30TU*$$@Y0O2TKQ_&Fa$}xm_ -hhDE|C`R(5mK6rbd-|7M%Qs)`Ccw`yi!=%$MY$U$jd*2uvFnSDP}Ok?qbM2>*H3MmthCe=vYqoJDDc* -(*g|^qb>u9v__?_Fo^pc>{y_S~PR?COA0xNAT+O^_t1}BGqKd0va>a~E2&tDxJ9|z~B!JF?-UIj-dXGhS+5AP4&y*qs0H9GS -a8+v&dygh}VM~AOJgAYML4%sJxcHg@;cRGq1W>FtUPo?`b8E8?d08khR{R_^2&vXY2{czJh>D~Xlo1c -H@gZ6!!pXP@cJhEZ)YuNeL*!G~#rV}``6!!0nV4RFedjmU+2pgQw3$bfpen%%HHGRlq$LMl{zRbI0J} -?mztvq0iuw+JLP8xP^kqm|kcH*x&b%%yMHD|FG}_=Oc8GUowbgBg7Ae;{j&U^jgd{>irw$+ -WfGT>^_we5K`K6B?S2)se)eV20x7s28<=a1jw6_L!`&rKHi -f)4@~Le0`5E|Cyy+I-v4@7HeW>`$#0DLJ58rzaus;%(Mto1G{HD6S)htRM;YVEA^qvZ0w@S*loS36>9 -rC6PGkpo0|sDVX6GDiR)nQHQ73hi?wPKR)mO@ZHh*VIO$?`!hsu@MHF4^<%+hK=c^qWz1Z*5?QzTG(q -qGf>&9|$hk@Z3F0-MH-ETJfOpKG-m7cqOE|)Ro4g24p|v6z$EBskx>Fzms=e|a{7GX~^2ns$-GFpU!q -S+uB^Hc^=o|K>b2J=bQ$J=`*GcfDNkVio3rsUv98=|jDAPvG(t;z?gzMzAg5RUYwh*UD -k0V=T(lZXLVCBuY|M!14fj68xKssBLudF}YS*ZPU9#C!qJf$Dtp{48{@|25B~$zsg`YIQ%%Hp7`dSB{Fa261S{rO7?Hw -0x%Ap^i+&gjsmy{S};uUoE$3!v!zKfsRPfrOi9uBmQsPUEJ)#k?N&jQn3h+zqd#{CO-iKoL#;Q{sJO8 -$EpQX&wAN@$js{eiB#Q1$$S7$vpU%-r<2J@S0!)aKratWPvY}K59GF7?ODt#v+dmw-Or?0~Qn;6}ahb -d;V#Y*mK+$cUpaJC=j%g9ffKoFEJoufwfVF|WCT071v9M*mb%K|?%Y6+<{y*Gq|ULZX{agvEKm? -uo_0cuM7I`dJ9%Us<^q8E6PNuy9`J&7$RDXftf$yGw8oCHwF#1C^=LV3#!NGo)nc=fUuP%I_*hCYWKA -E>|TJx&- -Yq0ax*Kb>EcvOA0y|dNDYTe-LufE#-8vgEl{iiQ?zxwj4uMMI4@TYll8>L7%GHuLYfHztK51i(KL~VY -Kj~b@jz~bW|(6?enGhAlaxt?BJ=38f1b~kD>OfQzUdAZBp&C=5I%DW@k0tW)NcD|Lno-3081kFWC!== -0yD%f(1WO>1MCD$(DT?$!kUKiqdPVXbYz02qd3Q)k}btHUaiQ9R5UcOR-q7f48P-dC=VK3a;-r4Q-dY -vG|Y)62nV5`&RYva9bx+mUq^6c9DgfP+)^wzp(`;tkm)pjj%x-Vb4vx%yfS|aXh&nlg;ZDw@U@Au*iZ -MXU~Dg8Vny~en2IP!Z8OX>Uy<-?jim>uI4%DvZeov9GiN{6WDj%lYWLWMIbVhZ*NO9%#%%+ORkjWCr28l4!MvxrlrVErSXQ}clQ94E^WrIgb~5Dk -Y#e49{gEY!6|AGJnKFy=hol9%gt@ -G;x>eDf|mLwpyx4ylmQGM$1T7vY1EM=>0r@Ox8X`0|ad`)NZ9hPng@Gh)Ufgg=aB-^m{)J?H$FusdD_ -ZGo!+&!X+ww+_A#S~=`=fr{65jJcWjiRE(_Flp%>qa1Fh~M9&(Um7+-CJdZo@BSc!f3UL+vwSNDE3M; -M!crLo4BD0^>AWoa-t!H9UyHQo4fT$%QhIO=3!~VdpdAXVhrT_U2#mUaue7%De0%^}rpBff -Jl94WE)~Q9Jh#&xwI>t=nT%5Z^90qY%BIfF1PxXgGg#xx}uY{F`xS7QwxHwsrgF$*pr3knroBS>()RU -+5AjSi54Wt93_FmA0iHgQS+T*if2?X`}a;JG`;nsv--=F*PbSa>y65g10cB*r#+6`L1hTR2OZ_xPV?f -Oc2!4%qsH{v8)?Tjd71oTFdrmOM{iW46fbiCl`MQq=P4!R31KUl|1alqvWMY4%^=2iYA;{aqhfw)v;9 -1z-Yw>Ec`XNQ<%6h}%}j1w4@AOkWPgf(gIq?0VP-|;j369y+vLl~!&9^GL$G-FqUtrY&SxXXJM_vfId5>$7Nd6J0Svmd8oeV>)6`(qM` -#p(hz>dXmY4ZKMQvx*bQMMygk@F|l!J2!0Bxk#lz<%!#SP2sAOm#S7Vzd~b@FWg8Q_nnq61GvqG81q2 -i_&SQ45kkO7smoQTUwcWEuNWOGQDYGF*X2eoyc1b28D2nCt5`*|roEM+Y#n>IREYC4c>2zhLOq5QYwd -|XQ?7tbfx+XI-G;Jb-L5H)RVG#FZV%xry*UF@3o=F2y3}qBu$I+O>tVSv7g|N1?V9+A@BRXr)=l&-1# -tF~z&tNwCNKP8`c{EQA`h2>*{6lnZE5un>T6KT_hc3rVpq!`6B2xARyroQtD3PI4xn+m7YSa(ZG=3_6 -o&-8`v#bCdEFJS5dY0eOU|7NtH2FXT<1>NN?H%!DciVsUMm8&D6#}QGz+o4pL%;MW6l|rL5f8~Lu#lX -VrlH(?p#hHnGRefo;B&XDr69Jr<`j3;VXQWEM^oE$;fBff42#^Z9&pD4z-6H#7c=EfwcfRaT)slT;wU --qAunOdJX2V*k->aaY#MEyFwZL0v$&&8_~yYKOU2AMh$YJ!c>Wvsj~F#KBEK;1Gm~hOQTxY5&{Wjvo%Kydzd5H$ -e{@t`wP5)mK)VbR=c<4Ar!+H3{L9F#PyqeV_1=hEsVWhw07A5H;F1}5k+L+C4A0g(T+F-VdD)J(JnNU -5GbFiZ9SPzVd`M!(#1FI4U95h4T=Kr-+60=YNCE3;<=l5O5RdEi-NtkBro=Zoh`h`5y;+ek^hd2Pl1q -1D#7vvV8sn`YHV++clX-g?vWq6u61A8+xx5Gt-WUXb|!-&De6Bc%}K>`hs3MFjN0N^`_lpe5v5yj=}B -8Mw~m_~@4ts@0%5;VmMMa5!WM3^N^zjqf|)$Si-QG8;Q*uzWDravTCs^UYze{m4(*!nl1`p(r`Y*Ma3 -K?rb$AzEcSV6`aB%`R%5F-(X7|u-7vf{KI${$X^SH+DKzDdhuBv!CqgxFiTO7CxO9V02M@9n<4bNMtk -ahGmTyfH_RWMjXD`yZN;;npxcJi(gna#ZPdwbM_03JN;G(|4c&r^U)Ea|+;jHQhxIXFCqD*` -AQq@D}2D>I1`k)Ect2^7=u^2?DI*w4<(j&1PaI*)KuDv9d1iTUWQU~NtPM`w1-Arn12O5DBUhCt<38N -OEnft)FrJOm(+f&P`JWZcm*v#P$yGc0S|=ZUpIWN(7^}gy0gOYvBv!2@ugQTP5D`SPsfkm*K6B45*o% -*u&7P!^YnC)W_@2ivfR=BlljLd@{UL68xQ6gEBM7D@{0cepLh%&@qdXwtg~%5?6< -WebUfCF=*<@`gLrkYwAH6#QVgthF6NR=D@PrZtwrS3q^S2=p#tl69Zgqld0SSx9!;L9DPgs{8Rpv5)! -AxTrlF$qKqn^VBW5Kj*#Fg^fVkR#6Uv3i(P|J`4frRD%i$kVWjA!7wjG0|2`X{4m2LKi -pmPyAO~&v={n7iuv%b61Xp7Pz5+G-vWWAOn+jc*XwK>=aC;NGQmio+0z~&RP^@c#_W&(Tpk3DHdtg)b -3n+piZm3Wl6xo#dkf8wdxQM44^t!K-(KQYZOZbLDh`pk?Js3{B?6IXOT6%==*mro6PAtAE<&r8i245w -5(O9jIqoX~nUQrI-T36_wb6Sk!A|AiS4Da4Z&4OItB$K>>QYD66ukz$yzIlEyfvP!1yJVx=zH;jjvoY -uE6SM@rCQ++C+l1{aCUp3U`hpjP!B05r@u+6`3`2;A&W)-25$x};@#4X;gI2Ck-6`)XRWMeqRQ#-1t@ -wGsidEI_w`x_j`>$M8_1>%3-fDi6%hcJM@B{0hKS?M}>?lvuSlAkk{EjPxoo`ezvKf(XrI@0mVm8wu_X7;Rfj0~ -F=K3IHV+_sr$M9FslU)GgJmJaqVyhc$=|Hd8uJCrj>_Z9<=|XzC2%GKjByAvbSunsD!?zA@#GT>l_>2-Wa{0BVw`A{s}l>Kr5@=5`n#j^Zt(W#>`dG|s~4Q6S> -8F<+Av{yFBb@5w&Q+%{`@D8CAF;b?EkWF@MP|E?PSdb$&(ICnoPUSJ(l&ENa_O7)exMpQFfAjHBQ6Qh -~Odab@(w)$S)(~Y)Oi*hP~j4z_J-N#ju|0(!9wG&?;us5`JDPw0EJ=o(f(VK80UoLcFe)k;c?`Rab= -L)~e(h4fsvUO8ERCVTh<@u4foOHsM{PI8p+pHo0x|7JMUP-T>$l_X3F+fo(|*!yCq>iW0*6D~9;6*an3FF; --OstI+BjkB^Y9hG@-IgOfxMu&?ecnf3a&aF{};PX||S1>M4uVH(WXabH8Nn_bais`f^-w5ZRhHks7dD --cONC0iZ3dv{^A3hbuz!U1^#wjT>}G>^b(E#H;fNrCYR8 -L*eIKoUYh`Jz+*!Ovv*)x1#p|o9_mBCS1aKlQBXL8BJb;SUHUGl!(bV43alfND0?l%AT>(L0)dN#Lsk -_5jkH;4|~qu$=A7@%MyPu07ioflqAE;3#3qA^wJf%$KBMHmJd5}u{#W0NKkmEW&g0B=1CFj;71sBqp* -Fm%^_Gp8W<KNl-XY+yS@l8B!mL{X566f5KV; -jL{jlSl@_42yEdDp-mpnV(*Y%td_=^p4glru5@q6jkr`I3W9|*andc><6Y)mb4NVKc%1bOjrKV#H -yD681tU-z4|0DVgS133m(6^q9;X)Cen^@E0SuG9;7{5h8@<+P3^kYwD~ttVnLXyInIB-*FeoY`8aF&O -JmJjmRFE@g@T0JF9r_hiGyH{cGo&^w2XP${3n>#CszviqPL^G3G6Ex1S8Wb8RI{-*QdntBiqSzJJfKA -+MH%mavIKF=1M0T3uuS|IopLc{@XqFdd+n%g$GdAs_!+`~Z6oYBe%hD!t8Q?1(z7gyp$F?*rrPsze|v -{9k7@1(&k9K$7uOf?PQ7(@QWZzpidSsy^J_Z(#QsLiKFYAbjuoTW70KaK>|-Wl(hR}Xt$3_OYjy@(HD -!>TA-})MJ#G9+7@&yBAqC*nEZI|Zg+J3$eNN4G3DXc;BvMbF&=5pm4N(fUMCiK5G5R)@MUv(rC6+20X)Esm5?=Njh$p=oKFvMkGy<*Cj{$kuii6L?_~;ND1j -gSwZF`pP&+ee36#$Le9V>T5Ru&1-Zgy^KMcqUcD-1y -{$gDPhy3Ao8s;9er$?lh5!IPU0lRy3O&*{yFax(t&pZ?gg>T+;phTtSYqilZ$olxFT+ -ziZI9zB!t8H&$`0vl-u?Wc_TXF>a)?WZAt+oT5`oo&4e_XUFntcu{g4i>Bm|HMxngqPv`^Plz7@*^7b -zH=e0D8xNpQRsdk)rXO=OmFZ|vEpI!!ovgxP6(^KPW*DiN>5kb_qPm+VuzwPnJW(HBKCVHO5_@{+f>E -*)a}*;9K-!~-}b+i)t?(%%RO8`ltYsq>N;N5nEvC~61wV@rEiXP>DXW+bPs=Q)m?Hb_T0EM-yDft(Z} -d+g>agUBqUZF^N!(Xfq|)XG(a(2(L!S_=!Dp>mRmp}oQF(}GuU+XVC9EfTh)L3G^5ysw-kY^!L07mau -|)o^1F!rn-Z~==nUnv5@jQ={M@J?tuTcjhgVTyq?9N!AkkAq*wj)lrnV;mONGf4%#1|CX`~ZRo9R<&I -wDKI!Pgq7ff@{){tuvS73xVUc}#T*ObANqHb7EZRiYo?tWk4W$~!U1qJ3i$$T+i$wlqd3qoj -nRStPx*+M48K2hnF;(hNp(+R}qE941DN#`iVxVy|N;jf`8HLeM>I$U!`&k$|WY*6TSXuHO(H!r_G7Oa -vPo1|*<5G#BQN1du(l~UZp0hv-AlwnOC`2Lm{*D$V>UL0%HBL%Yaiack8ja)ND$NTFei~q^V785-E4O -k^A!-+SanoBPLCFcUcT`ejn;*En(@Ag&?Y-9Ry~XyvmF=BzdoLG!H*f~+L6v`Cd*0QB%*G03h$)B;Eq -Ub$(hg=tLJ6n1RdIGHKxoO!FUdIF(mX@8I#Q5CohmN(Hg;h$priO_T$h*lJxZ>wG0k -tyP)vf6ckDr;plpOEn8Fg(2SxUVxHy|B1B2oW|M`y;o!STx!(!Vfpk}zq2xjZ=iEe^5#An4r$*>lrBq -uZ(CSkrz|)sC|#5KeH=$)^Y^zo9N~^Q`sX3~3KV!V>PvG%YQOn3?C`C3lF>b(4 -AzNL9fNG8u9QWJS6p{#Y_V+_6TUtg1pgl#(duMp7j0~(ec25z+Sm?%!xs1(!WM7&kI_K;(q^>+7BCQk -1b+z*{3R&(9RIN(c$a9CJE`TTd{M2tTbtaWbNM@?|IIUc;JS5xT$n?8Vcen!tBx~NmPA!vEYHRDZJfn -aOj?m))JBAei`Ko(X5MY4A3zI~QnsY!<|VwGc{nxUa&T>r$fgq{3uD1j=iQQb;|93efK9sxUs8jvw2B -cuHzi~HT&$BEh(=U6V#blmG`4C(nawlQzyR@_PAE-j*y?q^+52Miv;Wz06#dKtsSMWY@x-_AkkX~2(f -{m<6H<)F8ce1RKZX(dYhZ26LBucWhXPMeJS|XZKda(NvyMh6u38Tsz^3*k;W&~xD -C}mCFe{rBs!51d{CmOB38(1BEBkO^71lu3DDtI7n2gY4?nZ7)UIQYjqo<^JN(k -c;_3J^&$U|+UULZ!6G3fS)+O|MHEmn%umuY}LbC1RKlBE_d}bz+7z}#79xxELz+<6?xLnQPka^!V+I` -7XrxLwqH_7ZgAJ|5)GCh6RCPA`|Sc10TU-beGcy06Mrwl9;xV@F|xM2Y(Y*GBwSv-D9+2opxwglPc@tc-|wz3TO5fP&s^SR)aw9I9QAXG9Jl}Q*SXneqy>#A8J?~P7FMA7e!jy5JX<5LX`< -1mHeL79|Z5vu==jSCTdq2VzofEcO)g4e~YjMd9c+G*r2D$s7A?BSQN^(x8` -#wec-QNb!xUZE}3VipIVmmb$M3|NY9MSqq>hdHD4kUwynT^6O%_%=@WcRD^~S{bs%Ghkfdg~1Nxbv1L -#;!)To`D>#8EpDV(TuFD5Dt9py*t -L@y8^RwIcorCcKp;ecSo!&rWJvjyL2eNuO~N|ADA?JQ=}sibYCglHtR)<6Q}&z~41S&_lso<7uPA6r5 -bP;X#8-1HXhaskdsT#@1tHZ1ai#&7h+91OA`pWnHpQ5phM7=g;1y_LKJBnEp_VkA8K*8&=?qZvUvZJ2 -7O2t+@ma^R#>Y=spg$$_gJPw}cy!7FNyDX{hQYuL6{!13DaC@U)+?hQ;T9KYY4LAvki`MQxY-fm$T4v$fu6{2iuuVNOVtyPq5+s -bkseSKlMryOT9`DVv$AhzD&Jm(zuQ?~N{z=8OqcI#-;Yk&^pAV$hhl?In$vJ%P#HS{g9NSor>6G!C?e -WG(`lW&wU+&3DJ9tn^g3_-uIxy_8|&tM+#L(npW@Sw)le=`_l0?wj@3Q2*BwJdRkL5j*B0Xj_6ynqHO>ik&^(jna1G=0IVQShQ1Hfe#q?7Ip@i##X0gqu6Mw9- -(a--NZ6wAsLAHB9WjZ9=uq!umzM*9A@f}K}^{8*z~Vl@fPG7FEkMA?YV>a=0S*~iLdJh3h&u}0IF0!8 -12O9P3K-MHE;-OVLd3`92%>!qcO8~e;P;Zo_*ktb+%o0=CNW2i_?+VlMN7g@xVjVFUzhS==$XiF7Qv( -nbq{HUE(REJY)aR-?e7y^BAXVJ#)*{ua7}BzpGS!Gn39}`K6{(`S{>@{^v68a9aF6m;bT3JS~CIXK8<~Vp<7`CJ4hcZo{;SbC3wF3g6HG1uZF!k*bb(Lz$Z>W1qE)OS%>s#~;yJ1 -zL=41AO*Rjc(SE1gvRv^~8>QMmHU7cxSX52B4>v$RqrD>(`&Rf9?nw^FZRB=d} -!9c=1&_<5-*^+%eHJwj%B%@{X;%BqQ2sLr*_*^l0RX;9-x37x#?SJ=%w27skqT1#Oz$I(zT&wy4o0U@ -7SkXtA?0E$oxlhcCAoOQE~NNXLTENWe`F%Q9MXfX|gPA&h4Ozu;#E1aSpbrXQv|cP)J0?yY|yI6c}Ac -MKr2}ZDdx1KN!deKMFoa*hNM;7@$tAp>b?GpC(m>YnEg9mnz_%93+O)9<1XoRfz!qanm+~DIX~Y=Drg -t$3kP+!oHCW9hJ)F(-Mt|H(T|RWj-q7QI;Jd_J#`W#@uZ`_BO1AeJU`y0{^qVrkVc-D(u{Q94J5HF5))X`@_j#cy>h?IW$e>oI}Ea2 -x+{4cxYz!G^bNXsUus%dp*?|62A@(!=g_nSdd#Zeoj#^&kRD^(0e=uu>1x_u9*WB|AAS^&$U!PXu~X2Vobt+vN@uu3iHlR=4<+ik@o4ENuYt%g87d&G-2Uv)xqJljbaoo@852=Oly89mP7 -#qUlLYDnvJ(}pV-2q@CFSoDx@kFF}+U|B!TnMS+z*xR -}A3M8tevbe(v|)dYCdu@q6=DvJsptT~FZ7@aVk1Dq@a^ng%5C^f -UVOb^hb-jSDO!jOF}7aO(Ur>72#X9^XdwO?fY+JS&HqM3mlJUrcvo$mG&_MJcjn&)C;c6{kpG#mxnJG -;Th#lM0-{dudk(i!kA`;ki&VEP%1gtirKcegwBl6ZVlWd-9LIiar`K<)y_FKv+8gPv2S@2Qa2x^{>OS -77H3zU*{^5JN!jZR1Xv?tSIyU%lBsLbHEbYb1cP^+vM2jo8`wgt0(nXE58lYmH|6D{T7fPigwATGL+} -RC2CQO67HamvN}7h$8rp=OZ@fH>F-?Co|EE2hXuAoapsm;^J{sCY;{^6R#8~!N4LSMkeK%u1O`T73#&P;LKO#=zgsuAtReLr%)xB) -7cK>NJ&WpNc>?hJb3x)b+6aM$!c_V#ht_#l-3bW8h7?(d-a-oZ-bmAcjGnAZT&uone_`QQ%!>CwQf@5 -S#u4Ji0ddJj7Ymqtpix@`}fc$ylZl2(@nt|8Qr|+jG=iZgbZ3xLj7=ThZ@rvpRu;cKPFFK^5;1 -Q4_T(Ne1)wlt%l)DMC8cn$zT-o5oflbP71=FVAA5)NODS05+`*@O`LSbd0}35bJ$i8iKL7LU(r?(|B~ -nUBjo=PxmsAAihz*^$p);HE0KC^1yK(|BN<#IKyYej%|9xg>4plR5*3aOs6#C%01>i?QJRO&X+9L%m8_p4-Fx7jF`OoQncBwMrUeqwhe;u%5(Gj-a#m+!^b)kx%R=$W -!=iuYNW#JiZNG-eF1dtZD;Bbc|8z&=9B&-tI$cqqZZG%lX^uuHOhWkk#xmq2ldH@v0ceG~rBsDjpV2m -$SisF*zzzkt(Wrf5(X3#&r{KaYb#_1|-#9~R%O(}sFH4wBlJ;u^%=pXCxc!Gtd1~1;KaXz;|nM=|ECB -ywm+^cMuWh_XK>=mRI37r@LSS<+=_E)ZPrv*0T>gZsEc!Wp!inlG5@=`1wfzWT{RaR6wC>%^O;-Y;kE -oxuX>kNcc%C?{?J|kbUMVuQcSCk~sW+4mrL}Lh^5nz$cG`W#yAB4zC6LTtLRw*>|0;%3Z3hW9_<)jXc -4CBg0+2dI>!ykx*se3S}P@t_V(bbVLG?;sd*T$rh;*K&R)1(SDm67o}#FHSgI_%kCVA2fhSV)5Ds^F5 -6^2ODw!Jv@vi4)g&Zo8BSU$r@N?1aA9qt(fwkIOmFdi>h51(6@_7iI#{AbM)&Yq#5;McFR@&`tf -7#pq>d!u;Il>M*XMQ>!{ns-qsMH8nIq%`e59HczuL>K@Xqp?A8F^inD -Q_CpMM{Rb3v^NJw$NrqG`fg>{y;}LQ79{*$^joLhrG5Ijh+&@+0ce^K8vGKN#eNBF^7>VeW~+8lR1bO -Hw1^rP(EX9>yZ2i@-IUtW5P(&920k;e2{l6O*?GL>QC5i$xP6wh2t!*5D;m`XZFgO9l_AgPBpu)h*mv -rb3^6QPm9U`WYCryH)Fe8{ID2KW6Pt4+K;*%aBN>gcf=s73fH#r=R99@#8ZmC*jM} -w*YhA{&7BuClkF6>3WS$P&f-UO|itHp-N>PJwI3ak4l(ZbnvjL#)>7enkOxdEVCq;3yP0PHjmvtHA3_ -h7a3g;Lzp2H{&30`ZA7OBSxN(XAjwM~Ejg2iA8$rP?TL}acdTrSiW?3!jdsGRd|tB5Oq8#d9L^M`mBu -7UHD@De5wFdTYZ;@eUdjsys(V}wl<-wkK2NJB$rhZMnk>ZE0C1-Juvw1hTT^QxtK|6RF07axmEs8e7Ic4Er8qj@Wx75HhVru=SQa}y0cL6k$Y?A&u+%NzXfC$OIyLkrZf~Q -?~EFApyFpX>vOE9eGL*<7!Rb`0|>hytFG*UQOR6XqW;>+z2wK3Q3<1m+NW@Z$I^P)I}PYW$D)NLuvf! -1U@~Ht_SZ|=%?&gFM}6SFR0=NXy}3Sq-uhK2PWFPIw||9yc79bZXo^0R_pJzY7I%y%K18j>c9!+s-B3 -+^qBjbbth<`owd>twaVgY$FQrDaJE5f}3CUPBMp=1VCv{x9-p0lT>7eZ(NxqBBC;E<=M5yaL9m5%iYr -__ezfoaN{-k#?ow#~E%Hg4ugzD~1I2CjS6(Y4hS*}*|)&i1%Ovw=LNBJ -_mvt71|^f<*G&t;0p!V&o+Hb10d0y4oo(5X=FM|}-2qReJNYW~1$LA|X-`5&DOmqf=@apS_@dpu^sl3 -FoYs@4$WPUq@@c{Xn{q7FU -PA)5pRLvS7xk~kxXS1@Mpc4L1TRO!%!=w5qsRiZ3SMw_MRBVVrFYSyGBRld7QK)`)@&)9OSAV7m=d-Vi4xy&9K>-pJ;dMT(WlB4`3oyBICG$V=HV5SGEM~dh81YEku`?n&O&mtkut=A~_VUhDI9$!cvl}v=OIfp4TVx@PV9;3=gK1W0x)c@YZ5e-CnU_dR@*{l(Zjg^Q}D|JlUEvQ5?-fNhZ?!XVe;!l&J -9OteEl)%Cvy_h=fuf2~(gpQLx8}g;Dx3=9gOXo364V&ca03`1#5Ef;flP&?7m4=+%D%o|X)M`%#D7H! -dtTZ0mAQfIFkCK$c*%58gvL%9z+Ax^{Msq*7OV$#fczlc|Z5|cbRCW?|V)T1y -IY&jGe)`&*j5f)%&@>ue>k;5SfIQNB7p)0MVgh5wC68`Ku%Alh~GNM>D{TA{K38(cxTDn^!vEgGxFwS -|jL}HN!S>#-12PpCgaccUAO|=|XT=7r|VVsNd@;4x*nDP6UiWY^-7^Ftx76BXmCI|Z<=yf^ohalj?ANwVrMxxmNM{TCYJ-}Iln|(g0 -(nZy3uSUbgX37?(THS>OwzNNi}r;!bDN&t$k?)Lr7=np>~dm&Sl4N%^tLTNdwx(T%OT1*^>!KK=t8)J -XA5%!zur173{I0#8`~5ItBhiUKzmIal2 -hzn#25d#LH#%q%dzY7pVld56;;@(=*m}QTy9#fU+qAqH&@hjLRzSMmp1AsEu)681Y=(l&Q@T6KWStUVN_LV2P%;@9L!so3=xsXfOPu^sL7 -!nmypUaIQlVfB74w?~1*2ZE*2rwHFiVDGz+iP$MFV!Ls1GA5T7=pz9w#%3C^J!hj!rfwK;qH`@_Ezu+ -Y&mhi@$p+ty6s8W>PyoHXd7Tbk-^)VKq}RwDnjEz>-sUJRi+i!KBXo1LfyLQ4P|f!y26##tBLvDoFSV -3e^k)Us^vZcae+K#hyhTa{Qq>Pb>0L9p@Z(geJ(3<*KnNu3>8UWcG3j16PQJWAm&(f+d0tQLJ2f;KQ2 -d25nAr1|R48?GKj6LUEP7v7m0q+}q-qB!IKUB1pYl0yTr~)Ga3+5{y!9jLPo8Blz#z-Q9z4zui^#Acf -eFuH=tYk_k!eu#Sb=$Ag2l_T*Qb%JAO -x=eKkR#j+)XIlWqLPD(6UZY}#fd@%&xz5(M1@H5M!2}kdl@{in_SOqkfm5*m&jD0y!Qr$-zdDGRtd1%JaEG<92*vZBD=u0#7fJ(Of=SVFTliG6-ay!%N6v|LmOZj)@!g6Z;$=qW=R6c*Qp^_-@oGtNgMt$MbY5^2?24PM9>ukL -v9GcMO%=O*ldW(gBqBC}`~1yM*M}o<;p#*XVf^k{c+J(UmrB@XBtlht1;^c`JFD4Kot%XbwH_1Y&OTe -He*LZnV{m-Nmlo#Tj!JhoLBY5X7&TF=OcofT7=bI -R8{owpDF&6Re38{^J*(Ou$bZ%EZdfGW$~mjY{h(~*$C7wIu;-8m^?Yp259eW#C=>cHZ&L_Rf(hqCz;Ss@2tddh?eUN+%rJ<_&#x -frvUZaV4DP%n(j`bcyF2bb*-94}8x9XI)daNb-h0gQ^WT%5MJFdNWcpZsAcKdzx-n8kMC2EdCIcr9Cyp`~#SV?KTOu+v^P`V)!LB%1{ -$4&TBKOMg3u^nTUqrbJhBdT*m81Vc7q^y>F`;pK`)r7ioZdjkX2m>TvOD|>ZFstl0e)_t)Xa18uya -PLB*%0F<^a2nQ1mlN+YBZHQPhC>#H6+o~}&U4{ZIY~~P0uvw=Y)U~Qb?7*lO_}|#6gu4>u^AG42{i=R -{Rr;hjZ{hBEyJ@d+j+YCV`u-_!7YDZA?vZE700f}JI?Q;3Mw6k+Zeo;88 -DpVnf_lI#<3$yk$XoD?!v;OO~?J^WeB=o4Gy+mVJ#lKV_Edk%FX4cH_Y#k3m(< -J$8bpE^h-xEJkVdvbAMWJXi@T)Kn`SNUNDSZ=lXET_l83!|BOFvTmOu=5uIH;G@X9$xuRRd6R_vl(KB -{Tu^@-gr_W2q=_qBD=xCc@XA!RM(B~6fe3oIH+q$D)057EmFK$RCwRL7X-$?w4M&@1hEF(A1mTz>@bg -(#7Tofy}%8=?V4)Xby_+*YcV;OWnpUK}SO`d6jej(3>#jMDE=^i%Q$>4pF!F@6=@xNj -$7i3;&ZpxG7vUkK>qxc842=7}rH`E;PE`n~7=gXWAYk8lFc#5Q8P2M0mh5Y^_2Y5v$Z6thcG>z(Hy@= -)a2#G~3=xHUd?0!j{1P|26SNLFqT61YBBeARLqxJ*0EG+;B_ne=Ndyfv3~>Z^*6Lmz&V2&N*v(%d;}d -u0r?VJu7alCMV$2UWA7K7)3W885SRPNrR4+sfiLgDNgp^|ePm3|@s5GCiog*1gDwptMa5NdT7C^2Y> -WV37OCh%23H(>$Fs5Io%8H0Jy7fALY!@7XiH;M#*srvyJOHP~^7kMW~PV$cbF6`V&eBxq;&MYY2Wcpf -f8o$>wN^8E&k$VMBb0jwxqB=KP{8sdGPXzeBBr1+vdWbh)ip>u(|I_$vM4q{l3ooDb2K|%d#pUcAN+- -u?aYb3KldbdFY?5!@yVo0c^VWn4w8qo3d-v}_jcGc)ya(eypA82)#~ogy`^x4RJCnV0iH_M=jCNp7!O -Cece2H^>T>JXP7A3U9bZE!Nbz{<(8H2TxjAIW@-%^Zq0q2EaZ&|A?mGcSvBZHHnx)D`JxhJIvCdMy6$7U&CAC2r--q$%+DW(aOSVZUn3E0j0bem$YB%l;eDlHNdiCxsqvif@1F1s0~ -xqB#AU083kRC|u0aYu7Z;mNCTS~*;69)10~xAwaA+KViN$;Mo)p^z8W_-!^l9gHt3r0c(09##-b!9&ncA+l`gKW?sYGdf95_J!ARURNG1CXW;PBFja}-D%Q%QC_Iy`AoBAR_q_Ey0 -V%_NkD2Nq6A3KrSKm7FT6SJw%3vBa&ic-opQ$lHZP@3six5DL)W57OOO>^ZoRaGn7Zplil-KKYcA*x> -}h0g@HnSR+_B_h4r9$f<|uN2bGc_(O0W7Uqn1z8HR<=9F~KBB(0gs&!h%#rH&dtMw@Vv}iKje1~|JH& -IkCS_egGh$Vu9%Za(GV6r;IwA>g`;3RWI*99dsyz?|TOZk*7TuPKPa$d22xt70R_UX%4m5P@>FuWnK1 -fA{(?60ucQyIB@E2sce)yN8QQC_x#2fL9b9n7aif1suCp1L{tKOn7!wvfKis@OLNgWmP}-I;%#97slZ -0={V^Z&A$bwfeJcnAhT-!G~|qB=T*E3_XiXg_`vL*jdrY&Pz26aqO^>|9>`SmLud^%FTeg`Y75>T_x1 -Pz09l<&V>e*18$k(%?v!BpUC8+aH!{4bCXQ-MS$QFZG>s#Q}WAVA(!*f6Ekgs?5l);;o3>r?1EF0iI5 -G49Cn8z^kR36t6cpGjP-4XLL$Fyv?M1MW@PVRB_*TPUSF)$H*2lCU)MJ)b?VTxvh}*X^Lk_J9{j2JJP -#)%vVaN+1}f+6%^^9m^=Ht5N}z1LZBouvw$9r-uHe=^`04N;WDr?=3I^Pum{K6Ojf1K4{PA$Lj759=! -(*Dz9MiVu;>;w~i%Z>{^OP3@)&Yt)wO|phJe<^m@Q26p;vY1$Vm#zYTBGorv3m*Gru_bJ{ip@|Af=Gx -6P>iWz>ls7*RYYaplNT}mI?<355kZ4J4vmV?B7+Yl!7jizARHHq7dE#*`re& -Izt;rqJt>h3MFwgrJ3h7&XTwNkcxn~k%EuEv1jF>u^H{AK2%@)fb2B-=Zf=r;4SciN?{yD3R@ey~QXdo;nhdnHreOPpb=p6lb|>gyRry2EcH`bHMjDicq< -W(0j@%HW?-*7gWVwW1Wae60SQxAH8V8V~+jb&&tmJ54GFl9|imu#)71-cP4hb-JREJ;>g5A`SieYqjC -c=63bB-wKU80MSnT`m63mg}d?FZ^e$}KM!J^s9+yti*Hve#ldsE6%oMnu~IQxxKferrZ98M!uE2g9Uf -K-;HpPQW$$IA3rgq?+5Sj(Otl4?V7sUrhRBM&v&EK}KO3qe+Tr5q?h^{hrtQtU#L+9+F$LdL;`;S8E> -f-H-NSJFHsigLu>CkiE?@+);ZQTV7>_V$;}lb2UtXjxAg?>fw%vEm{#GkUSGK@H=E;E+HtK{m+8i)|$ -xo$mbKHBk%NGhni%jbieDWlktOVM -tdC8V%Q+h#u(UNfJwZFjg|AUN;0gMIB6E+$o0-6v0CxuSVwXZi1|9L0BlOI(hX1s0=uWI|c$n;gu3$prGgU+Q|<#?>qH#TlTYx94K9ZZO24;P#L>m -g%xY(oQ!^QpNHdHbRYnLX%AkkrH&-s_E|*`~xb)p|8JvV;aL!Q_-McPFn`N&99{WNQ2KC3CqV$W{?1b -v+WzUtYaryN(xb!vu3?*3w#1skQ1svJt-z^}Ba+zx^T-6~^Krdarjsy7JxV;wv&!YSkOmYu=uEW4bZb -(x2{v&lw}%$PL$mz^sn_pzN*-t`?9cdiRTe{1t;U-Bf*wurGxi^u)0ySykcu@PISBe1}$uU}JsS?s8twK7I4ZY_P0!yIGsx?f8-(G(RDn+g6~zSAh-^Co4yU0C -Rp3Y{)4G^Ik*l0v^lpW^v*kq`QS9QY -e1DG|+POY^%Lfp-Z%tx)xd6LAN4@ZaHqeN@o9h5=f}hzM(6>xbXikzxfk=r48sM2@P23awktXd8PU3; -(#gL$_hC1Zw!@5dj4!Y+aFykI*`(j-UX6St(A -@P!o>mP!>Qztu`nUsz|NooUA)+y&#-z@68}@4D#NapH3Tjdn393pz;4cx!u@k->)ptidL~2hK}Ky_>C -*$H>i()07P}EJ`)B=qbLd35OsLqAumll`=&o}hisgmGp)m0^Yx?p;}`v;yidjY>#Lb9dhOHcNgUOGef -MvG>GwJMn%4t#o?k^u_+SyB;O1*we9gN213Y}8Y0rPxZV^)e-E7SQt3_OR8@^|7xvy^2F;pC#O)r+Y> -#pvP`{#BFjZwId&O&LmnVIGi7toN~`h=Vv(ecO1{RRShh~xlj7*ZZQickr|QXhs@V2F%fX_ -!>w1KZ%F_*ZU9R&qt+8Zu+sDK(Bs?K7jeS1Zyj;_%GycqEVRqaOPhb7Ki{*qOnTy`t|Jf>*?#)qY8$&OLhnOSOTZ3D2K#xH8i -`_DDX>?y^SL18TT;6uesj$!|~A%-!qa%09;A#3A-2;1%%D5S1)#yxG56v*1_&W|1rt9TTj06pOU<~_2 -{MlltkUFZ@jYe>g)OX`tCZtdiKz76<@YL`+{FS|Mt7xr=3?Xb|3A*`S4O6xdiCDjj;sZ@Oh_m9E#89#3t&F;j&w`0Qe0gG7a43OEwV9y -?q8f`+DLa&=A5}X0SeW94mU-0RNAJY)*h8&;F6Ro{*dxxAd%-ayum*U4< -l64q%zO7@u;u@mj8gq;-^C@SLbbVAz4C>54{2aHqK`wU0uS@ngAzklpwMWQbQtd*YiB&FP;?UI<*B6a -;x@^Al0BA5IN)lAZNOBQZx#$&6Z(7x8v9Za7}{poj(+xDXsrwOV~`pY87M(zpq#_mmSZg>U%X$(+$m@ -AXyfLtB;YqbaL+v?TPx^^629oV=Pt8rg3EG_+qq1@-e&d{_5PW^qWty&Ef!XA5+{2($rmnH`c9F -lLGP9-=~nH3UjDwU9DkIcLb^XBHAj}Y$+D`?INRvXyiQdc8MsnV -cQD`>|)kQ3(cs+`1j!7#$kspVVd_Hk)Z@BD|3((x09gIuHtKY*frg($M*slB`^pN$WhhM!a^bh9v|D}*T=`6 -cbLk>aa^Z_EZGnQ##v?$?BnCtCs64mJ5S&CG0V?sBsvu(*)ioEHf%l#N@}Ct06&trnhJOEy?0d2}sTe{)-Fg#&NZjkSsuPIU9x*}ti+w@f2t73`6cl -|3u)%u4n}GZj|%1lqxSg?xU(6=8zBvYb?|J06~hWuUB4*xj?{WID!F)^0URjD@A^7WWwDv`XJ*0~_zK -uyBJx6c(_l?$5Y0|CUxdXW0mogpQ{e#4MNsDuXKsE$Zk;X|zBAS87krvn24?fW0G0)|l&jgcz1a0v4; --Ol-#DEzy^y@bT|^Qe&et>qE{wYNq*o&$<9*Ky5-ce{ZA2J~R>-3%H+JC&d;oX5t*1hWUgtg%6f*c;=a+8@0gqZLc6?%4B}L -hEHQrXgc!d!Dzvh@mSVW_AKv-SGsYciJW7-n)5;ybQM7f=loyURlU5nMQxf)7wV!YCfSs7d2a)QPRse -vgod6At9E216w^GMe>{N8Vu4N=xdf5HToh|O<)>yAra!K1z#=4BbdK4# --_-nIimJX4*jq7Jh(4eZ-Z(U`p#Vg)M?f)R10GSzOQ!^Yr&BC9cv3U}r*M*BS_$~SM`2;iLwJ -Ud6kxmvqPVCm4q@WRDdxlVqQ)T~X`-Y*qYRT{&KRtFqkz^1{WsYKGKx6l=hG_3Lng`V8}uct`q}oJFE -Gsw>VGzT%rG6;R+!Tf5(G@GNLl;^5qDL_t|5h?D(suUEPQEcE~yY=R_Y4yd)di6=Gy1cZZ68>+{7Fwh -ic&Mqoa83lEcp{6Idv`H*NXPOY17r1+T7qerwsh0YUoDoj5hlO&BQ5oIjQS786&dP)1`j?e(zuSRvP^ -bY>U<^YZ`%xGc;iG8>|YY0WEx3nO@-LNgF|e0Wq>+v7?+jqM#Y9a^cJXWch1v+jIKvM4m-21OiIa1yY -U!NT)^88Kbss%)B|*msf4v)N0xmW@Lbd~FaHzdIO+va2Am_VL*7_0uyrCDE_GGar#ZPu8O&v)5tC7(~ -|&9z9vNcyzDCmlPx%UryvSk$OXxI3fEn0DfKnZ4;F~r%Gg^PJpbAsn(>bXe0C`@ChCrz`dDpwD`BO`B`RFQ{Rlx?$f^drC$?`~s -wd>20zghLMSovn4sr6K0C|qJzsBVtsCb+h$B5JMoJ_)0+MDsT2b7s0x*9pfuRp=ylH#(7T4&`jLgk;P -h4A(qx|h|+)wL?ho)cCsxhf>CE~?km^%~f0_ng$n1R@vVjbdn`dhQLG63Pe)HcYVu?Di$CFH)^+C(lR -O^XXHRX;e0H9ot08;mjNg)LNzAtJE6~POXAp@sTZ&p26=U%>z?p$SlqKzy@DT(UlQ%ifo1rd2PUbb6M)-xX#5Rh -6WH`g@UjiR{JkH690hK!hhbIW8rUQ-xWD!=u7k)NeSY947&qi8e8wWsnQ!w!Qetg9n&gdW&$Cb+>|(mbVyopXsKGntDif(xAM;B)V`y?zywyD|I{-$ -1DPezr+HAwn{%w2ypj|+f-Xd1!FLKfv(7+Vm8MpZ)R81-vSei)160srW{PE$ -r#Gyhd(d*G5Iq>t5B~CcPvp*U_C64sHyJeui9IhqotohQeY2;gg$3m|fWL7f@pW1MOXEaMP@5t{mOPI -zXO?fya10C7Npr_Ej`~VyCMm?W7+Txh0BE4Hf?Vn6^&254dOE+v5PU) -pywT9KG7YC>&#Rv!i-*OS=3?ISsjU8Sz(gKoAu7}&Uz*#m!)l@8M>gcb}g+KNW0HgH&I5I`+QS%(jvJK^!9y4=~yjX#JIZ?emhG)8 -u(t!=A1A*l(rrN~{}N%1BJ -p~=uYhXb;;RE_b^s#vf&RYsYz;kFWKo=*h~0I*eEQQ$650<}c)&*TwV5{U|eAFd_U7P>UKM`7G62Ic! -XRoWZ(ll!gqgZ5|1=U;reD74jK70OnDFFdSr1^y1TRtV3(nyJ$BV$SYTxoUynP{O={EO9mFt<=MOVX) -I>ce8GgQO=W8;28TZh2ghEPul@jsRLwEG`O*i3kcfHO_oF&s(SRV<57on@;)I)$YwiuER03KESO#BsZ -W*?hk3#loBLaI#JQFLEl_VkF-EGuj*Vmyk0jUG&P(sAe -Ji)E^qHz`0Y?=ZPq`a(od3E_w7?h#562Q`Q5F&Ar@{q2y)*x1?;QuJ8=smJ3CeZRM9XHx6RU=xnPLCze1>M+FdUnq2zJN$_#zo -%b|!dmDfke}3-YmVd2q}~bHGFmc+72#Eqj(>Xx7?#9XZCg8OmX_oeR-`k(seuJ~k(#adJAJQrF;U>-D -X9hHtNdsl|C9ByVPN2W5=MeVFM00v|_A%xIYSk -BjbAk2b8!q+I$2Bc_Ezs-kX+l3{K-%n!EWNX_%g)Yap>1`v|1B>_;gJ@C(iQk26&dB;mGsZCd?1Gu2p?KnsFbCerv>kx;v$<;0r$TB?=incqluD2~7amA(`A -1lJ|2qu1|^s%Fao{)Up!?IeQ*8ApzBSKB~;wI$0BKW^cx-h?E?6#8U4shRyzo_DaY=r}%*n**iX+YAV -uQZTToh?4Vtfr)emy21k1Z>4wa}~8O3st)XJptKcSrZ&zqA}%_#qUbPs -M~KFIc_hFfZ>^ikNel#j+U+cBvbmBk5Lpx9ku#eUU%@wNvVjn89|aC~%IoC?}HkWH#AniN&*XM>F0b* -P(g*zS&cKIgI&E{yDSj&Uyed^QhCNK<0svWiiQlHjjo$Kq^o| -2XXs^OgwC1chhK5I&(Mt!)WaTCNd#?;EzPq78C*Fj}GaF|E*TSnkZ;Wlq4*!!ifXLOs&^kEut3J|^F! -{lOvL`bn0sxFYXx$o#f?V0cvsIF0jw78 -vDmbpR{Es?wMvq1KWcg9t^F&PX&3Q_)ZN??jB`Ztoq-v3t;`>!PSUrFo_mc$}VO*@N%9kN%cWK*r?Z%jZ8lQ)b8cc{+jFhM9CQU;X|2-IsgGqy3kUp!UJbeE<(%ULEYdOupOu -arfKj&ywel4)Cvnw9p}en?dr9beDc@Qp|PRe2t$ffRk}PvQ_X0)%cbYP+I?hcjPWmZB(_>z$HjMHL9Y -(5#lU7`b7ZgTa6!R#Pokr9fluN*fxPrDjv&&F@taoCN-W`GnEO#jO1Xs-R~dgBYfDwpAvPZc-T1)N>IRCQy$@xZSccD>Y`< -mzKOta2R;qRZ9r8`s2dR2jce{VM4g*4Obt1OEX~Rpp`F^x9O9x%Eh0HLhmzjymW)y4>`-+w_Nct1$Gd -PK+Ku*QM-oSRKE7x*nqA`#m2UpSl5kQHG1}_adkWDu#&ow19^%B52?)>*q4?TLCfNOw__l?8;00rI+| -??@*1Ls(ZNEpaD4a8BjicHry~D#PLIrBM1s=h6vgyAm~8^L8em$6iM|fsF4C?12Y6jO#djimC*U5n12 -BKgFFD8M|re}r@OhDQb~{f-^bqH<^k|<6OqB-z-n_g=_?KOf3xYB-9clWAlQc0xH1-vq -iGDX2eb^{Le#mQ>!udAwYD7@RtqU5Jo1 -p?&ct%O+1vM!SHxa83+eqTt%OC+wYtcotwhT|FDKy!OJB-pqnl`WCRATWOwSMHcE85w_8{ke -`pUJEETDo~@sc343Zne&`8PTjd+up?>ut(R(%s{y@iWGn)G#tOpV%_AfDu6JN3*o{05^$nPX3W%0fM_ -#$5#30z4ppQ%0T&(Lu+QNkL3C=AE7zo|-@}zurcVVv-gKhIc&sEO<4Be#CY+V@#*^04UXNt2`N*W&Hk -2lJhK^*YNLFe=&S}xt#?jXMH^WC-PokXGVm8OFeSaR5Z_PCve_XR>69_0Bi>nUplb!4U2w}ng$)u) -W0^J*9b?;@9X1Cpy`+f*1c;t(%iu^TSB4jcR_D6-Cy;aEVYgIli1;m>kzu?%Ze+*o2h@ty@#P*_DQjH5U+sB>t_*;)di-6X_=Xj5c+xj=ES+hv#6=tDn{r_{HR -y6W35z{BtcvuFl~H(OCrH)Z}Bh;*=l%l)&D)?z((og;#jb&A*P6+8wd=0#on&o(<}BeI-{#aLyZoh%( -m!C3or^K+S99YC(;vKU16!%t`^8|vu`=;y4&DkZOhT9L+N^)&{dJO@&)=;0yV~BG8Q}G7x7qs4&USzx -RKFbGoIrI#6B+JyFNnvW4<(75gQ*E8F^V?WZ|X1k>n>2FJstnjP#T7_)U7Ar9Bq)RQ4Xe%VyrenIte{ -e0=QSW2p5ph^}{G%y(VVTS==$oVCf^5~S4R1i0of?@OHHx7oCR+Be3{PkbAdT`TuZ^`%O-^h^To8 -n?o$19aFJf-fz7!?V-K*-cEz=n@%lV6o`>g{IVFz};Q6pty@S^xPN>gUm9zU5d -%a{3JqOWEYKhz-|oIf(xD8(vxgpBSiOSZ0M~r-z3#dcM}OaFzsOa5ObR@QTuEG2DhyWuIaS|uv}Rbm5 -o0k-=1^rHOnVZdHC%@1zNbUA!p;s^U*fd!K9t(sIxAl4434vKBL&QXPpiSp9BROlD5DTC~ER -)MN)?7Ya)p8%?RRT{iM46qu1R7;{2CGqdj#_ckIM5gjUR;FpraG64$rNP>E -$vU>IMT$TGz^G&Wgf&@4tinOisN_~^8t3`-z&7P?LHug?HT(@PhI2mkRg2BJiBqB -`34_J{MKanKl_x>RJ=a1mM^xMfn_6W0Z*xqoWUAE~Owhl$ec+=~LHk^C*YWcn)#5YC>4+HO)+JR8sUc -^SE^^%PkdwQXcX&4Xble)1P`C%VDM#NglXLPm$)N}i*PgsBG_wlqz_iDYH9EyCz=wxJh`@MjQ5!EF)9UhiVLuo_7%+*@IcNY|j~6JfzU$wnX6DCazMd$umOJ>0@OfR7&Lou|7$cJ`kgEa -}j@5A9AgnPaiJ^%WbDyKhrXz6DFbCO`<2tC9gIUZWJ(>ndtFm8inpPYu%HNe}2Dun<7`P02a|EwEHWUa?j9E&{bk!*<)Q5qAJM -!mo(+1^hFV*oKky;+v3$(g%Z#k5L39ZMt}jFNTA^UL&-qNH8zh*j1uf3n3a -W$F->UCE`Cb!GdK$2^0K_KY&%Ou1)$Koqm@-Lz|-{ld5Y4GlNgxWSS -mAcOIKFP+_c1?TKBToMudjlK|XGTJtrVsA4zsHeoJC?N|B_L3xuxc>vV*K-h@(G+3wYq01GE3h;FF2bQ1?L(<|M1xx29a}gZOhy#?h>Ct -yT8}4jrZ8fdu*p}JiJ4#&dGQTxEpnVXbz#CwZoS>1W$JjkbRkSr}JKBs%p8SNs+|nlgW5W+%;4N;Sf0 -eGq!UxH|z|Hv5EE0VG!t0e#v=|`y=3)Ve*tjrMfEH*X#M3vem2EvEvAIF%QY&S7AaWOfye8R%un}8Jw -?k*lw4}>0q4BJT|`IdIGKej$Bu*(ozW|Xw9#v;o$|lguA|-{LXZL2|FEUXei#%n3gA^It~a~4~&R(k& -bBnO{U{XdPZrj*{L@7n_7W$cRblQlsRQSLg2=%q7h`MVukk2-Y2)Z>4?bev+Uh02zq$+I7X3M;25bl2 -9|#|>!RO(jUjv`Ga^Zb@>Klx$W(8^`BiQr!&=^`sOdvrMcgb}zMI#;rL(&+wO1w)Vxa}rG!{syISdvk -q_o8r%`1xyc`Ue~mu0u>AEjGxsB}!l-}ws~&hVEoQQPulyw}~ESel4=$C#mna)MtJ=~;(vy(#;=9W_4 -Rw~uWdx;yN62yz2g!_=5pbqaAKIvQ<0IBQI-&lk~M`9XMUsM6#hI(bG>3M9fH=N7f0ZSg6b1GxystMtK!Hj3t~0@xNNBCA^}kU;#?n=0 -@)oKe~v7PE`uvtJDg=RdsesjB&!ljgbs4Cj5hyGr1;-NimQJ>bt(u^!`cjVizRI1P20!Z$&B@<1$7tEjn*zxfJ}4RXG0p^CY4h2u&|Z@$8sh4~$+m65?htEl -tCJ`?YW&tbiduva`(jzJe#Y;b>qsCpz4^ZKzt6F3vP=_Z9$csuU*r~|lWxQ$|BC|m;AshgRJ)vaQ#i` -mcU%L4xns9GXxSJR@ngsLPFaO%ct>M1~@35LIAQ|Krp4106peL -o4r=uEeA~5VE9DHIbP2_404D^7GO9??iF#Ho!pO)o^`YfHEWi#K|X8Ok1N*EF;60jWE)IAfzq6xdaJ3 -u=EUudAshf)LJ8@D^>9Vj-3Cd?>NvU9$c9Y|E*rg+q-U@ZdR5QoAS0 -mbBvbqlFK%ecJluLH@5+7Kp{ET?c{y(3RAlE;YfO)JkIj&w9h7Fi$Cv^;8zuXVjv=fpVG8vSHGlruliSr{?onF#IncP;QGHws*gan3!aP0b-Dnb)(Mz$@ -mQByjC5I&)$!!^$5`Rg8=B~U&7m`eb_Es@D+D1M{~7{bD&bd$5N<`*%%5Bjs!1?_ -xft@1-x*YwD(nQGJ}deS#D^e)Q81o)CRZ&E@vd-%qNOOk^&K+Dkzo~_*+_1@8NEuWF*!J0Xdm|4@C*D -g7et%6Odp7IvF5`SVkhkha8>`{Ygziz+m846G=#nX!@a%5V>r@B)kLO=e9lT@YOiO_FtB2FY6UAiJbQb!3W$fLHEgeXu&seN%SI -Kg-$rcD-jD^Z8jha>h1YR-(~05_R&Dj>!^r}7!Yb@>idia~9KV^D@n(1m%#kn*|!X!Py&unT5F8lBq&Lb_{}81m+uPB~L$A5&gm<%ba>- -BqS@5IX?YdAh#~^{^6l7u&N?(!c8r(t>O|(ll@pg{KfCIGmCs?Jhz#lLZb6=Y$D0!qG)nL)d$%qDa$t -;`cHT-RdkNZasF4QI=`Iy|VUNE_|$8#_>knfd&XK;ErvT)|wylvS|NmKJ4=l9YYMyOU7V)4JK#|Cw!C=M+!Ac4Zvs`%3NCLrJ -19$t`-pxg;nZfQr>g~WznuiO}Mx(dg}`1UZkE<9bH{Tg&)dDExG;7+#uR8OcQmesEJ{`u5Bl6zxk3Z< -c2#%nNbYK8}VGq0otN&U@6{lo`(7b?Uh1COXK;>-C1}o<4*9EJ=e4IR{Z+Oyg%rn*dRI_4%TT4@G4}q -IXZ=XV!~(L$_!2Royf2x>)}GZo`F#>>ZkNk8PSdot#juGDMf?stx?|axK*Dk=0ea+FH#WT7OJ(R5FB+ --N&G~{7W(Qyq@GG8DFq?f@%bZP=VS=|hDNfJD#gR|@eRza6tPLO<2L#+Dw0r@prZk{UR&1i -gK!wbN0fWENagtMwhza^TCbqrUIL|a=0Poifab8RZWA7T|5HZzMFu<;5)Egw2Sic~>fuDFTO_DaC#0{ -<2JWRX6qK{Dz&sk~6Al^@PC)zlB8;#WnfM++zOPLCiP^UnubyLf -gpdGom^5BJskE4sqVT7ZfQ2G0htStsgstql4M^ZJ+Lkfl~p8+UykQSzd7i4`!mcBhh{f43ldfrR)037 -M5d$S43-SNX2&ku@6mV!Q!$g7nsXRm#NxJ_Xlt6pm?8@zN;*H8j$uecGf`QPsa1Bon7%u7MW%TnjTrbe|if+_L&amhe{#6^SRy^?BSke~NYXMa!zIp_1c&2CjEuWL6+7h~bawqhjCI>ziRF0d6 -);+f!YvpcJB+VC1ER`9jkQRT2~7WQ(*1@|3Gh1OQ76NbuOl5?ykg?3Wp!7Wb>j-tzFP -K7;vbPk1`&A$-kXWp*TX9eY`%j3K21;K_>>25SEVs9{ubh*nk|1 -B5werGzh~nR9l{TrAqWkz64ms^E9a5Z?zw^KdaW!8v@-Rs`v56r$q@4@r65uZi^s?n7|9FQ3hD>Owl= -I$4e2t?3l4IEl^9uRKbpO?9@FKWx_4+Z*?Rtyn35Mk7 -I=7j9r11N7S^6ix3ZNLw0TBNrnA6;7r}7u#f9)z|KjVThZYqgGC{CTu=bRZyH9S`X4XJn>Fn3~(vEj& -LVSVtkmIup-RYC9+WQ`puybUb`OwFYPCc@a;F3r3>)7>JQTMg>6LuEnsw_lNt}W9}iqG&e@P5et|}$a -^MG>2RJ}~Db1OA8!}`PCl-t8v}>m3T4}Ub@9mj|z%_+rxZ7ZEoDs{ML#*(u&9_5~DqBiYxKESj4mTOg=Wa`9szAMKx)}lv -`-l*j#i3y?0Khhbl?7xbDsdc6QJAGpzJBiMIGW`#e-^r2T-;Hh;=d+S{#*j -3I)267SvhMr@RQkJ547XS52-$ICe>QJeUthhO48i7V0%dy(GB*H(i?tWW0MqPD)U5K7}eL^XVX+RHA+U$Wien -x}#42j<+ZS)a-CTwqS>^=rPR3Y(X$J>v!#dP<_eWA`6r@k1N5GM&;`0;DxV+ub$7wO}+r*n96Er%7th -Ymg{)TiMYU{BBYW{46R-e!nDGl -oq&29pyV9|?MXb2oKEwqYSEM`=c4S9iyBN!K;m1Z6dy_z6T-Qa=<9M_f7o`m>ROPtq$qTr%KvDB^A{R -u(^!0Ty0U`q%ZMLXY$aqwu~24iaXx57!I#1nRO7%VEF6W)%!pDKn(Hv7%no$B5OD9o-=UMW3jSp5-6< -v-9T^tCljw^~2PNK?57H#NF7z_K}~$G#1Wx<(?RTh2F%p=SacciWeVL6uAbep(n8Pi?&iI^4QwpWGU? -0GIb0<-bMmiQ^U8f0}OCp5%bcCE{lRyt%M%cU}RKF9NMnd0$_z2StOvvMKQ&JmiNraQl_kRJhz<1a*m -a@HA>HhlQi^+Yz#zOTJZah@Ob%nJ|$g4g{6aTqv!aai&D<-qSwK$x8eOo&yVO@ZJ=W7q8A>ariypcbM -yXrK0ckHe}oZpbktW7FnjZ$&$)GOM%OnMGY@O6(&l00B0b|hkr6wNut6MQ>DV_Gj6KFnkWkjJk{NyL+HXD087Kj+YhKdZ8lHV5gqZWrlOK;G%DWE`!BkiXqnIm+ -alq;w8?I__`J9YNbif7Z(+|^>4t-qj+S?{?P;}c-KA%b+Zd?Qui3GJ@ly0WG{Gc9Yv-5H95!49n5)ca -M1c6?_;D?ZVzAB_9XK_iNylL2#JNwovpi7Gmwf}NZXTwQG?Y2aVBm1IA&SN@GB -A<8Ha**&a{6_UZG4VGyEK^#0%#OTx_$Nk!Z}lFQk?$b_-)qd&Cxy&ILJ3&PDs2civ96U6sc_HU-^`JjjY!#};jDgN`*X{DU}?(EoQQ!;j -1K_GLKLI*;;-biYPQiKGQK~MhhUnnbop*XM|zy79ld0Sh?x-#B_1Q0TV9`sIsdNatCvgOb`^7VM90W~ -{yyZe{g-ATGTJ=0T(Tk9KEMV{K5XSem40%39ns(9GEzv(3wMJ`fcCOgUfz|_dWmg?(AaeEHFC^O|<^O -rdY2fb`MZDEwMtX5GIcE?W69mM(_pc8kd_17cH(P=>8-S!CR0FdRnP}2pWaoJMi)S}5TctPe-R4~A^; -QFJ`TQ|3NU>-Afzjh}tb`)L*DLZJNFF=M5o|Z`r1Ms4liWCh?3OM-1%}HT72nu$M?ux7|T~!JqtKk1~ ->(=dCx0AEM_#_=9=hH}G3yW# -CLCUEM$H-x|3K#4INOZfoNuX$W)Nac3@*Ko>zHWV+jr=qh4K|UMAOfsORyXcc9qq)Y{a!C4`!U2ETLh -bdL8cZ&B_!0SsW8h1189zt0F5qPRYQuH@oP!X3iqofYryi -I#yyfE?8AWMZtbUxmi5KkN&52~DCXv)imf>Uj;SK&<$c`eqkzz{c#9|{}PxEZk(8PKCKDaq`ubalO5E -pW1uiVlut-88JbD-p#wi_{z4B@KKFrQ!?3h#Z -|^Y9hLBKz$BZ=;%a_U=HJ -t28_=SNXIx8c1AR4bQMISZ!`S!6NOsKFQ=)t?rrF#6c%Dl(&YHSQiPC%D#Sr&tKc}rb&ARACX8aAkq_ -RRf4XN$p#s5Sa4wAnUKq7|dzL#6$mNcoNsX#ibiq5rIy_Mi@GIvg%gBsn;M@>`m4466;(xcd*imot~k -)_Ho(f*whXA&6Y@5g3=$H$fSX1juXAab9mdZI@gsLn`|hgmmem$*K2>?WL*8sEUANp&`r&W=)utx;`o -TZmu&u};lUx?`LrHmUDrIYthqbYfJ|C@MV+&=J*29*-rT9CnkKMBuF7OgUR|ZklF$zMilDgXt=vZ<9; -XxF3vP(u`))2zA5IrSYq>vP&pz4+!5Zyksf!z?N;4%uJNLu*eKcAUkL_99~EhPa4VRD6J!vW>_osY -t?Cfz=bUSk5b%2Eo#^O;nBw)|ALbJ|<8VW?;>8CJy+5iH;^5PQna`$LfS5+Yrxu%^L8If#(}nBrb%l0 -~0%Q;r^!00K0`8GC*F)Gp-kbDvR2bScP(MfM#3clWpC#2#Jeb9aLHES*>a=ZW6L$u`!g>jX#d?bh5mg -PO_IEJG^^|Ieo>~SMZDp9uqFYn4^Gl`DXIZrsA+DP{*|L8Xs2F?FnA%w4kX0nu~WEN#`UVU~Is(M53J -dB`U8+l?K8wyk_`%#;<1;Z!F@ZC;T8V&CgFU{u`k?r*x+#cF8>Ffo^j&&(!2WE!VgGJnwhkSZaz&ql~ -0(<@KM)`IGYw9;R=^LX0o(#+c%AFb2%#vnG=(j8~~G8K)$#p=duk9iSPFjP4BfBX-}9F9Q7~hjcRS*G -XgzUqQ?68*GVP^iQC<1ja*dVsvdt6?8yVo-)jxFh%8IYJ4%FVRkvt>P9nK%AVj7DnVg0^HSRsHPFp;l -!aLZ0iTjbqnD7Wt8#Og>IR&d%>W!L0ufqD;yT7R9`=7pSs+0gZUCNTBeE8PVK5-Rp8ZOz_=x74f~H~| -*IXN98q_-2r6(FqK-TfGTX05OiQ~f$ -Kmt)VYz>HPfSL6$n9K#7loTF$4wDT%k@O1|xPwi|2fvq?y`8J(~mviP7umUG#!8FB~9c#arf?o6Kw4a -Y{Dqcy!>-x1uk8BOTLJR8o@e?Z#0Zw}(Np^R)H|{47x3+h8;rmW|W5IRW;bHY5Dj&AV#(abA5O;U&qm -BFghzf1dBY3My?U;(b?!p&V&8jMiM-e!qa*iO3qJja$Oje-j*_^DmQTfinVrb$T^q|xowbDHw_q&#Ev|}v;}N{Ov($i)7ebY7Fk2qNccE0KQs@Rlk1CL%NA@=5ThJvHaSDt9~6Dfln{a+)2x&Yhla7d`?}jpx?A2&(>*?fE{7-MfeKmfFDO1Iql?Ma#6l) -HxC}Z5WgnXb_A&}%K>kBBbDQm>Em*hvk`}=(RkubnUtNx7>AN4ejc{oSO2|lsJ6l -lqKO1viVYiMcM6^M=D}UR(V{I|^&tyq&B%_Qs+ccvCoPspj7)v&h7cLC#@OGP72Vk*e`w?58pO48*4v -?bKfCHA-9ZD%;T|e0(Azp@the068cjpvtfZreXeNi5{ho8jz&9|?$6@b3BH%n7Z(rz#zbfBpcGf6bs) -DV9{s72FF{BU%(@F^`w+Eg`YrRex~&Bmlyiz73ksxa{7Timm_CZx|n>-1W(be(uAv#{9M)Nna^nf)%_ -6teLu%^e`QmG|Pkcc^)KVdpQ0tQ&zZ6jYA~cC^SMK=eO$>1=Z_PU9nJo=31O`-a-DRSxguM|dr7Dwat -c@h++>v?`qc)u8be@a^};AZePlxE~+daA+UlcOal_&WghFddzvAQdD6gaTLc7-#Ff~gtRRrY$Co5Fv2 -CUaX>H{Y(yrjFGW20S@yE8ZhaA-vBTYUG4U -6KB=l80^+IrW4vQDnxKPT!zG02+nPM;OSCWfx$*@HD``#wJt*O>^(rD8#L!z&aSZ4f*qkzlb%adeA6}Z{jg9<_txYPzlY|a2$AkqYVI!=q`O{Mb$x0PvD#s*=vSsU!Vfjbl!x$!Ltxg{lLtxWXJ@^S|-ubn2v{qpw6%=UX4w -bM|J&XRVf}<5G-9goZPz#B7sgW>ux#fDx+**`zw_q!1Gj6F9SQI! -MOXBSUVVlTsxOo@Z`o9X*01A;j>Xs1F+xq4V^9%#>sJ>JNg3E7Oz!02Cp5WWffz^h<_AZT@apEtPaI2 -O*h4W28@Yzz?_{L8;vYKoV9{BHxvB`pdUrFMR+VI$YnQ!7}^u_{6Q;|@uXG_nyS_}3ooF-YF$oci==R -hRMYjKq>4Rju20n?eg4@xe9Fy7XD<9eTbRO@`x7(Zwd}R%4VD2H^csjdIuE^~{eZ=um|wc6Eh5GDUg)A))jF)$&rl*Vtv>J0imgI}pI@V?!}cbKr(Y0wW|alBhr{2!Pn2O+6=SsIz*Tu+kf;ZfCtr*xQfHu`)#b+Ll0@u-4{Gn+d}Wu0=FfA;woUsj4G)8;R(WO~vC$Y -x -ZzS>qR52mE|XIuj5CWpAQ0mDpxZ@|*ft%UCz<0EED+m{eKQhMDcuL+X;Nz@h|)V7LDfv=m*eBtO)(pU -4;hNQJ~}?GN5GzE@5(_RA6H+$t2#(Efb(vnjnenUc^?mCyt;={pHwxEP^W_H?{43ARZT`?)GU*CGaC{ -?-_b@;tPp4Ylm4JTyNq>o+?~O(v#_h{L08uwEbpqD16p&ef4MaFQR&I}Ed}3Xg$ntar%8{HjWK~&D&bc+o$x|a^r_yho^T}YB2M~-w>@Gzjo%QsPjLOX9`zQILSwjCcKc-;QwGi$2 -V-~kXgsKm3W5R0E|8E3av$SGOyIkRY4-EHKULOCq6A8fYggAq1oR{X)@A9Lp+ptgw10Yu{Q-i&@jpfs -03`P<;{jrg-(fk57!=&|#e+;JpcT*WjV%al@WMMiydWKUe536f<4=qrku%2xub5$G5em@@mlRV41WLY -PZbeWh#&-6KY>#kB$}|Z{L#B!pT+ym0jr;)8@F>R{0$v|mzC01U-8oKxX)6^g!ic724o)yb;^Ty&ZP) -cZ_9%6xXwUBLCeL<@ERjYg%*%Oel{Wdg -vy?SE=hYJ%Nig$t+S8A>a+2MInRF}{WoP0n@^L+{Tu~dSW;Ds*%ga>E|hc9ggRVO3U1`J7olkJ2%>PwVrGUOO$$^&+jA5Jv5+E-TeYBA+7`aK -|shjqCMoPsV&>CWWI-j&Q`q3#nD7gG~?BHtKM#Dr87F|ln1;Z)D!8IN*bd}y8$sGkSRh=N6yYYW7%`X -n+Stv6M0kbWfB%SqU`nrfxoFj6a_b>+`Maw&Y<8Qb`>{G?z8Z9^|aU{QleHB;T1f=U|1j6~yInBtu(sgAj^S#b68P -7W^$R|Rv;J(5MLQP>{bO(D%ey;rJO9Gl`8~VyVNdolj653Ej#`nnUjfZ{rn)Q -hWQ%H7fj(@67U)Pv$1pePm43o3@M3vqs|fFNANG6!>C{EG&kL?Dm1gHdxjAHA=gH|lfmCLUn#c+D>0 -;qWteg_jZ-rlqZDg0l3E4+R_gI0WYD7m;;^Dsk65dCw@HzzM^lgZcoSf!+Pzn47|~<h`{370 -o@GFjwzYOBzzkHa4#T>VLi>-a(Y438xbl-4${QdxoMCY0bBBgZyEW6L#Gv6mfg^l8>6PCG-8Kn{ZP8G -TKehsLm50araE`kt)Fc+|k*cbb4W#Hg_qtbtm6J)s7GaZss=1DSXAf1wiq|j&GgQ_cb@f>X7nA>;!<` -vkesAr>1X_XTUZ2Wu@8N{bF(*hi>~!!R4X@mAnATFB`tICtH_?@z^lu?t$Q)VnxW39MS10QB=SmC>Y!c|?-!LB5q8;kot6p>3)Cn$eaoeog6^uv9I< -~0l#kf(%xfi*wh*6?NkrBsR~=?>psr&U7=lW_K^l2lMU*m)x@Oy -FmQaE2{NPhRnNEetcucmYj=lMNo~-7lO+|ze+9k9E=CEOrPQtKfC7T|J(3!N^4jOV`M+13%`vc#EjuE -R9h&IGN>8!HQkl|CPje@^!m5aAwTi5(A;@M^{`e90eF}{lv8Kxjl@cjF7M)+LAD@@5A;n!jh`g64zFUcjLEgc{)pQ4IhXqKPLD#S0=?nAD+F(Zln5$0KK -44!=C0^X*gw5A7e2;F43RGvjB-8HQ+@yvVA{G^p89r>8P1ZqFQM+cd)cjn$2-P%f9vu7KXx!EHdVzC> -5)E4FY*kt@{s1!c0&$qp*ccPlUMA^ONmAQKtM?J|Xq<4cae=ygV>5=ahklioSly;06Ur;_Y!tUF`c=4_ty% -|H=OW~Kpq|svnd|+)JJ&6o&3#Q3O7~cRBWfowsJ(0^$=^7QQqc-gwQNx;70J(#wNpuSAqpV1LF>36ov -C66KKyxG@?H3zf)$|bCa!{V+{%29<~w}A6PMGF49sCj5Oz6Ga85@71%#z7d;ao97z@wxG1#~FiHxdljG{aah5(#rX^(xzEj*u*!O4%>c&h4RXP}uhds3$+5io7o(d9C%H6rnJob# -hW9suf4z10y|vI}%bE5w#VsBPmaNX3^9rv~DRD0P)JMvHuz-9WKN_KDj!9xsA6=D=IH_7U}{4#lS?`> -d~0u_c5TX`u|7iz|b9d02bXNZzt09KTV}I(&B&(W!^o8cv18Kciglsny>Y7zK}aQ<$xDB#Y6ik-;RXh -)Gh-)+uJ!qReZc7lM+xo_(^Nl&mPWfusH$8bC)_-d4U^H(l2-ivZC%CMuj%z}Vv9FOamaq5?{44(DNq -)hzg8^(4JjHt=n6?yM=6mYve+s2fp3_9)!0RvlCO3awoz;4_|mxtMn8Bhs;N#OFB1P(mZ6L|HhGPm?c -{di~SWeiz*@P9$`oJEW~go5uy=Qey7t;~t09iIYd-irk^eO&}KE{nY~AOe|IT4aP8@?B@9xy$ -FbwWX}v}phohp^E-Ep^1K@(dEpGwv?QRR|0$A2pJlD2DJt^>NEcfsm7B)$vUEOXr*pP|1uv3p3U$H*;UMl6h -FN%yIektB1ixFItwujQG|tP0%&sm)k0!X##!;(Du7vt$zojk9W#_z0LnP%0E7b541VGQF5sGUn9^X<> -R-PqOpPemJJ<3y$=9ET$F1tb3l0`uUI?fyZdIK=z#6;BYcUyXH%@mJq}(jt@y&vYD8*iZKP9L)$bXPv -gv8EJGd=)xxTU;2j{I^FbR>0ugJ;kl$3e@5>&adTMWALc-iIH_br2w>e{wR8H+R@=+y -@OVH-AosQ<`ZpY*mZ_o#{1uvcsur(LF@u`ZaVAMDXa}3$3RorOImVtDG+a=4CVCnlhixY)`C3{28POd -#`x9N06=2GQm6q3CD3`0r=}`=5q8b6o<4J9+J0|f`nE!c-mhA7yAbvW>K;FZNXFpKnmHkEz4?k@r)itPo2!y -?Q7ruOUbfjz|!yX*E8uyP}4Ob!P9+am^*ucX0g@3l&M}brHZP?or(q=2M`Wwmd`A{^>y>TWfFvfu=3i -7B>e?IlYlH`uXPdeW`tT}*6;9!jpK--6%5>MgV8#AHEb$lT6p`5NXwX^9ZQI(okJxD|--Wdqir2pj%G -k(B)w9*#eRLdp3PijP<)~LoBkrXa1Y3q@WKPHJlh1O(lNOfBH6Izs)F|Gv7gbb*)q=tEe8cDr=xcR_& -x#KY`hG!r)(NC0`40CYG(334V+F4|}lNX9Pl46Q)Z-Hol!fu!?L~Y(VU{f(cm$XVMWJj!R=tz*ji -$+X*`3cr%E**Uv+Br4C|519`SmUv>GS)mA*d=aP=B*nUZ~KDO>Jo}*e62W`~}`* -1_|Y_V>21+*=1Vzo%A;U=tDM$>FD67!Q<8%|Re@{`&|odcSoK`;KkUst<~@K?Si=!C!0p16(X@9r}H$ -xD+;6PZtp%tM!_u|*yD+Zw8B%neJT1N`{cQWoTEtef5X#(_#k~?^9d;hW6=- -?UyurL;KW{d%cO`^Hcaly1gpkKWuXra$+rMMC9&_w{%3R?$5MJxsRM$VS8(*_p||mD<0aCg)g!TO9OG -;ousQEuC%_cZs!N{8`Zoj{!tH1U6F1p=Fb4Kj3NRw@3jJ&(gGUnzHln7K!hqdiU1pe-bN&#sjDon(s9 -EmjBfR&;a3`z*0#s+HQ;n}`3bydJ%tzJ7i59)49Sb^P=CHNAOv^!}ZWb_ -0jaU5ZaztyBvEdb}7Lz4097RkAWg-Yh>`(w9n=TCA$RRIAJS!a-ZAmBoFLpdPj_KYu)=l+ls*$qf(F8 -v|>*WSDr@6CfW$OpJS3VZ@y$wd$O@9`SiljY<6=`A5CF8C^_ej47T}zBBuBdkEv3}(i#S5~a93%4$;>Sc4uO}eZHqNwCxw$Eiz{YoUC0{hrHK6tM*;WjJ)QiYy;>Z -PEwXPD9cXciYMF2qnXn<8DjTr_3uGo1zNzyuDX))69I-`X!y4Cmq{*h1L@gus- -Srql?uw1<5azSbN>Vo-rfTWPyC8c$C8OO)k}keBG|@A~SR=Yb*c6iFhC=E_R@^h%Ip#jMFNTr4tlgc5 -nUZqb4~pL|h5!<~gv1+6@8PGpUwQmbp5Njz+f$BsH(s=uPQ_-|G31gU -w;?S0aHg(=V;@oLldBHfPFxNj#xXKNMwH|}GXBxWeJ3b~_}m%^7P>Zoa~W8&?zoDOxwMk -zRp24m1sj|@h_(ANBMt1xln`LvsL==?64AoVG>cW2z|p+Y}-S5JN|iYJoL``w+5`d8lD-)e&Hs -1r!y3|ISWf8MzA=cCX&BSc%p4_0-P#e>6o}#aKMqEM986-;|J~U-7cdDU3@@`(zz_zyNNfh{K2Uyonn -vrSmH7+5Lus%pg1`;!N7+m@_1Z#g?(`>&Qh(7850m9L(82Wr#%}&OL&%+8+aDCv{! -)r+%wLi~p`rU-16{7G#Fk(3iR)nDz(iQg6pJi7>rscnQjpKDi&28Z*wLXGYXKW5v7Ie&P_#!cRuDYDt -g}d(+cB0XQvJhbHV$31BmZ<0y3f6n7_}=uZu7ilke8yw2tlnDCKxqq5&ZW8&sh{r19j>atZdn#1tZa3 -@Yyym;Le_gY7)PVnDXGmxlpZunY?OT5CAe&$C!*-=dK6)GFJJ_k3X#w0m}5ym+|%v{m|i`IY&qLb -}R0{K}V1`!uX^jMHgyf`dht>e1OL9S8LVM@E~J_*?|PP2*gDYlVa8HwDGRU$dW8H-Zmk9=}T7HRC{JW -NOFS>~aJ5-3_dl~p&`PGP*zeLZu9L_VZtXBDe~BQ~f;fs&u20wFaH$Evq=e*}oY@pzC?$UwSv>2u0>G -I!+3F`L+7BD)+rn=sN`&8KLh#+I^QQABJQJQ*EvOlJZ#K4n`uwA?kd)yzlItMe@HvWvLxj?pQl*d~T9 -L>rLfV}CJNKk5=)NFapbmf5ZS;xXd8+Ng>$cCW9iC*Q)8-3?vd`*Gj?c<8nA@L?@&oHV*#Qx6~Nwz$} -CEf5P%PSSSxU?Y6sntf!yJ@z^PTX$G{*SHKX!YJv@*;yW7K|ZK>@&rvK;i>(wogAu6xA|=7({f)EL{@ -&KajKNx2nd*uvo0mQuJka_#F_?Ru0>IwUaIwQ?GT_Q>%TleO+_#)V -QZ6AB%s!8A#;xZ$NoNTsXL?WVDlkg{^IEB{d8Ey++-86$#l@gQz_mtS{Tx7irX=Y||8?ya?`z0f%c1&jiv+VYIo1G?wc1`Hsw=35~&lFKd6hO%-Ho!9Y!5LaN0f^BaNVl@r^{0Nm -Nqt+fV%NH`_-A1(THASzHi5i#&Y$!0Vi)19SNj=%g3BFr$b41Tc|6^V3VCcehA-H9slE(Mm{IGBowFl -}>=A?$x;Bav5mRi1PVKd_3Qe`X6P=q1l=~fk_zS8hxMpyW&trn8$~zS7PYhS{=QZ15eQL?1@L_y(~^H -3l-;Uue_dNkTw%UuU5CIhgEte?%Mvd$9vDv53(lNrr>-k$))8lE}es&fynjKi@m*k-Zje0#1~C5x;8V=U!3%jYm^L%Cj8s`4FCB8znvz{hH)Z7g$giZwsS(w}4LTj}WXxrtE--ygXkjGmYMg7~wW% -800n!BuAISrrq6q3`z|@QJg_r|Bf&1mLw_`oP_vfw%``X*fwnCpjlmNYcqTubA+SaK`U!{WYn)t|BBB -f3YtN&ZaV}{-_v?o`oU>b<1E@f&Zw$u5EDz{082ztT2OB3M&SW(ET|Q5jTRo3)Yz8dK$;CzX- -{)V>K81wwXznn=PFW%dxpX!Co2mi_>fc;6oG{Jxozjtdm_j+!{Dwr6dIKm4!C!9W}HUqj(% -BqFkE_OIn>^Z@GrVVgAY{57Kwz$}>94w)a*;3;azP+*{e9MYUE4n}5E+gusD@kOnTo6Fb3$`muj^}yK -(#qjsVPOHF383Q}8w#170R;5c0*y)_(uoj!sf4<_MyuPra<_8X5Nic| -`}7(-$V&zb}tApTt@X$k5R{45mqz@Rvody8Gk*~D=%yW!D)C5Xczc(Q5Do= -BOI8N<0)w#OPyPmG2iPQf`CXF7~z!jK0Wg)(;ipQxz`>?Uy0>H%w|#NZg*WXu(42oCl{t~9)Xf5|a-E -+m;24@}6Hs5h4i)m$jJNt6bfLOS~W?NKN$UTYh~tZ!-{Z9NDnD;pgH>cADk1v6<;BJms1<}EDj)723D -K%+fp+PmMPgqZaB_tZr -`eJHPQo{^jyvS+*>27+-{+msKJ?@i1;hvrCq2ziAY -;Ha4W_!`5U{}gyl)xg70GaRn=)JV^fe3(dok$D1<;m??T6?_pR5Q1kL=3Ij<7ks0v@|E(Cm;qZY57hHFc{^@g5_kvnpaz|ZEP83=e_D`xVmOS4l6bDFv3i1l;XH^HY}sB>i<_BwY*<>e1gXt@fP -QDa^uEa4Ll+Z%fl?{B=D}7Tvpar*5|?8iyo?&4|(XZVipnekyM*PWYmNF1rCg0*@VN>%!FI`5Qaf*pT -G_whhqaj(#`WESKv#<{3f1du$4Z_ID=6_m$^FoLyzQOJgb-wmvdfB>Q|i?XjPKxR>y+n)ite-seX0*v -oSC8=6I1L+RNwH%jMYnCU~}u9R^{m8+wK`Z3tmcHVjZ{J=C?XUiI|qhPu@&Rd3S-mZjQSB#@fU=gVR; -^}b$9(qSmvT*_7lT`v`9d8g>Aqa^&2G-33dU6HtqAog!`b4Uc&gSVx1+!hgbFq|A;i$?rvPZ}p$MLI^ -5`MGg&snSp?-H}KWSuJHsOIn%nr|DEW$@Y=NX{#t)Kp-tK9Y#ICp5^JFt(rU`iW>l{>R>pAR#phwe+<}&nt#q2~Yi(afe -koRp`XLuMj_~(wyyI>Kj;$PKCEY55{(s4daej$6m*jk4*Qzztpcm))YVK`LkN9BIE{3~8Bai6rBgHUPtb-zS43QcL%fa`No}JJGNu8eVS*MWqYVCNIehNEtrkLA{f?+W5#Ga -7Ct0a^z(vDd9aL`8~<}(*wtCNG6BqKTS9(e5MQpxN;z56 -Nj|Xav{pG|(lXLHU+WDBCD1snUBTavY -US0(Zt1|AUdes5_E={Uw#dZhnqX5eb4rJY%n*nF*citwTu=LVc<6GXecVW6`ZCs~K!rPC$kz?Lno+^g -`Ve2w*Ei%a?u!Dv?39YzFyW~(*I9Blb#Y=K9N5@Pu;k;7+!Sy*WlYCXs+OJe`S%0B`*FLFU*l{s>Shg -IIi8Jmm|b>3pA!=?n`KHhx&uv_6fhc0az(DF^msO=(wJEl&PC?}$H=F$L}RnY>9Ef>I-6=Tt}#LLtc8 -gLEOE$FvdGM-75}NN9ZWUQaArP``f*f=&<+p9F(L?N3DYi5!A$k -d}Zc6O8GX+_S%huhl!5GIEr7emgK5aLDR@Z%M=Kn8;9SHYV+YSCo{EJ4>ReYdyN>5VTH&fTq?>s8LR3 -z|ceuxj9A$x=3)zFLBva}k(Ct(e4qx(8p>vJ~$!#7;#i3$w?1;E<0Ivd8@TKLyCe3ljnk!!U#|W`GUJ -l60Ggk7eH(n}xv=u~gI1SG9uxehg=SITh+CeWIned}3hW;rt~q-H*3W+uGV{8nA!vwe53nY@gfn&UF&n@KQ>knwX=Tq7d-qH?I^1moq8Icqa8g)HW3vzl4Hd=h&5GoU -0=Ht83dETkL<270sJ44C}RCLK -4xVHZCTU~PN;yb_>47!IGhE@a!S_9z+^pPpGvP+X}6V)nv}I{s*)s`^wjsA1{53*CL=1@#`;lzG7dQp -SVH**v8046THwUJ0*}V;U4Hu%mPGJy`wG64$s`bKW0X`>XLkSic)QozW+q--di!*_Qmt+?abryY&S$J -cySO2RWU-#?Mcr;B|H52V4B~qj0hrsVzmNH4hhrn1z~NzWzbKMy7M&tG;sAB9H=JSNTzFin=zp_TW-A -e+sJ3cYHn3YPxQ6e*=|ldhSbm6)wkQF|H6Kr(hq(MxZkGD~(5dcnsXfS<2aFT#tW0K1w~bw4^i72P^5 -m0>lqnmB6zTEdSCJ%)Tv?pRCxL -;zAd1?dp^dgsWbNf5jf0a&I4h0>T6FmrP<2h?rrERbqZ6^%`{i*?W!snk-dEJJoZro8@~PKaw@TnNU@szZ -NvSN1;N?$bvWqv3VJa6{N!~!3Q2(!p&S@D0vMEas@(qwGyjVc8a)PH|iH%5Fs{YLb1mSG3G=##@cKK4 -M*5o+DjN9ZBBiHHIV{_fwuso-y02lzh2vJxG8QVJ(rwEDvlSB#nYqku22oBu`H|cOrjdROhk3@QbPfU -hDqDTws)k2@kp#}uo|19CFY79j03V4 -`eUm%j@F(R*Qp2~%jnf?~;h|3efDj(&LZ4{)Q_L -`1eORroR?80j-h^D*SQ7A(PRA`b7EHpi0rilnf&+5im9ZnD{&6^pFtQl1#Jz+S41*+MVfS0NO1sAlSR -9s}Zw14EzTINOZ_rOhf)n@tMbBznagtv}s0ou3)f`$f_F0GWmrACpQ>2%=T3+I=LMx#;>8q=lq)Y4H^ -kT5dIJsw(0_YLuoNTo`PL6eT>M<({E`#!GP$OKHvR{>2gH7?Z^eVCrlpM2BhcXBw`SAcUsFgrHSS{BT -EtMXbWrHahPP9@1ZteF6{fdh6s>eP>;f44mKyS)Wn-uxorcTsl)jJTCqr{uei(-UUrq;H0m@9S^@<=B -=wULcanwV4SWl7ekMQC4}S{OR4P0SA#+Ywt$oCUcE=xiD{i)Qf<37?r{ph%u7!XE%$j_t~}=d6@8RZ9 -)?Di12XZA1wY7XvQhh=@uLKOnMqYa-x4A!pHq-27Fbczav!ch!S;%86c4M3dQr0$!lr`uID5-U*a -cZ5~hA63B;3aKQvdZhM%R1}4&U!nU*bPcIc>Rq{QGa8rEWb9@*S7lFp;tB~(hD3&b6u1)&r?Zj-G;Qq -pSIUT{kAA+%91vfgp&QvDwT93mJWZb6KYtDZreQ>!IoNU)}9mf=a!sxF1hQhyG#nm3YnPzsy;9N^^ov -yb%eOARjFi+Nl-$Tg*`MYO`?7lQIK^2HaB2AycP7{jmpkz(I_|y`T?1*w}NqSkNw51Q7aRTqzQ+RGDx -(Ni7!lVI7{PQ=Z$xxnb&xVG{N0G)QNXqhQKu@UPR88jU3Uu^EDRll9!_=;Pj;C39_(fl`N_~udoL#>Y -esxAO;6Manx#3{A+7OV(kJi3AWB-NwBraOWK=F4rd0XnRP{x;*oTNt!gZ9qjFFilo$ecdQ0G9MI&qfI -LK0x6eUo1?tv|=)^K18*8*-Ngliot*=Q!(XlE2OXCG^+_S^)W=OgX;8lNlJw~$IiH&!=!oz0XT@mk0h -P7h`!k!sIP;>H%C-$<#)weUWKObkXdcba-*ZZfubD@cJ4E`+247igAB*}+gT@cZI -TrCK7pfI;{VA`sr6c{GJsCA4X;X^86RipZ@AQ&!0a3CHwE`um0@$PwfD?%`*eUXFquMOZMMCJ^PnupF -R7wXN{qlpq|MG)>gP(u;{b`^{Xy=0d{y%>C!JmKm{$G6gzP&k9YBod5{_hWd^xr@K^nZT%Gb=3Xsryp6tz|1rYkSBlh;^XhVc=GcXPd<6^Mr`S8V)pT79`J64!#;B+kue@Jj?F7$2 -7eEb&1>UO{LU=-nrJHs-@ESYNSP#fkcE|PwP*oUJ_yEoo0J>}?Y#+V8@3*7i$o -HY=fz3F5&TdQs=;EKzzR7U1@L9wrqZZ?|#WG4Wdyj$wIf0BZjG=`Y)jc@W!dMc*0U>Ospx-tknMZv%0 -OiNJ@f{#d2F`XDQZGY3MAHS{HNusqUO0ltUow1Hu>>y40Hp4D9&Ftf-J%7}A_OVLQ?+A$6kaU3MSU*~ -Sf&#eJQr?@;{xvZNvClsxOlcgpy~9#-`<6YZ?8vjJT4)vkZ&kxw%}GGhz}&Fs~1@Th74U@gI^HSvXP* -gQQUzEIDANw-!e6%tU3}X=ZA*WqrCCWY*R-;)HhU5{*gK-wBK4$r0AC -3&#mS*yKmFrYvW{4=(c6jX1sbIv2g%HyDj$j_ymPfCmav96%`~*)lnTm;Lp3jCEJpD9V|+#}ngF4{Ft%}885U{K)~2hY?x2Yi1vrlDRUu*3l#zCp -getFGnpp#lcP*2MiJY-8 -=bA7HTGJ(z9DLT{wZ)|9HZLzLXkv%bW!8oEx&9;ht+e$*wGeK%Ts;7a!p_2`afCTQH~0JHoctm_+-Ob -uFg4(8+YjAB9Zeps(&V8`PQSE*LXeKLrtQP8X|zE`ct! -DEsGT;sgp`7AL%`;k*VBS#_LQa-*5NW1HB7n1%-Wm5+-F2~vVmh7hF9K^$U&npwqW7fj!NggMV2i~@A -l6UP34%aaH{7t#ntsrvfE$=DGjL5a3RFfhO}fZ7G`8f#K0`vw|4g!V+7$Dzq$zR(b8HngQKfEbO24|c -j;%HER{0p4=y4kuq?-mUFPZDI+A8ztb{8x|A&=(+8H{Eg8W{z|w(PGV -&Hh(&A99bI+uY+f}hY3Ws>7Rf0#JeLjYt?bH*3@dS5w29q%e!l09|bYuuH*TWp(!sXBa4rLV_na7i1H -;iQ_G!doC&Wf2MywBq}!`$^Ac;A~#JrP0Lq2>{oWB`KZi6dx5F&3N&Kf|oPHKp=K)Q8QWGOr%1-IT$7 -Fnqg?Y7lUhY{6w;ZrL(Zh{>d9ZgOmchj%a4yW2#63G!BzojU!&WW1a(0Y;9z1Lihd#VuYr5v14qa;P -aBEK(p+dy<_jl}v+8uk!GAzdn#9hZI0ceA%-17pOWJ^zk&#XMl -vvf-bkac6GYt!(?w=6#&BXrglE3#V5U}1&x43*N_;OGq7}-AcFccCU56AXZHhjRK! -}f&VQD198BY`n1#v79^Gx3a(QvcW`LUyxkvFSu*KH&g!pIf-et#9EJ(^~Tk&GDp(>mquh^f?ozYnKT4 -;B+%b+vzH;nr~{(S@=*KAvF=d1vg*X -y>W`EgYu}*#2qBZ$1<;X>@NW*8*c@T_n3Ok12?7iTm8D7bKgN*t@f1ph*C`pfA89gp|6m3CYKDen9sP -=&T%h_m2w_`BsJO+iA_j%B27Iq0-WN8W`6ld*t90rzC36cx22-A{~ww(Cd0VEWEso@e^&M81Qz(x)?` -AkbpW9wDEWylZFY)cUwRg>`r_#(CC!#r}ye5zI(uH}?AJzxtkYDtvt`I9K@wigz_@kZUWyW|vnzsC$2 --!JgXJ`6gOSiFP?e_nE6*N++gvAkAGHTcy`u#077Yjvn0a++ZvF-22W0#8iz^Lyf@)}TA-_2H5zf60= -cv+NYSf^e9ng?#G%w0^(SO+e-;Cg=p%gs&=@sUx5?F6e;vm;iGUOU0!krf${|aDMQvj%4R7`H_&3oeX -|tC7m6*KO|Z_j=(`+?+;%W6ly}jFY5N&c+*@lt~Ch;q7-7WY*Lj(1ukQ~fm%t@7Ic}PnZw`|_L^ZEPS -1Jv6uP4VLxg4^ON!O{SzkRZI?x|%ZA>&ui=7QeV?5!;Yr~)w@)cs)S)!HS-rYU#+jrl7`>q}#xWz@?! -SG-X8w(42*I&QByI-J<9rm123+WtT^#+Pmg^nhK=)tTZU$mVgLiA1JZ5_m{u1VC -Px`^bmR4ueIY%0P4yM+)SCaGpXvklT@1#C|ZtUrCi)=F>1p=Of_rZ45Fh_|BzmWDTpwO!!Ulp3VLzm2UxH49jWgMa -@}csFR4nb_9)JPW99>Ou`E@rveLO5hGHs}0`(Px#jITbOf4&j`F*!WD`dyrV6o+TOi@%T0K8(ciZx@r}%S&-S0b=9Ri<9v -&q>j&yPCkDapZy}<1LoQJl{gunj<101)j1WZh>edgk=*I=3IyoGlj>Lz<)5Bkm3F{o_U=0zFc@$qh9b+Pvc?kcGuEytQSec{qv -#SZbj-alSs{;AU`0{up4kzPF6wAlS`6*Tt1q(RmLJ}LQBliEWoR*py@?okL+$A5O;QF2^hTN%o_(-6%12Jg0Jn`dHCH=f4e(gB9{b> -Mb_mB@3&UkO^H$T{~p#~t0 c{Aypn=R#0;{Nu^Nm;45Y`aGKamu!Yz=7r*ZT3E<4uB!g%ce|>!|4i -3Z~Wbb!p?_>7c*8F#S*W#xGVeOB^+aZW-CIIx^j(|UOR?)3P592YtG=gS#1yHJ8PThLj}j2h{`L_(X4gyOZx&XQFX7Km8CS;JwGC?7q!%)~iZ=M4I!<0OfbD# -{Cx&?}HoC2_5cnsQ=VKfK+D)(`tNTD}a$Z@QhlWvb+l*k6Rf)Xu@PX`hrXrRQM26UMGD6Vs4uSy1j-9 -7%8vBxl$69f)cy^j;&3ZX0%J7`W7?hO1MC4j+XKshT4bu@8@>FyY2K`ojnq+c=f8u($8b57j9Rv%>QB{kb)n}cXDQ${xu2D>C -jNVG8xs)#=(-mNUrfG)K3rGW{-ULI!Hh>?T?uW@)Ut)W30#$zx -CjQ4{=gv7w9nHiQ@vqCx17gnJ(ceu|x*5;cO|LKFcE!I+y@^J;$g-qv -aiDDmHCtdP~4Iq(-`A859aUg;>!)WqT@>xkihiln_P(k$gBn(>IlxLziqBM49!WT~Mi3!tA$*T*p7gpdI3mMLK?s~riCu6QDGVq>UjTZN1QhfJ+{PLtk=N^LLn|8V6Wb -b^0kt=Nz!WTpS|6tuukDSoD0u7kh$Z*7Nk_H4{&tB_ZPZyiUE~2s*SQ-=^FdH8jNo6>8?<2RqMFsvq! -sMOPNuEa>nl`1bV;?&?^;(@_@7Q^rXf`;(0*uPv~!z%QQ=cAgU3?C9E)sq%AUn63!jn0`Y{NENXHv1g{R2EZ2TcvkttE+0zHL+CMc#t|v*Sm9Xdo-Y(JpN6XMwaAYtpU#w -Be<_sO`|DA(k^E!z5tk%YMvVUig`@4;2=$K-ARH|;Mgn#cP}Y9@B&#m@_qZQ04^o=32(nQrT|O^RVC+ -cTEx*ETB9HnTQ>xmhEXCYPA&DyV^VLKESo>`Ai>p?z!WTsr0CM5Z!+7%9oo6P2aFv0trfSr)NFRGUIO -2dS{mnxr-mC;8zh&an{v}4t*o<`NH>$!rikk3@bi^+3t)DwUCT#saP^+7+5o75?KnlOaL^uY=uv_7id~;vaaIG~YQs$AL@bC5Z9=B -4s#g#h(QsRF>fNL^!@ob4UkoLK!8Ddg`S$bHRT!2v%QekmhR#?I2p_84u1BZ#-5r8BN>=r-0*!PHIqI -)Itt?Sr^X2UG|Gf0pL#u39x?e!AGDtE&u1#ynO6OpSI(dPtzX!IM0-PAZlGeXi5|yB3>r3C)3W#CvJ2 -r*NFeI0OE%(QqpL{EhLM%(}5Z|zNH?CGKe_gcT@LsuZ?W6@Y98v&O`a={F%oYp)Z4_WZ_^sK5$kCw%P -J(t*`^*b1IV$fk(4L>h_1KeRQd>EOxm{q -|)?q<=}H0oIhPSw-Ngzc*bA&u`#I&JSyQdI3=~gJAS@ctek8P9Y1-k`g;|iwdy}o&HqE!T*I~%H_x&q -gdFi*ApdX^t~|b=gMk;!w#)1_6jIhNKGu-iMLy3Kp@ZwTnZm^cdS5}XQl!`gNGFlfygpr>o?H}fgud* -yHD*2xpajivrRJQ|hA9_r5ITzhVjx^sMHEW1PsgXn?<*+?WE0RDLeNLv)+mjuNkodl*3eqy&?cegS(V -;tGnr_X;A`kcanwN?!rU(E6{Ix<;97E(@Kq&+2og{mA(Z^5tE-DkncTApG*d{mDZjSSIQnH=MQ9|fqi -{*1iYiW9+B!PkMd00W7+^du`Kb-z&s^pN^I!DP29n5SqHG)=TVc}JyOUX3@=ouU-HaOk$?#M?ekZ%5A -QQo^)MF2r;|FdSlW+b|JgGce<@KTCCbyErM|f+JF<|;grCx#^iLcG{QoU;<)#NI0y#&cFO77TBn*3GZ -`UqwL#ba0eqtP+77c09XaP_0H;I$76I(OQjma-x%ubM`g*sI~lXBz6E&~DUgK)^v5C*C}cmB-KSjxgm -&r!;2x4ZaM_{vs9&-1Apz?pa1Uiz8)>AB!hNAnxeTH0lC|IncdKV`V+3{Qf -?!Sd#(OU#+@zXLnWm?v~wJd(V>){1|urKw3{o(400cY1lOgg~9PaA4!R`IK3n_geJ-F-6Z>n_2%_(nZ -(mLD+8a}t76PdZKL(L2$Q8}gg~62ItYI03xgDXTI=S1J3X0P9F-lNB2;#D*SMS0_r*gH`SC+ab6UYH@ -IIO}<{YdW&C}+5M2Y}k+Qf?HePBZf+ -f%bXkVuH;!SC<%Lz$Som^F=Ac9o$aEOO6H~%~f@ru&bx-?C=`jL3NU@VReJyPCH+es~H;%jTLrUPUDp -|$a2gkx6LddE!OBY$!Y_}^}n4;0h`YQ(uiO}+i+ClSnm|f=?fSYWZR=Nvwx<~!40=T1{fGfCGPE($bZ -B^v?Zzu|5V%*#l0nN9zCbzJleYKVrs1L>YJlE%P>!$j+;kY7OD`RSFDr84sAy6p@8oOlXb*yMaT%(L7 -vVVsSid$$!bg7tY|Dx#mp-`?~_nz$JiE%NWtS`^IoXV`^grM1R|M*v9~okK)8#oTpOXAx_UZxf?GF(` -?byey4ub6b|Te2zqf~Li2}-Ax)-y(-|ppWr=xFP(+95!i-*k+Tm|CoF|?Vg<-0ZL%XTIy9CU(WSnaciqDsI=Q{du=zfTf*JSZV9v3Zrk~Ks@*DM>9X6 -Zh}YR|F;HtGvDR)A3GOM)zM17JdR5z1iJMz5;KfTBwHu%=qH}2%#wKCtAOYE7!)r+3a*zKU0=T}9%H0 -F-R`Xwj4^VrW#y0r3GFv9`W6%+~tihhB>}_g(PD1+-;I5i353+^?`kxPS=E6{f{KqFZ^203^fwU&lCw -87ohnIaWc%6L9x&ij%>N$+iT>mpGasdwEAx`|$+iybSOOVZ#z9!NQ7hBOhoTgdgdKuI&@DB5A{@hA_6 -GEh}Z$e4e??Zq~&flVf--Z&^@Y^xQ)D5WorLXW>7O|0CC%%V*N~9MS&R=Zca1=JPxiKqV61;p;q=MIXKK0I{+g?{YfZ4KiEeW146*YgWw4+l` -WivF4NIdhP12?7=@V&WDi>4yk`6yZ(O-#_P#bUdM7b2!ZV5%_{mafl-j&n7w!>6c?u$@%Am6%xOb_Y@-d+tE{4u8X%oUc#*D6m?XHHbi3xi=@eBk^4_5Z?)^^=OFlFh5Cm@S89PzYoA;nH+j@oPm*tM#g#ggn67vkQ -fr>iR66sQlK*gegwtY@Knw7dFCYLeEn91xeeo~8!8a>f)M40detff3wms;0c7E0R|U-Xwi*;KT~(+gb -kxCo$1c0D2iL#HT_q0g+{Y8R=%e_@Y)7g8476wHpPNk;!wRydGYB#p$ymc3`~jkMERzS%4MTTIpR?=7 -p7J#aewyAa*o{^eDjlbzodJdhci1l#R!*N%-|=_n5uH0lbRd6`#QfozX{Y)N!91GAdE`6#h#yNOG#OA -CtZen69pkFcAP>}OSEuxfePQFnJY5=Ns1u01Fm0BGS&Tj|70AJxVX9l#!d7P8;IM_iR&~^?TEfzQ<>? -{AX9%IA5I$1_F_bb$>sn4NM{Hr?WVJ14YZ*mZ`rr+O)!pGvrzIB$NwsOf=&``-{~|L!7E@DLP{-=gK@R{zpI8=dYiMFAXVs0twL}bEfZ@bi(M-oy$7J8ug9ZRK9K+>x< -7uE8Zs`fTQ58&^2SBHFCs(P3=QF&#dua1_K*zTUh#(Am#yr}Ku^wN6wNVIYcilPy4Aq8@Gg|BM8Op@J -599+@%ak443|crK4KR0LKyxL!;W^*LAg;R+C>!9b4sE?sNE}J2lc%YKu}zIUNg<4G=;KN1U~EGhn<_& -?JV_x1%oCJSfFMrcb(58i`$SDT%+eZ4s&4CO5vQiJ?W1V{d-@ -nI7$*kFditcPr_uk)tY7yO(ey-9tEK--ZQj^s4RGI5^y-Z{XvyqH -GU$)$u`_kZNDjk2Uz|{a|S0fLfIlnrMi!2e_vBpbC`3eXaI2N4|WdJ$oh`z#u3?0JH|cXOqhyrZ@fJF -G9q&ext1Sh^cebOTn!|cknQuDTRw|o@&_06^T@j$#Wc3?B^gwAi6$XoeX5;PK* -g9b(omY`1E%$v)!0elwM;dM$CrFyK$hP-XW+7nnB)Esxa4O7Ak@g+{~+=N3MJgYi$Bi{`nk`q&~wG=| -Q`5N0+N5xvHoycYAdeTaE|M8FAk-#5#7}B0mf^lT(xl27yQZxsCsM|j#m32@CZcSS_h!Y>uzrMaUeST -r<)-<1uM9_hj&%OrN!-nw}U()05I6S%ADn7NwFE^JX#2%W>qtX(LMp8Bkomg!&Jq>~;9gGI?G+W=;&Y -xi8g%6&NFn-3miI5y_NP%&YpHn`5PFKlS6G62at3ym)y2)+I9W*{Eh1JeeW6*}shE^TBQkEf|tqU_~t -Sjw#3mF9Hf=exnP$@%2@gUl@vLlGC2oKg>9ZihGlQyhHG9P|K}A)k#W}}Sw@i| -?c)ASqg2SQIQ3q2xVeF}w>cL3IfRtyFkB!H>2FlrZ+dTOwm}!db5}hULG(G5GabB9z?2$CQg9sZLaK(7pKq|01tw -vJXf%1fH;SC*5kLNxN-?cD?MbW`_mMV)5x#PIGlQlL>n(;2d+gYsGFmYM6*bNw{l?&BeLSL8ldJuvb@ -chvb#iCFuI>+)kz2UJc4FvD;h~&eoG+Tv|G0N#3jBtumE8o(om3#L0m4-800;^2eUdBs0@CjFhQB`T? -$9Yycjt;JF0vln2qz^2H6iyAj{_?*Q&~4YNNH3L{f1nc0&7^4p$Y&kHPIA6ji -0L|ul8Q#gKNe~B-6_I-1?83KF$fNB`V}}k*67+A&acyeK@YV(qdp7#~AswnoBxBpiy%K+_2^V`mJkDB -sZ)%a=Jmyp%AWi+k&4#E{MRP20qT@;GSdwC^?_ui&rF}o3n;hvdFtx$yxw_>?F4oLo`Zn3KL-Zgt>`8 -_1`p+?w!eKHyj#fcMwMGer%{F}7WyX9AY^KD3(~7;^@<@CMY8 -?9B~Vklqt1=u$P@4HqeDnjODj$1Y6IsS3wZzaybgy7r_9p}9CH1^s1mcyVz&aSksp$G@E6wPlyx_=@Q -k^v@R`4zG?cYX^wyy>hzTss%QN!1!0c#X{Gqc8dl6ZE%YT{&_KIB5L$)ZHM`%l|qw_>oLrlDT!1`-L{ -b#Dx0=iYc>-9{GXxerwG$F%ob##p6j!C3Yh&>)i#W)GrHPV^AZirNDcP3GMcBDim|hxI^$TYO<~~&Yc -0L_E4bGWqk=W{%TlLGw~DKhV@_dEMiW#`Q_-{v-dRAK04#h0e@z3La!XdR5fRhXb^#iTxS~KU24~8hC -VU9AzEdd1d6Z@7MszLIJMu$Q{Fq4uzI4(5LwK}s@30g)a#7JL=#xETcoxcg8PXMQn_)ov!~vg``EM=pnG&A -9YY+O+B*rsjeLU-1eE-91T|-m2EmTO{S>g&0{kHXhzIy?}rtXY7J6t$e2|iBLFE*Rl4O;3*cAqO*Y|> -H5oNBSXw1uWMbk)z(OM7(cf8BMQI0acP!;gnBI1{xeC2S9oi-um -n4EaeH06J+$wW!D4L_m(dIX8XIbCZE%HIj>K*zaUBCM`lFQHk+6p3m(AUql?F{uMr$TZ_oWx}@+&qh+ -FGRUwe4@-+Vx_WUfpy=O1pPyhz>c$&N3Shcs1`X3uE4_qTwm<1g%pBD7-p^Cg@EE(Foi%^3&yhrHAWE -J}s$H@xTym?#GuVjgn`KehZ9YQwRjGt7WiTA0jv49$>JQ$XN?0R8gPePlg7-xRSz~eRpdWE$Du&f?XF -%DHH2GTWK{6$xtJOr<&uaETUqsx_f6Mlo9PbT>dBacU7pO@HS9B%cxM00m+8 -xwdZV#%wWYdD;iEA{s73Hf+^Eszjd=BwnmwcY6>gxEn-U7&5Q3BI^!DF1Ax&De_GnP!ccN!x=>}7?(2 -;pfXV-FX1E;w1vuBe-`#e`;f&$wSbj5f$GhWq)mxbr;x*+Z2pe$u|}jo@jFwNmUFmqphdUut4-61=pKfV)4>qSVh+Vof$sSacrQ>%?(cBm?73P90}h%?d<8 -vL#Mo@%(|-vC-i%$f{-#sp{N%~1rgn7s_8gn@(#Kvc;+ZA6**mzR;(Wp$CM5>p2>u?BXKSH&ri~Zq9@dQ~_CJbvP;o$xXx_fQ8q}ipQQaQf!-WZG+CIHT&)B(Kjs)Ba8Y1X -MxfiW37$X+QVq0=!v7(YYi(12D|y8cXGG0IYwP0)pq}sWhjdlw1_E*3bsC=%=OYp)b0rr_3GpQ~a#&bQ3ON`V7=xWf$OZ_k=;|W8YMUH3c -q9?u+3SH3>Z-2^{I!S$CN<=8^H^5=^7n_$zH9p@{!$95WxX+&cPAVgCV{0%RERaRaUMj``hk- -9@o`nIqx!4Q@-UqUv$^MX}F1BG}Y0n|-F`Na%9GD`YkiKx|r;$El9l0gN&a{KxxnaJI=C?SMVCEn90) -AXG@6aW -AK2mt$eR#QEaJPHp1006K7000&M003}la4%nWWo~3|axZpeZe(wAE_8TwEl^8#!ypXYa|+(;p65@Tbi -*p^9)e)Srok4GeY_eusqKx9;p3Gs}{PecQU$EIgGE9~CsNGxZ*(v`*f4Ixz*x -&|Z$53Ud&(UB*PL3^mgk;Y7f^>l@7&h~EObz8He%1}rZgg^QY%gD5X>MtBUt -cb8d2NqD3c@fDMfW+ykOKsD;Z9JB3qj9NhB&pMZ6-`oi?=tmDY)qNzxRhfTI&jJOBMSh+=CkeOM;tEB -n?_JNc!Jc4cm4Z*nhVVPj}zV{dMBa&K%eUt?`#E^v8;Q$cRqFc7@!6|8!p07J -PO&>>BV=8zgi~GXJ>~qvYM=MfsJMulpd<;=*+~dvaNUq-5bYL0y -ub@!3X~u(H`XC1P7yoGh-`zX#*@{H?enCT_`%=$yO{!MHbKly7c=A6QWMDlwwo;1yD-^1QY-O -00;p4c~(c!Jc4cm4Z*nhVVPj}zV{dMBa&K%eVPs)&bY*fbaCx;@QE%H -e5PtWsARG*n3qwfIJr(dnm!{o-Z747oeH3WvY!jhHjilnZ!~Xm3NQn|9I_VxJAc#aBk9YTdcRX608l^ -3>mj=v2_>X;CmsTkm2EIwLTP*fkomeB9ym*4TQaac0R}03PoL9WDpbhKff7xz7zxQL^vbt*gh=(Svctm_N*vGEo@O_ZiF5Ka=#8=&WFt(s)or}0hS-kW~ziZZnm-%045nasc8`d~(%ZywL?qYH=Iv&ArGiSVtRr>i_fz$+m&YLSZgo+H!sh1O1Bpruoa#s&fdCFy5=k*%t)EB0AjwX2xFW6DEeLnN_$e5Q -4%61@Mf(VAj`enTM96PXFdrnMT#dEj$CO0V-1Eh3Y(MR`BE+qG|lYdfcja$xo4iLHu2Wi`AESAQ(^;! -S>A*@H|MkZL;qFD$)ecB{aTv=AgCG-?I^67K!yKGf2(VXt1rWy^*_C$U!!X=`l>K8CBH6S1PJUqx7KX -hT%l{$dCZcvC~1us|wSRSFY6W8CF}Z8YHTXZSJGriRNvC)fKW?5LI0XA{UoMCmJF-GKWki2wtm6E-8Y -)z9KCqAtGHv8ckvqSMwpWk}>=KjPstq#}{LH8d3RDj$5=9s2<<#~_$V;4IRWRZI%EDUNQOG*WzB4gz*nJ=%uP?(XU@#`!vXeL*E%VIkIX -O|V16&N0;x3Y^59B1h;sYm@U2z!*qB-(kUb23EmO*aX84u5m(7uiieoXD?_2kBK+v0JyVQ+7Ln%i^3z -5+Y7$dV&}0s`IQChTF$4?`(;94S?cR67B$6G~hZ&+sFv*a6huMkEE78l!n3ggX&tu-6)~XVBpcz`dzL -Y&*oJR-nm -5`Rj$6o>4+<7**c6YKJgK>Q>YvoiU~^6xb6yVQ`0XL_jf~Sl%dO1f}j!WE4W0l?s6rgo)SX%Sb|jcf* -xT*F@}EM+7{wX=VrP?)LTvEXf*HS8&xJfp`4`!cs%&=ufS>BLogNc*U4Q8`6Nh%PJ^Ug)kzp$a7zHS1 -+w`yOMmJB|sYyW&++cc?kh|<`*++X5QZ31r2ffg1nZ%cXJjbn~B7cJrtIBs}!@)V7&aqEf?LEJbO6nZ -%sa>>Ffw@C3KAfbv}ac?u*9&yOv?r)uK(N@;!JW9FT}#7Z-8!8+{lzqIxTZ5Lt?2b2CrIzjBM`P$VVU1PJC~P-nZ8 -`aU0PJ1T)7Ql|p7G5|W-5UB}S4-zM_kUB|G+_v@gi6ugG;e{VFewb+S9;0Etl{oThL-Eka#Zh&!fH;V -JTMRIXW{!HOpD%0fZF^0m^z18yC<%Q;Y({p@}p&xp?yuy--&3jlBdG|>?#aBiiHGI};D)y*=HJ}!h03`ze03iSX0B~t=F -JE?LZe(wAFJob2Xk}w>Zgg^QY%gPBV`ybAaCx0k%Wi`(5WM>@0zaAb}16r4~` -v2*%wt*!a+cHj0dQGW4a$cu5_6bAcu`miB?*#jB$bLRlFjJjjTTX-hu(DAqy%AKnIgq&%XnS8&9h2XH -w-nuxITwEgjl`Z)w2@?s*#Ie7)2(suY)4UHBF|L;LrA4mF(*>db_{3mW~jDIa+-au6uXQ-+!7KV>h@@ -D7VJWU#xmV1TGP%oQUjI<=lqbncyqEF`=nnvn^n>5M<9Qqs#+}!q;p~GZy`oAz(b}hd!Ke5DmlsMmZ8 -NbP!I)OR*v}g%fdT4!{0v6&MP)h>@6aWAK2mt$eR#SzRWD^nr006fF001HY003}la4%nWWo~3|axY_H -V`yb#Z*FvQZ)`7PZ*6d4bS`jtosqFl12GJS_dJCq_FLGH7?H|?s;c`bm$+PLE>0b%v~Q1lz0yOX2*?m -6j{W)fAK~MJ0bLuW0V>BBx+YsL2w}*?a*DlCNCpoMv%vEhePSm5TKH{|F>+}zy|`s?+lXKbVty=4i0|&cnw2p9wlX7iHwJJ@Jkrp{Bm*p -?cwe7$;IT`@c0<+@%F0;+}->_SK+_#(mXp1Bm5fxy@Ql5wJ7FIEkgJ=xj10(19|^|k0py4F4{=g6H~x -l%~$-9t$_1_iGsbVDv+3XWZ$uJ$v(clVrPH<`aC)yD)n>k*m*?H(*S9E-B#WhFq_(TFP< -5GQpn-DnD^FA>npeoW;`K64I8!B)9F)4W=E~%{Y_gP*HKYs-x+qoDGD8xt?G0`4u42v1K=rtI|OY|75?>g?pjL<{pDq_v!beWjgrn>s7XIuDafPFZK -!Mh)M?EA5SF)IWZFhk8tE4iN!NUdBt#?)Zkn5e}eTT2uTp%o=|nf3OUuDH%KI=ylsA%Z>%;=FMZmD<# -Sz3_cXuhQ$k6W{pAyuEAAn*n5_h4?K;KV_5l_4A?W`zVk@4-le&SaDuCrb_Xz|y>3VKcaKc_Tg4|ZH8 ->8s!H4XMd?E#B*%T^%WD5wRDJzT`Vq^*5`pr -R@AXY9|}NoNJMgeppmP-^MV{u-Tnvx7W-oW(01T(A&=<&z%}ka;D -uB2II8`zL28h+3VYPF)|=h4vXUBc#HOuXUU4lni8o0D6+q)%AYo8cbRrkYYP0Wv9& -===!v_me$^X5*PhNlOab&l-EVdAGZk8>}Wer_UB1`>nQvM?}a3&4ZCi*ws%h_0Sk -I+*-5V*yM5SJ)_=k+>|lLAu7c}=BFyMgJZ1F$jt*V525luxX5Uzl_5(8I@E2pf4>-qr@UVlFnnxoNmTBtx^yG7}U9n1zJxbC#;s -q5%nS>BQ7S19zr+t1OoBi_wV61&hC%CJjXoZ&*h@1Xi-WuwS_sf5&Zl7JE#WdYcYvO9yc6ubDcOX)h$ -`*!Fv^wAL28(`~LI@1k9Y_X{TbMf@ZH+omt9>8ruj9#BmW0Ksok0G2~vXd{}7R;U#QMftzU0XY3mM-C -aiPhFUs*_p`j$vh79Z6fCK7jiP;6Rp$Ib&%g%2omr*;BxF7lkG0^nP=iP+B|g0eVL4vB)p94KWK8b{gGUHHMsOL4}cF -t#{Oh@^Dp1JtY0FB34V4GY@poQ#zQvmZFrYz6QaIrE?pyKrvV9<{@Zwsts=UOLsop{w~!z(S;N6eloN -a~&(t$aP9tY~W!9m3!BsA|*RysGDf~46+Ljv_b&38rcBS3>j0c_%rAblL$!x$m6_e7weovi`G)`d$4qf5sh?WLq`q8X! ->*%NFY%1G#U)_!`Kf^y>hf^7rcPod)o?zn92#Nda<#%I$W_u=-ZauE3yE#N8BrN9^rqSUJvd1vTdEy> -s21Q-SN=q!dtLMaHXd)oTl4A5I1oG09t{$x+Cl1h|eEhf4E?u -A%s!O60{5%v&pF;28IwTgmC_)$TOb$8SfVr879$`f#W6>K&>-p7wZOB42aFUC -aa)5~4~({&?OqnUr?Czb{5E;nGU+KR?&W@;i?(w*#s|EeYcyrH-;VYffOMJu+898YxY2t*Wkn&+I*(z+?Y|wXfcM21>WX6|z{WSze}MDWDSmqNU!OS+h@w!u;jlAz3@X -UDqhyq1Ef>&8>O*nxD32J7tS9-E2Ew-_rMcWxr)(9dIJ95 -TT(H;V(i(-Go}6R1#~@bBnfPv1680HESy$Mic6_4`&#N!QfXCd`aa+!1 -w#>GYE(h!fefmM|S)^cJ0NS#C!$t!?YTb9tmVf7%CW;W2zpq50?@>kVF*FKc45z^UKC!RBGi@W2dG!R -#F`R(NWSg^!EZz1J9kb^9G+Wr0S4#jyso-G?g}M{zy94~LkuBl_e9RE%Q*$$^1FoR<*brc_JoRGXHZz -%$T+>I-wZLrmU@3cEPk8ttbi%7b0LK50)5;Pqb834UU@&4}B!#&A_KnA9I|)2DJ937JAxQQ>4TSrnZ% -w-cDKzg#l736X=2Nbj_PlBD(|ojN8;%&L@NmIDPw+rK=6Ez -5zi(|Y|2oEx%g|N`vA{4oxKDMR=5#ma^c*>=JrYSv3+jiQ^^&sMtcCYxkUN!sWiz;a46;z8%%91pmim!|98fvn5BrqV$~hcxr87E-g_N -<7Em>C}yyC0RYv=?*@q+2r;sg=T9c{ONBk?m-;-6N}x4RU=l8lDuGF^|d|J*JvIqx;|~e?l`Z!lH1+? -;7daa@GU&;^R?@^>B@kww4u*4M_RIutxAy307bNxI~~CJw8<+6&i62*AMo+#MZ8z3KAmYJU0iJNKtY< -r9NnwXAHR{s=(eGk;c@b7?Auxm`%S1mvKl+F7#CMXM-p*?M_~GVeYe!UTu77M!f4NjJu{TxX*s!#irO -xnLx#Lbce0J$I#}ljK?zEk4m?@Bz#|0eMES1TZlWhmbBOVz0Up{Eh)b)sMcp<{zhNhX!syAvo^!+2ULm%X0xh%WZk$Nxbmj$W`pZYt_Yx8J?&E|mRJ_yg80XD2UFWpw-ksRy1wA&#wE -6ohqXI;_D_7;K!%RIjevjJlJgvo-?Dsjit(toD!`N`ih}ZA=9}$!wD-)!(y4?7K!qQAAa)Ze5_K>B<+8T;nAvTn|4rr0<}>Ze@GYM=R%IwWc#iJWtH) -YHxVk4e@5CUiOx!@6{<721k2(pvSSPl_#!LEs4QrtE$gPq6ugZ`@GZQWuKnG8|3<^S!hGhk@5xLHW0 -o0D+bPCZ6KA4JqGY0O&g#<(G=YRd*~JdMU5=3DN-P*IR5*-p=?=pg9VBL^&zpX;hQ&a-VEIY-;wcTy5 -5IGKO7UspI;V>g|;e2dQ#R_O7EYI<ChV8( -!n4qF%!RXIHuOP9R*f38J9e%3{?$H!{MG35DqOnO07!NfB~6E+WN?dPa_t;u^tj5l|E6$_o>Ah&S2WR -#4bhh&4@Fi7mNJ#5eO=!U@Bll&}Qm`e=|q`I=~>5!NDGdXJ#-~b2|PAv9@H@+A{B%(AibzLQOFvVWwwuWun*={C7y$T+n?Y61t9AWk*xQ7K^XcRKVg8b9XR6zW -pWbbW-yQEz@I|!VYDi2S$mG|LxM9+1jFo+n7n|S9m;DkaWfR-8tEOQ*8x@zIgcnD#S5pJ5S6)U686 -R-I6Ve?@h{Sh^!`JNJNnG@b%E*|l2YRe)Y>$rN;N4DaBgQC-;#S#bWh2kj%s -g*ZG|la#Uo*XVxVkKp0-ejFvRbciYC(1Iyf{C*SvrtO-ov9>VP}#@Y2jeR)?)8$gM1mxfN(*`Dujx9f -0PA{t=h$L4)Rz${gy2(=QzN%_=`$$t5~o1^YwAgI?mE*p@CcLZC=TWCjSdx5qG?!^6fPP!lWU%ENiw{ -H4^3Pf@e_RXF=tIDwTFII9JHEM^(e-=o8I3nH$;qICIP+&z4VTxvzeRZ#e?=V3@R_z-`k0 -j-4{oViehFXP@|xfs4Z&Jdoqtfov5EP&iTo7saqQ94@4r0bipcNT)i)UVM+O?$4sKt(w=6D|{-4tR4N -yx11QY-O00;p4c~(=DC2d}>1pol%4*&or0001RX>c!Jc4cm4Z*nhVVPj}zV{dMBa&K%eXk~SBX>)XGV -{#Te4%P#f;^lThpaL({8PUVkioMmZ+EwMQS7!$1U>TcSw=4r6f+{2U(YM -{SMFhNLrLaYLsF|c!p(<*3X(2q}R)(C=e+yWl^OmQ^`{dK+!dc9soazt)QDoKKKO=M^djI7m@_kxqbqKD -vTp23*?6S8p%Nu*qC!hsc%L|2m6Lcbyxign@T6D^W8!I^QSooT1FLm>3fMMmYa08x9VtCEp$Fc^T*lw -qaNA6StyQc0>bO+&HzMp9$7ju_l-u;i3qhKjI!1ddcGxbi8PUl0V%{l_{EjIJ@G8JgbQczsldY(7*34 -69Vqm3gn%1nQiwNn-?O-zudG!nKNe&D$l&dGClSR7!D8Gm;@K1j3Aojd!IGqgMn?r?WSaiKd_Kmc4Lkz2W@$W -71(5dCqx0}huZ+dhm}jcewCg|0NBQ3viuaR(L6ySj^30$wHZ0oT%DM`_Sfa_iQ8OzIYKA^G;(#j>wS^ -ZS*dk^n6-!)MU}_~96T1g^xv;$Ew_dpJ1Ay{TrJeBk6Y-u`Kd9kwQm1K*d;O0-uARK6~^S!9^Ik*d%X -Cf^p8qLH?`c?G^n)YyA+x9qC!myE+Qa6SWz9_4qSYJ=$pdP6W_SNuz0UI$;>Me~WBv{|E}o)Gd?C)0s -r5I^_Bp}G6Ac=2`6w%0iG(SLO;(_stu`2l6ypyO0`^PNk{&X>IqF&EoN*4~HL3`0)eftFB)kVonnQ9_=RcQg_Pl5P>o7}iH(Dqe?8j>mK6pZ&qYRaO -0t7RerqSUbTh~=4$m5xNx0dLP!3_D6;7{^;MVxqTens!<@W|_?+2N!oBj9%HlIgF(UDaG?gnVmikYUx -kT6z);}s+Imc-HF8wfTRHxQ>1C=(F4;Xb`K(b!#uHY^Ftvk62&BpJCTOu*Zy5Z@_d*ak%S_~KKXiB!d -($tpfuF8_{A3z)h3=G8y@sj!>2s=FUc!XQo(E7($`B<4hKh`fqnPHK?~78~`Lt26lXB|ZauqqHoa&3= -1nU0iS*xHDjHh*O>q+NAvMoMy(p!)nl+pR@(&DBCeQDV8!T4*?#!Zted0BWCWL)xy;Sw#R$#JyV -+zqLUZ$OJ~@WVbYd>>cs!X}?c1Md$a`nD`4BZT$bWB5Z`us0E<3k23SO_>jC#&_a!i4ACc2aQ%-JFOh5z2-!zgweb{Yc^g -A%IhN{ijipxI5G^;?8Rgh=ByP)h>@6aWAK2mt$eR#Uggy;>Lv006Ta001Qb003}la4%nWWo~3|axY_HV`yb#Z*FvQZ)`7fWp -Zg@Y-xIBE^v9ZSzB-9wiSM#U%|>KEV+uBjg6o&Fb~OI+I{FIXpGH+VT>t>@^CGYib&1uB*=g7IlM^J# -dff02Zl!$$&crLGjCeiX_8aLzP;LT`xlq~>64PeKmS(qe6y)K*^rjE+3vtZDLdj;8}-oA;&HK_b?n!k -DXlW4yS~tU$CN+w8g6|r6cdVTeqe8Sq1^>pA)A(Jzwuu;H$VM{saC?~6wr&u8oo9Atatj3DbVsJm1GVx>UH&x2!V>4Dn) -)~9j7(_jdiiiKe0jsl)=J#%D!pgFsrMSI6>iq=QNk)Y5PG6=B~FS*wx*h(UNtFY}e!%`)!%qmTU$DUK -wD*6+!F~8b4-bJKO^5qh;(^Eu_#)=F5(P>odJp*UQvv|N;~9B>-ViD36 -R8J3x-?*;i3x7>f(#0&2?UOmFbIR>x -Mxc&05}_ak3loHld7ydcHEXOZaAgVdF?NpB3-oXxytJAwc~!wVQGfw3ePlpENSldR=IO{5SjN>r -M9R9Ehtizt5e+VBQnHsv;I12lvcM`OQ|P{rLLO&?W#pag3TLJuN4Uz9c;uj`d`TG-vVYj)PD7&;#wz% -5wxpVZjr=|_pg!^@6kX4j>@wOBEa6wWvg{%%>?kqPE_UE29+Ff1m;|XRJ|qc&@IW3@azHnj$LWk~)~G -mHpeAdifYNo3lngNT&Dvg2mEux_o87Cm-QW_+Y#vDua{{bAK~M)WnG%F=*7i=bN_b|vGCljpW_${4z)|`O=U5d*aOoJA~hAj5&+~e!GH*a5p7v6JaOZkRJ*XeXQGl@yBAdx -o*K36%Qz5d_DOOP-{w(KY_Fo!M}y23R0x|w%7^NV3q!~Dri1(MlCkuvB`p`F7ak01tuyj-n0!$zM|OU -|KnPLrfn#jPgx|FKWFn@l3`A&GC^vjJm78THG~hQ(v##L+0wBhE=bDI!!A%V*(Z)KAYez`z!}~ITI&! -@q{?y%RX1}O*7Z~x&St&|2C9g&uvCjo-HTj4-c-wWdTfhK-QNY(x$jHCxBl9I7weEA -;{V2TYovq{6~7fNG_!Xo+bGC`nyG!o?^B@Bv1fEEG9F-(Fwdk-s?lqABH$cVysiQ*}Md*FRk;=Lr{ml -fz!wYp*0Y@?kT@f!~9Cw?@9VU%k2;dIi^ZOzeOw=zI#3=n&M3%$O@_7-ZVg#1CAu2I%N{TH(%Z!0V~T -UdEVwv@;H{JN*7flmTEvbqq*J5>K6BGq;ED0fR&3v}O}$W34ra>By!V=WYCL;X4TcvGF0fzL-#^bwq@IxP|j6!@qYyuBE*4K>9h>AM -=*{wlqn1xq_A*N*o_%Kj{br7ia{NfzE4(_$l6!-@28%BjfVHQ+iNb#N&0F^@o;m#fnZ$o}Uy( -kD{6e>$+LtG8XApkN;G~}47SxMKql-}`Nvs8nL+ma3HTT-B+!Y1nsgkwm6(hZE#)!PcXze?M}7Ky-q= -SaH}yAtgAdCtLIP7{ZYE=*pw{FwA*Q$`9oJb5Xwz7=CILgGJ$D)p{37nM~tSU>>eBsIJl(|GDs^(2kH -s+-aZoV}QuF^C!PW!{~$iuLNQqelokmWtc4(7nrA*~YfA4Sf3f83G1??GoyCsDSU1P)i{Xk?z1=p&tL -rOjy7yKoC)sknNU4#l9Dhtek~qf7}qdJRVT}EJ;K4pf*p3L^tJ_^^PdKMgF^i>fz~72i^$I0gbvq351 -h`AT?tsKfEF~%97a5uaX&@8BmN3Ci#AN#uOK0(}oYQDe)~5v`iMOgdj14qyG3Mo+7dDt{g-PKFh(v4N -^gS7kX$e57>;&z$(gXQ&r4lu+qMd;v(m8*STo|Z9CkzW|(K@5_d1pGoO3B5=(v*S{(Wl6-Un~=`WtSa)-~v-Md!WV1g+5bZSzGb#uXcG+tGJs$>x$!yEB}fy?11mUl$ugUXXvy8H7r0wEkBgOCH;Bz_C2gw(zvqSGaUQyy`jC)pk+J8 -wddb2!U(5myxE%vH-!_F=0~9F7&}^!^U@#szOwEKmWBB}HQu@WJ(TrHk4aO}@mav)1^%-VjO|03M&jv -sf+A;bqhtp4QPyTdFU5+~9`H&n39rJgGWZC-q(Z{n_F6JQ5lj_D5bN-Qp=e93Fhv~oSaSZq@{`LK1KFG2%;JXBZZ;t~yX*-z1r -6~qELg{_4W-$y$fn%MoWPs-Dw-Yupz`?QO8e+NWzu?H>$^*X~@vgo&T=+4=81evx9{qnr{Qm!I^!qPR -O9KQH000080Q-4XQ>Q(-((V8N0I~uA03!eZ0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%h0mVQ_F|axQR -rZBadsgD?!;^DClGAfa}z#Bl1(ow}o3Fh`_7kZqR!`nCy>5Sqz;A94KBp&R5`wQ6EOX@`$OX=Y{&itg>ID*-M2GEp$6uA>{iI5LLeN#`^9?Ncsj?{ZswGc|i%#C~Ka8iL{3q4YARwJxsBHjk -by?p_W|^xSy#0EHndf@7k3suOjlYj$0L_G~EIlk{`7MOAw&rthaaHJN%ktV$3Zewe2<4zFp!<>L^H(i -)Ex^hFg_fo`;zO**kV29*`g|xBov6ZXUTf^~}@tayeD&%HJiFX}k!5XB@p&yZ}&30|XQR000O8`*~JV -8NawNHvj+tRsaA1D*ylhaA|NaUv_0~WN&gWV_{=xWn*t{baHQOFJob2Xk~LRUtei%X>?y-E^v8EE6UG -R&`ZnANsUiVOwLGE$jmLsFDg-R1By6<1r(GO^70E4xzfNYi&9fEi&2#ZfrL=i0sv4;0|XQR000O8`*~ -JV{LU}Q2?hWFIS>Ei*9)~vwD9v -WB9ugC9IUaQjN?mwN`otPbt)vt#}1)!S8#)>iw(u)AMK8Ig75>?s3W1AKk-ZHk)OTv&2B!Xh>8IGTCW -iAiwBW@KR*&z8})m|q_Ql>)TGUjYsi6G! -z0oPL#seZR89P;_*TdV#@n6Fzn}`83u?H`FabWdh`vLCGtbN3L&Osa&aKsvLI~&UWVOCp-`uS4=OOFU -INBYp`)kSWh0N5>p!r`B@rvthPYK2mTOqmLAF=rEdiD19^KpA#-dcMHxza8RE+p0HK)gZTGAIFQ$7LV -~!R1_WnG`0d!enWmz^>ZphekD2>ankbg_DK4n-)@k^aZXouN~n(+c<_2EXFK##nf0)ihofHD^(zTfZnk{KZO9a;YL`L;2;X?R#lS;>WxkIPZo{rc{ -ArulG{G8dqa~foGk-EeBJMWx#cYu1VOHPD5T{D9o-|~PdVft$QAV!bQ?KJb@oMu8QafZ*S}KEN$`-u` -1QYh(w4_c#?b=-cQFl`yr`%@Opc5g5O_HcPZlKJU-CHI!8dQ4UFHwm+f2Pu1z0)6e5@TGVF;-r!QVtJO5xa_H>hn -Pd*2hCuU5=sy^gr@^>C`FQo$BBs` -4^Lz9$jko4`}r5<2$8L@1XR}b5s)dWiJlgdP8ep~*cANLtKXxiIBZ=k;sTyB@c_m~i$O=qW#oDgbGPH -LQ+j^hQG@s9+dfZKn-*L?xPgEgJkAMwREG_iZUN+7JkaBR9|6u-VQp>BPEH<#Et(Au&(#UuSsG(LapO -#wrcZ=14VaTRuRApq7tj^qAPYhD7driFuC0R*FX_nd|8n-n*9H5HeethfpSd?`Zj1Pv5*~)04omjZDI -8#heJEhHh5Z03a-;Q(@FeN+R<*;}-EnJ`OXCW -62(=C;u#*K|Vk5EzaA16oI~G!tj1Pa0DCYUBCk-YJtZWGUqe79F@!BFw`GSYd-&N(R45$)R#9ySWg=e -ent2X92^38Y8zg%NmFz>pZUllZ7!bWxFZ}*E9TRm^w{^Q@#F6QXBjp`s>)%z8e5%YBV;6u$Ky&Gfc_J -H^|-qa&_mnrl`}QXG=(s+VDe&_}Hw%aJo_o -Kv>fJvAgm@i%j{DE~1pOl4e8_E)wQlzC{s&M?0|XQR000O8`*~JVsK-IyX8`~JSOWk6E&u=kaA|NaUv -_0~WN&gWV_{=xWn*t{baHQOFJob2Xk~LRa%E&`b6;a&V`ybAaCvo-!EVDK42JJKh1XLeWjw&7Ubf3lJ -51UcF>pFG3K1r4_x3YMN>Zuil-T}$ejE2G9zm&o%ws~Oz#WH}GFW$VyhVL2rzOAPi?YUK` -?7vvZJHbg>hGOVC1g{5RgX^VDn(tgAa@G`iBwEu_!H+rsz5c4&=#&6t7nlD+z+FFI4@RnJGxS#9SbDg -vtvkCFsw2yDW%Y2Uuxmg5cj#+y}(3yh{XfFVZD_ -B(!O2#6MPoE4^K)c!Jc4cm4Z*nhV -WpZ?BW@#^9UukY>bYEXCaCu8B%Fk8MOU^G!RmjXO$S*2UNY2kINzE%M)=?OVaA|NaUv_0~WN&gWV`X -x5X=Z6JUteuuX>MO%E^v8Gj=>GXFbGBOp2G4emJ$z8sfTXR4I;}SDNX`toEUVn^mqh(vwb8kxL4@-#CG{!^g+>l&v-7uU$o -njI!`RZ5-!6>wW^F|Y_^n=mbPGRL5ocF67PR7MptxBi|&RyrdfhWVi)?VL*4^T@31QY-O00;p4c~(;w -9HPC@B?176^#cGN0001RX>c!Jc4cm4Z*nhVWpZ?BW@#^DVPj=-bS`jZZOpydvZKhhrhCs*gjL<_GM&9 -d-^grV&boM0%JB1r~@2^7XbA59>>NyRlw+FM&(!9cKNgA*G9-5|sUW;O(}VbD#2Y(i>7#2bv-FzF^lHZ-+Cn -GGE*x7o7D&({FQ!)U{h8#>&?_$I|SOuQk{4H|3$dcy=88rh_8cR-d~1^5PuH#E9oqPOdz1o`t7lh=h1 -c!R^61mA?=28=VJHaNP8m`#E$SB$X@k}TIe##bm<}%&k*v1mYxC>D=4<-HPqj(lxy*@(4rzo(8w?3W4A?g5yn2{N<_>^bXe>-v)EFVG_xF~(GwiQ*ojYW8 -_G-pdG6B+-1wo$1iC-#xn{DXki9zX#W6mGC<7~^&S_>h%Eqf5+*^H}yqpWYPL|Z+{jH2yCKehC%lzps -pOg?243T3En`Lo@6GnD9zGEBkVH-jqJOUrOMC1jBSG#l=o&~N|!Rn@L*W$g~*N$kM7+OA>cpcaCbB^a -{uk2Q&UU+2CPBrTX!wq^)@M$8u$l9{o#)5a620IsfjbrgivpGAW5aIFUDQtbHPV`M;a2a!Si$9!8eWu -Ip4MN=Z(lsUNTuM9~J-U#qa93i|4LRYVa@Fc}Nl%Y6Q>6CksH%9?hfyq(mML$|3wQ-}SNo7ZGQa4ZKG -Ypty14}zWxK;+m{d+bM##w$ahsH>G12$#p7SOm#vf1gNoQrysgRA5#(eFtcwdbsAr`i)xk*4i+fuA~E -%SGbB;U)6>+|h7o2~VpZ@G}Ggj1GC1f$asQ%DW9YA5@~tV=>TvY7^+WcgxTz$3-tGI&yBVHCObXo$8t8dyDr1kq;%uFo^?GJbGplo1dC$v2W=5EUz|UA ->*#i&0BrI}0UQ>}lVFg`N`+G55ENLg~ko5TMuKd^zMxEFc)1L2*dD@2+zdOv(bU|_})@44_RwF4~Jj4 -#^wjEmQnj2J!H!E)_3Xh^l&TxjS%nvX=N5(#qWV<3V5yEIat#sR&Oq086W*obQBczO@9VYp*rgdtX2o -Da?`!J`2d}a2zs`zS~RViOnQ+2!a=m)|7GET -d(lkL5G=dUe;fn!tKfa-;4V5g2Ai$Il$dYa_coW7O9DoVlMCn3<7zT}B;)^0S3A_oyg&zn&3$Z}}yQl ->fFrXPAO1g>TO$_cpzi0$lh;LAEtK>KMih_ky@oNAS{yHCwhhBIUG8-7(VB`jkHYu}-0kzT%g)GDjkY -&tB_`({%6uu$T4H0he1z8JK!71P0R#y!6omreih+GXE!-@x`( -?{;3xN8qPe+DLrJr5hi1NY0ve9pnIoB()($$H@L=6&t-Loda -Ftb!Rm%>rtxYahP#V?qE1;O8}meXC9QZy3We+W>)wufad$EMbhNX2e)j9XaCGF)7@pQbH;L$-I7Pr -x^ttJvkl2l*^#etnrBCmWrG8E6?hJ+tv6w`HRuazLg-jf41Ni_DHnZ_tUtE+j!>(7 -#7IM%i+IHNX>j}SCn;v6EKQHi6&t4ZuIi4Q(7TbdNpQ*T41zQu2gl5>I`V{iS6?%W#=H+SI*F+Wb%(Q -$D_Ge|<*PxTYrmwr6G*K2fdtH%N}3A*(XQ$g6lvc}@48F~p{W5Qf*mlo7(bNGpCIZ&|lpQ)K^8G}D-zRc!g)`NGZ ->8`VH!+-7@yuk5|~P>nSOS=A_$Tq1WI!iP)6Z@_*l;?fDCHRBLmEw>jSt4 -8e>>sdEy!P#*>;H940GX1uFVZ3k`l7jhV;_Cqv_(IC`ww8G=>G-K_hVlLhWW(*9yp8JvVk2F2m8$etvNUozzf{rXtB0H2}Jbbuw(Qk48S$1zaqQ1TS)c%B)fQ!0Y`){<`po1_JU|@oFc!|a1WU-0Y_ -J7j(OtYpN*#>u&a0DY3o>w#S>A5S96NbI{ZG{3AUuCz&^qUp1VO|tq;+a1-4Knl&xC%1)lBX=Ph(AIG -VUFaHV2jluy-SFCUI}%jcKfiG3cdl7JM^dBj^4i7?|4)(+4UZzub%@ziGa`5_s0<*)tpD!Q6LTh!IXJ -DaxZ*b*o0Vm$TP9(7B4bHf&kL9a#u2oFmQG)0EN{48JNE -NUSE{Zy(^(SUw>P0{-)-q3vPauo3!ocSWuq9?DD+k7wSs%%a+<_XXR8a@B)T&IuIUVFc -?7VTv1rU)V!cA5S1Q_8HjlPg~kXW;PP)D2KD_g_02s58ZKRR^w~gZ~adv~ls-Y(h-E#oIF53CgGo*=> -1{IOPv865nA&v&(Tl+r7Zl+Q|NH?&R)qzbI_vNVRc?cJN>hW52UJs?WATXZGZHEz{MeCOl3ahn>TeSrNSCWVyxOR_9*1$+e6FJVD#WkCSWT3<=F%a=UQ2h%Nc -R8dyRNRC&D;Q|szG{Adt5hSK#+Yo6k&wSfsP60utdHx%HlS+0SV(GdQ*(t$ptj+2}?*p%6!FVu%oT(q ->#Rkm~=XZ9@)doFU!3-S{4<@(GN>McGvU1v|+zL!IU^y*Zkf_^sHC5*~u}V76BE1xv&&XNiS0Tq%L -B4k$Cwb(+bLo+rK^gM@(ZFLEt1peMM=&H{hCqRSk#@1F%LdREz;qUx8jGT(AbP3lJOtbmL!A8qg7VYZP8gGBEHYxw -u~f1NM4}>jH+;^kRJ}dJ~5mlwK|j3^p+06#g4ZBj9$xI#ZhfP@ddSK(O2?43-ViN%e4-7Z9+&IOSo+P+&_}Ag%c?ln{J}J#+Ec>v&qBIw1MmO?rAyio$RTVNV -@uYF=2ewrMm{c&G_h1^YG*cTz<0B4x6B1XgqUw!U8ZfX5>j}_XFt*}>ae)ID8KTNipvSBF5{!N^ -<86YT0;(Qt9r=}s&8I&cN6?L&JMG#PU5uYWyTq!BYzyZ{f_G%0Ulq$M0>-K;nF=)sWC-W5t%(Z-cJwc -TnnrI?n&#t6>~_*BU0(r@)5bY$E;+Z%sEu#WH?CVW3XA0NOFNl*Uxhwgfq|){shGlgV`lEPd?i{EUcP -Nx^;ZgWz9UskCVYIu8-QRY%0I3WK*HCKiK2OkP?Fl^Og{c{kn6qdhEoZgVQ~)Ztr98 -U&6;9;K#wsBDPqZrxOS(NW5LK1_m-@ekP2MCVf;SDNiXkl>nOB+=nSLhHe*l6jBoT3Y!OEx=th|Q|V+Y`3#l -azQCV>USAacj^iGdl!llPD?NA^?TnArwl%aFz52!)Su14XbhO7B7pjz_KFg;%Tw;cUTRvcUu`!u -VqwhSp=o=CHx!%@I^1{iEr?Dam3&S3A!;0TvO&ZSfyU8seq<9w!zaS79EkB7|=9cqSHjUfs^GZaMUQZ -fSLmLi#G|eGy)`pa~J$qdlbyYkvS -sus?lVr-;C}P55(hh#$UH+x~2bq3@1Z%Yrlb>k=0Ltr7eMcFQS0O)&tN4~_u{rtj-!3j|&0&EzS7x84 -sXZ_G}h^Zp|C8sB|TvHX7gGw@zPiv#|h0e*)$ssZDx&)_B(4c4j#v?-r$kZ!#i$1G%jUI(rmOEbMuVn -2*9^4$pYoEp~JLjytp(#Lo0F>}BRM-TsJ3pyAGX8{vDe;Q#hI&bl=JXZjh0+fO4SHKm6gZ6X#Ze*5ET -Pj{r0^p)q^oJn9=TByd(!Na35*<*#clcZf2l4`l9slN`68JCkP(%rdWuO)63Qnx=3Ih5|39%2HzSQ|u -=l8psQ82>P4o)Odolc0h)Ij(;l1yO1KjJI}OdShrFXGoeD -&-R)8w!uJ&$S*bL^(V?1d&B*T@FF%JSb46f&SZC#+wicCG`oqX1cFlWtkv)tfe}TO@9T;c_JibEM=e_ -7ATa*AQ5Z2D>Wp&!6k^;xkc|ab-lZ2T%mtZdtTynR&gxkx`6ISY3R-iT{=?nQhRpsMudsr#XqECv1eq -|nAhqlfjRCW~I$mF^*urnNIdd)qMvL5s8intvMh{1)@Pdiv2@msd#^mZ=f{f$^VRbUG!W9iq!^7He4=Y-d5@2Ob|rd?P>n~ll6{2HY<8>l>( -*W1pwGA)t)yY*yXfJpqrgx1ds34sdUDtW4HT-fGyP||6XE<17ISyFvyi=S@1imm -0yuYwO)SQf8Qy&&h1HFqH3laDc8MSQJ3xu~SOo6qtyF>fawV$m1^h6onT_I*5pGJ*8Cexs}&GpRm&;i --ayMDML%1QIq-f@~du5`GLk!Tyg>F~%M5>2=nIAvx}a~_rQq(^qrOm#1{bW97$x -*4vv4Vj>ofdrrs7l@F#1t-xVw%Ez-BPGvT=i2y-l^MkJwl9YM0|;v!|vW)r`A|w%o@F7_b!1)HIha2V -x3&95S+&p>77>N`4;uzGA{2AZM7QKQ1T$9(>*q_BY!=McY9yaQf)m?Mk46Fd_1!DfxIfm=z_8ojojgW -`*@()!#HfuLuMj11P}2!5`uYHk4=k-Rqk}wIw#g{YtVmN)UYi31GC?v)kTzaKsxYE-NenZPW^RE{vV$ -DAs60{EoXn8(f;DQk*|~iKs3cH>(ao{CgCClfM*466{lWd=xA}VOA=SOEIh|whs9%LITgj0%qcQnOgq -pUbh?R|4N7cCd=mi?fGmH2TLwo#OH3~U0{@*d0FFNZf8b3TE_#JZ76pJIOC*2-H)EFUEIe6;w-|0}5i -Uf!#K9L?6D)y#&;b~Hpb3_kc(Ozhl3+tJ8wT{p|56!fudw+2Lm6=F*Jhjg#cXelix@&0_a+m->~u1L3 -tfN}6`bw?E(bDzo9s+KBtUHiHnxDbt^xQ<&1^4zI=k9|HD@^){`7d0T_zCAN6_nZy2xv%v -qOfeYT+{O2wn`kBE5dri#;p4`y<`P5DObQsD|$;KcaPwh(PRtJLJlQh}Kw$iwo0VXr97o>>a>!%Z9d- -Q~cbuRPB@jk(0Iqsj6^=O|`K*5gMocK;ByNjOT{atlZt#FUW1j4lSkl{gbQO)BOy-c;r$vK~H<92mAIHw#kt;Ag&aQ_r -^F?o@I>_+5)}0f54b~vc!IQ`@S$2@3g#z2Wa_n{JfIke1C{0;u6*4sNY$*5rCeSb2 -#3zE|cX`Tiw)U%&suJOO>I$@5Z3vjZQJ$5pxNed<}B*tuMb&(vdGZL0frv -`=m(#*a%)RanPnOolXw0lo`#ZDrdF(ef45uIN}YxnYgbVtd(INDCJ(E2Supadta1!!`x<4cB -tnX*Y?BU`0;O-->!R+*8svZf(jJ)!e5ko;S* -;ol4PosbbN&BTv#ysh!XyEY=3BBP(n2k?jw+X}XD=)*%O5QSV&=Rj7cM5&dB>oxJ{AlR!dY+LSy3`*s}`=r`KI3jFZ?HK^2Vcaf*6?dk9 -;u{p!KuyYbgKSb3SPN$NoQY7^Ru7P)xj5k*RR8=PA8Jlapx4YEE82Ceft@AdbKn=Z=P7qBsgRSg=qtN -|)#~@aY)2#GG*ecyuw%=qvn9!wVq6-2m(vY%$g;^&1q^#KPhDwVn;|3(g1jJft=$;4QI?6N{H2E$$m$ -vd*H{A)HtYd-_rW6!psZ#+HTbG+6=>z`{rPf?}HdD-=upGSL&Cv2UI;$}jWHOzUTZZj|s7`YBynl8_M -bbAR!ZKVavltmx{8KGOHp=?qB!WgU3bUpZyQ^B@2_@Rk(~==s?;YqK6`qW$THq4%_C{gC!V9(ovD$JOc~+kpBuxz&E847Wk=)_7dny<*qTdzi;I!eh!1%1|*Gaje##Zj0! -VS|@Sz71uI>n`>?5mDLcfA63_Gw+%A({&jutYg_62oy@ir6K5lxSc3wiWvZ`NdS{CCDT=X> -gW5LYgGM6nwkzP-xqdiHT5@tcZKaFy7|*_uI7K<67upZ_hdbg3lLX6J;jxxT*t$V0)|~0bb#+z?jX58 -NfHGHeY({L+YllRhD&OvnCOsqJ!7!+}6W7}WOG9y`lxz5WMxe4gn22{*U{&Za8B2;tts+0z3&xST?wL -@!PbYNMu9EZAljItcx*?oTc1xIRs=loY$c{$R9-j)4k<1jDC|B)k(L6{|4IZNdE9ym;$;Q5kcTw7RmF -o{2qQ=kbXb`!l6f<<}fm7;c~ -28UXX{HC-f#D+dA9|bkDcP2Gx6i@>I!>S^!W;9>q1|%3655bzOR!hKVrUld}6k;t<$}w3PwNK>m23Am -@^3v_-dY4Z@P0i;>8v`w@@mruWEb5v>Wf)x`9Q`87&J5X9$N_4Oy=?aqV0<>~@K94N(kthp#=4nu2ya -$Xj9ux$bdy2~-yeqG6D{be^vqK6JY~XLqTMkRI)K6>QLOy%tV+v|Fjd$srZr4(z^yw?LZ1tDCLgjf-! -}xJ8ZC5e6UA1v#v877xvblQ-EcGu|bEAEvinhD0LHWKbLPo#|%_B(Tq#U&aNg@szdc3 -b5osLZhyrOv9T@4Hv#WO}TU3ZO3yU(4*IGT`UD?WD25!VSWs~}gCv>1f8G@I>&1rHd8_g?K?vLGL4oI -Ju0nBdkNA>OyKzd9?f0M#Lj^TPUe9!i8;S*lkP=ah_%Dow~q9qP+n;hwQy4_KAc^BCb_&7UPS8&_BFw -1<7D$wgas$tlnK=2&K!+!WZ^_miuZ4l1fB_)YZB`u?fJC@H}^zQSbAp5@zh?K57>Ob{L%tZg*y)T&Vr -x^O^7l$BZ&zghRKrJ%c8?8wzx>BN0aJN%wy9qwij6x=ft$1X1 -(T3(Cb$GemlbWulERO@6c~dq*#{)HB-;hSt2TgH0ue#FFvJqH0J17bmKBr;-h{7O1EAi}S1lVwEO8f__+3f@{R*i>8!BCd7x9ve!Rv}ixHN*nz| -``;1hs@(zz8vFLj+4!1s*T!C=|BnHZok+T9!*Cn9~US}*J`5-XLG>KA*35WGYK5ozZ#$xy&90zD{b3v*Cb$5yLGDYjQq<}p&t4fIXWfl&cE+|*!67txCYb -lZ0b!K(A7AVzEY!~k>sP;#as{0_I4tfuS-BsHGGIWE~%1Iz}a!3-)b$v-FZM@xx;-cExK&_=zeaMQH< -_Q$`sV~X6uc7tzfS=ToHZ8|I4@rY4gygkZ$TnIhnvf^1&k2kp)vIsr;!EI-sY!YtmyY$> -?g?1cO%FT+13=~1Kb5}d(hZ}vzqBbuNf)7Px-(~wt!=opwQ727KS2LW=U3m(&MSCdlGk@H(Z8f_{V<$ -s!9G!JB1NDhK+jYgT5MJz+es7i*6OgH$t?qKEA=E>TqH2B!G<7{Wdvst}@>7;6T(vBL`QmGB1^VOS3e -Vwh`5YOiY<*4Hy5O(HWo64{qR57X$Fku264_(sYcj+yE0s&2W>k0}_++_}1;Zr>W$VoTi5vWagM-)n@ -IA=+igC(7`CN4v3eF}1zN)h#OBWAy##mnBl6DrNUKY!FrAWU~UDI2H5&9v-XoTqV=3!;FpT1ZIl(@-r -b$E#$DUuE^qX=E+0mVE%?hQ}bjoz{vvjuY0Rou^T(ra}yIy*yLOJ#7@uWywgZy}~Vs*g}Kv-|hetIw6 -IpORw04^aeC%$O;PDwwNw^N_Rtz$b89O@6Dhij~p4YO>4zS?9*COLOL=Ut-rhWLcuGanVPjm|=tAImk;yf>>X&X3(Xkqj^W7|PS~xxjfp!w4 -&s$WmEwuEQiFtk4yJ(Nw9oW?46XJmI<7}tBpVGY)!PoG=HubPs?31gN<+UQH^(&wM{rc-3BE3Lv0|S| -GueWDswN7|%IW&4;JJBF*OOoQC^0f-MLH)zL{!_i(POwOPUC27y}-A2?HuK@MI2hRX70^7h;^tZaK62 -2?4&mrc6A}hyn?-qx2H+ThTG}thAtnpq@SGcH@a*}dx+t6csmgHEM{x+2?-u2jsayUbDTbKhu#s&gWQ -r;0X}9tq_5$G#P{oCyUyD9w4M&kDJ;V)$A_0=r}+d0mGrip+%h;uTx%6-Pu+XZibBJP#Fi}SQ;txr8R -s42XFN;_p5rxgZPrDzO-)TDJ80gXAKBU+z036&N@-!~JCh#?WXC;%VGReydb^9)9d$uvJUC^pk(o)vH -<1z-M?8ctkUJr;q3(U}S?Upk+y+_Q4hIh2QTsLx2~MWYtl7D_()Fp;iA~w#-lMngyM1RC&jfHmt+GlQ -FEi3yD+J#IduQoSoC>V4z57JIT>|`{wQ7ga8#rCEeTdgWAyBVN;xGF9C? -vQqk-Umz;$;U3a2lK};ZZP#5MRQXFuVxb0ACWAB5YB@$Sb-D6feeXP;`;hFtV%(0_6=fJRU4Xut0eO0 -Zjg7s*vC=3xehD`-0$?X+oUw%cQ!q1<`{v&ij~Dk2dTG77S<34<#SkDH_#}EZiqy`L2$<@=Q+Lvi{dY -u}l0wpaoy1U|8aN<*POxEN#>TW2qS--YQ>zZiC#**F*ceBZGc7wBL>F*Zmow-*3>s&)u3>!zlWso<8l -6wicd)-5!3d{bR3DO*_(4MMF%we(WB?jSO$0&+me|rn^Jo-)zJ6)qZxi^6mM&-I(nSi&J@)FLHc6T_` -*XSzjef6%uxVGr$i`INqFE7(9~SJAKzTyrNkXVhG8pS_9%iL;{EQ3@GI>sBS|QM -|$=`u%wHRB$4(WpQTjk-Z92)#M8eDbpvXnk-%$^HK7cl3Z=&-1YBk9WoqD>0tQo*g?BxnglOd2Bi_^; -qTb1kj8@Qm)Cd%6ZL72q1#0A&65;xjNMiHJHJ(-XS?ku`rvrFc%*mzxYE~q>r8dXaBtBNSv$vFMV9O^ZJaOS*k@lW}?;d?wB{GR@1N3;{#k7U&3xi -*GjNT3x$D&t3%{PW&Ri~$p`SnN*a6(Gug5^OA=MQ2wAD^%+J+}5pl$EDWUtunkI+0eFr>7&rXS+u<<~w^?cZ0Mg}m>8)hprw>=X6B%SrG+b1_Z6G)V6^pV{9Y(baqy~n?zrv4=q^WS@?548P!7xoX8B?wF -*FwMX;4PzvM5HLeBC_}>tilZ1t;RKA}4Dog039^CTmdUWC8U%rtI1PXb01Oi^u!>*EV$dbOBu2i@kfj -&I(B$SVA%=;T*8#EY+5qFriW~+nNKLT?gao>*q9w#q1(bso%;MDZ*>uxHq+*-_l(j`&X<@zWvpR -$2RHZVb;s*)Js?OUCT>!bYDtn&($WEFRTTA8qx@!<<*@>AnW8&Js&V<3Ec{LswGs@XTg5>RjUUTLGfI -Eb9X+=mYbyB)w`Uc-)QefttzmfFS`_=DGJx9>%|_Xng0X@zgBfZLH`C=>iMCfI8dbt5L9LeHIBI2e`l -U_Mks|e}CugL4WrC{?6Njetds_f7c}5Q#q26!V?K$bfVo(33_1r8%_C#J3l@Y6NyZn&d+E35+8{qZOv -Uz2K|<#GA<8`8X0^aGuyMrAEBL($}&?&f}i(s&NlABthz0;tG9KEYuYny*tYj()(|=~?}dHSc&N9t98 -FPs)TInac!DR9h*Jxd0{UPPWiVW~)`KQBU?wIQP7kW$nbUzrC2^Kl+38-}ornblS;EOx!J)gj)sCuRr -pch(nhDFhVW-W5f3%YQ3Tsw*F<&Dl+BxL2te@AGVQG<9?j8G#eE(<$kb2tbIzLTJoyB5}Sw)W9Xm>i^ -9?B(;9a0F9JASx=??yryd4IhZ=icy8p#m^Nk=xvEh{zHPy$5Pl&p#sf6HWyg-a6`RnL_x5p4!SrKLWW+b2`|YPZ%1R$c<6UogViVsTOGiqEBIP=q6;*7}tICPtP(+lF4t60!06H67HgQ0OUno_5HCit2QoA{p?BtQ$kEbxmCpuZm)+lA_mRBT0~2owl`tqO_MnDht`Vs0i>9W -UZ3k2kLc_5$%k7d(>vu>8kUR;^<=2KQ+VFW$4A?*w%iZ~4SQ6=`G%4KW*kowRFL(PIIIG>XPLmiKJI* -R!L4bg_lkMd7}_|5F#+pL(`RHK)H;1RfvMIoa3f@R&oH3FiW04()v4H%AFn2zHH|ujhMJ+o9=Gq6ET8Rs6?EM<^mj1-*{}W|y5i@qPJcM%uYD_nEV-aCxul#1;R4C@g*${=l4iniX$ -P;-V5wOlsV@NzY%W8-5^u=GMnX&X@q0OkIPO2HIi`FYPMo+bl9Vr}C`)rRvC}W{+us$Pz+jG -=4wi^P%Zs-}RqeC!y>0a7Kl1l`3Tb1>lG#B1iF*y@eI(ncW{~K6jm%Ty>5qb%#zg7Nl%>)K>^0D59${ -JrX~o~E1^Gi^{6`+D{+@@Lz2%|mWQ?qCXIZRscQu^Pa3PZ|6|Yy;;6G>BcZ=D< -4u0Bl71c`Ub#@vebYQeNZ@Vt|W@{c43{RXTD?P;pVRi3=^-f}MTH{LtIZX#9iAL-ZQ)s|kYj4<#Tc1= -MAx1M-^TlLrR*+ZZ>7x|4HgY+NXB>~UGrp#rTdAzX_;5eVaP9bGaFn9PTk=Q6G)QUwH{Dy2Oo8y|Z?t;nLM7=y0I4Ef6kS-)L^a7EL@=PR#Ge{}F`$PhR#jnEc0c{u;%Ax_GeQE_ -{_=EU{$fwRc1E%6tf3WuEbp|9}Hre_d@FysAEsXh|at;f0q;y6~0+;Kddq0`MaE66%d#wWz@V0y>7o= -bplU%$UHJ{g~3=l`erTF<{`=DROBEz`x;Ta|ec4N=FG|@!JXhic!BZEP`NRGO*bZ`g%vylAeJgukw_1 -*|CD4{}RQ*lE^wFe}&@ai62|sIok)@ZqQFBiF)U -q^;r>=ziWv{NWUlq@WeAfF#2YRjq#_p%hyBy7&-Ls4*laC=${T9%y-%EW6;LS?kewg7t^}Siag6Tr?| -bqNhL-TIz?xKtVHW37^veeNFiD~gxV&#uMpqL9mEYW3HYB1qlLZhud|TPgk}*md6nt_-)NU1dkza -v!#azI*3|iR|1}1^K2xbV_i&mIX6OWSgggaaxYr6X^IZ8>9&+Inc?U4P&EES%U!Fip-A!)BK=` -95{JwiwV{IiJ1|(I%L*sNl&)LAL>IRf-|_3ZEP4E=T+CDx6Pco -D-<890I51?h2i<9o<@QD7m-tvQ$Kt{*UHPbtZ)#R-_Xg`AYn6kH<5Ya|;^UJN+lJ=ojzxSD3{B-LBGv -RBu=cC?=?s>nl3kOGvnkK(zuVF{;v(H7t}Ki)0VS>I4K-WO+!WxGtyk!EEkII~Chz^(h34ZFoN5vILb -LC*;xZ^50malgl$q@0E7BqfiL0g-|-xT48E%iT-{t&c|L0bgczKD$fZ(r;K#eZ_4pJLz?Lf{laF& -K`ZFhNr^i6SILF*t+ZFoobK0n=ZWg3_-o?%{ALK>;=y;1p+;5O{(tF>2~n1PU;_Y{8FhDD+Eg!^A5)@ -&$ePwP}C+$~t^g0>Nu9Vjv9SbV2oUC1zRn#eovQeuHf+Szx^Md)w|mSyTW{FPl%muL9L1SR(YxW=qLy -s|aKvI`HtoFM}J?%+e=)+q9oq7LGwTEM6oCkSWApul&C1Pi~*}Cx&oHGr~+gb0Cj|E70iZhXQQ ->5r~e)eWF}5obBksi~he5AR5tdHwtJ5o8NIMy$l6~YO_F~}1w-*a}j*GA*TIT>xR^p -GDlHWH;2Dal>{`s*<@+|(`cHycYM-BZu!~Sv9(7!Y6AGi63{$|9w7xMFg5D)BmFK=lo&Q@pfbPK2mQg -0{aK3E6l#1cCx&xB*aqW99NEHyvw-RDj@?SSk}`D_v-?0zhF(~iI@>nzCPZt#v!EhuQ*%?V~rP4i&xt -7$vg^)|gu!g;&eJ<$~Rd|f%Y$eM7u3>m~!Z>*3Au(jO@*$NuXLAVGR11J?aG3~bjAw}os7LSCMhk*cbI|`ku6?(s|dF&Zv>&gs`H7UE~n5XhKer@v)EfH -%E_{6XF`OkmZ=l`p1{vnPtC6e3?jJn@v7qPd!$MrVJF^yu1F#lmkg+KI#e(3z$b>}7{X>7Ic>4QE+*s -ZMgYb>fmPkWd=6uY}ez8WSZz9vq4NM{YVcl%PWP%aY7r$^`-lfjkbsu2>H_r_e^+~>7!#Xi4go|QuBS -kl!Yz|~^R1`C~c$`w1W=>oqc;&xS46rN@nEnQEc9+Q-!?jBuDor_g!(Jf6yHKe;pS{O2VavmvbxZ!<< -t1+6;l-@r>Jd{_p*^s!{SBIYO*LQ8(BLy82hEK)akRG4V7VR`Wi`;aM>ATbS$snH0nir5M5{Gg)R1_u -n)2$KX6-$ic?6#I;ZGPWBdWiF`N-tVrf>?Z -BW{Yd|l8p}^qy)V`N@5A%oxzHcL`SV47UN~lO08NHqXlD7%HZVrv41v%X%8(d9Cr;oPPJRVVidl>zu# -U@4NUtJEELeib0DBRn-jcchnS^|Dn0zTBOnm-S(kd_|avCCPsw>Fa3t+KvGX6%2c+GL4sR@he~stS{(;;Y(r_xMoBx2quxg -gl7AK=BL*+cwxw#8^@cioX2jfy8d>1`YJnA;PU@2XnyYd59kyBNT1L3(~taB=>3=Ayp*~Q`BRp$8d_>aNt~}K -qkPnqwWWoVoB-NLpa8-<0rpfvs*NemsyJQw0nifIxzVyu?0Tq&Zy125=+HX2K!7IWWHbJ_wuHt))H<+ -m@cl!>@+9lwI2(j#tss9KLp*dnnTU@xpeDJb)vQFi5gmqhxhiKiaD)sG~}?xp)7W(i<7N;b7)SdqKNe -7$>|u@<>6$pw*yCB`qKqT`t#~iYKKAn$dE_15j+V8=nvZwd4KP>4X_Mh=uT)t*1oUl-La6LgHTZl13U -J(9p8;HmyJ@xes=z<@9&+a_K0-yrif2vx9Sn`Dl(_;Xr*lieCD}Sn#u6iV%I_d`28b3Z`XsxZIR3VA) -`@pwXQV}z!;t{(T>k&ds1rupp|E#xQ}!++zz)jdUOaj&LEgPtglX|(y@EMkt2MmkK~a|!(I+kuMS<IXI?_9O^{_AvfMXlTl5+^14#vId(F>u+7)C0_CqV#U$#^T|l#l)xz-a_guA2RJn%1xuuG57xPdm_g!dSkj`gx5Tcj0L9ZC7Zr^az-ckI1Y6cKtIEEKI -QkCA=UO8?y7grFr`vm-5`_;1vZ=XD#ZXz;mMn{T~3OROZD8u?uf;`Y+!7KY*G4}2ohRzitSg6vR!@nD|BR1{+4UV%t*BnC|MMm+Bk`ly*nJZmh&ywzq -UFfc@fJ9q35bnzS3ApVL;(JA!*!q<5bi4eD;1IbD3;qd6=dHXrd^3(S>0yqQwo^^zxh~na_r3O;T%8$?{udh3q7nT@t-<#UeR(=)SAH05kP!*iNGR`mc-T9YoQ^fr;Z@nunI<)gmagq -NK6Pu^M{4l>VYvvbu{e7ou+v!W=)m>x14X~6yeFod>Z*25qf(Wo&{|9rte;^&KMH+TJx4?n23Cso`+7 -M-!&mZ40*w|4ppT@J>aMrC`iQOnreP6TltfudZV*UQqFrIUF5XJF;v;F{$lXY5nu*qM^sdqKoFokqbucT2I=i=-);^ClOM|df1bcz~Q;Wc1p3&zb}s@K(Sc9S|iqVzbgsaMsZ7vX_u)>9wJxVD1f -b{9cf5IX)rbPf*jr*?%5K(4XI_fy7n**10G3Q{yDm%2Q$28=K7`Os=$$P{56N@sKZK%DDIm_eu~-Q)S=gW1WOMT^0KFeU@*xygVTcheEETd3-=Xl>ihrWoYLx*DSw!8(A4x{_-ZsP{e8K -p!I3Fh^kMQifT^cRX8E@xjwZz?_M#|e%&S*@RyRZPSz1){X09_*U&`z%1~)uNYS_C+qHXr+v3fCu^sD -86c4fV3?FSgW`m>_*%Wy3LESKOx(g}o#4|!v6EZJWeY7Ul&h -yba4_m8kLw7|P^Z6-vbOUL`mV?q^n0vKaPy4|ADf@dhf+P;{VE9j4-fr@nLi!&lSd5%QxvhHB!M9aMX -f&&Y{f_z$0-~mSKhf|B=)H_-PT2nY;D6UUV+)}SOf0vZhP@^>qgoY=I*l&(~k}#Y^Qx0z -GX(YxprhPWkTB+&C34}a&J=#_kO4N-_Y;Uvn$e)dw0}&Z!1!UyO-ViI-$14vWiMJ**UBP&BR;)mK4>p8_O(1b#?Kp1!6e6Hyr -D-B}JR63Kn95cXB&o{Qe-EG)G9P0fR=Eyw+VhRH&{e3!wm7mbrQ<SCaGxZM+|03ZV7F3l9>LmG$dGo68^z&H -TAY>v#07X?;ZKH!h14db09MdEhz?36#@QJIa^Vx$~$2+!suK!`J6g*9s24+NVeTuF$2SkaXg2PpFAmr -NdjCxN6Up1BzN_lv%fl+k;kT@B?6|4aQaybx~nDNDj!wGq|^LlKr$-eP0?c?Gx*siYk_6W}l3d(>tZA -(5qSyiL6r*Fqgw=c;i%jLIig1np(2n^Yt+di{okX?TC4~lR~EY*25EH1_MJB#Z9G;EkF1wXS%P!FZNq!e(G+FAyiCc)H>_BcC<@SKe`>{>n_*z9=sCWXSVSvJkLO0~BS1}TrKqf;>EE~`?4qKJ -4jNVJG6#g06Ss>|-HM}7ww5$OijB2%~23-duwrq}uzk9?J9(JAszL=UsNb20xm^4L ->3j=MXl*lj&Dn>(qviN}5a1ugypIe+(0Jw8WpGYkinL3;aIO8PVlm5`s|u=~u$@VSvgDl4gAglJ`Vu-~d0~ss&Sf2ZU+U51EhO>*v -G%*7+;nvyLpVryX;iz8{=t){Qk+D9^#AA3|*sb+5$ldyy21!t(Ed4BV78Iqa%vkXgPj!vj8&0D-R&DM -PMYrgNH(toVc9c{E#DWGwcmS)GU{)s&B;8^6`bjWe!Q8jb=fx9b;pzz#qZ4U8nMAj8rSe9`sdJn%3ik -K}~jlId}O&HO9Am6`g;4_J{SUfI77Nnh5=$$b}x7{DWqJdv-3wv3}U9+`)^S&+g|>sik8#CjuqER|bo -RR&Y?^eNNnLk$J;a!K$*6%9O)wlukhi`nV0q)5&mqZ(;0a-=>^$A`sp!n+wX_#xl3Q}x_Tso0@2`h*l -N_r#swN&;x@aBWuJ43oDgbpoPr`qS*5l!NWuZr60Z9mL|i)$wk6W!N!!02-H?r>C -6?oSq8BdRF%dy%$@lUt*$khk~ITal92KJWU#lOlrkT*JpZ5D!D>B5Pk#NxcCBf*B>O--V%qpz7ABgpn -Z}ihaOh^`}+d+7fCutlYY^5o4z?eqII-BlX!0dq4B+D5r3)uK(Mx|2S*rIUp9*CDj-m#Sl_#mXAvuws0zb-AO3m*jvpz?fJ -l9L)~2BaOQx9zyiqpzQfFhwIu8|P8LTNKd}lJ<>Et5f_^%An*ZIO~SDjJ?2v1F7)HvOqKq1V5IN -{f@1Jg+69XJKi3TOlcoBY~v@=azsl8-62YkuLN(!C;n8khn&pW;dyw!0P%boE*Xs#O6o=;8cpvPpR56 -o6457xI%e00j4hx`ne{6h-%~orLGo@cc&E&L+Z{uIAWqX+IY<#B--9ddUMfCRH0q_B&N&>uN8feLRN8DS -X2qFBfHk02DmrW;?B6FmX*>`dyD|c3SAt*4L%q$(w8F^w;1hHqzC!GH_}@<6NJ{(~Jg-&-Xo`CU}$Tu -?V+AYA+aKl(Zw~lKFzm)Q6e7egV~W;VsTF>!1=@jga?<*YBeBy4k|>Fad$aGq>mM)QN-7jYr=<%P%{s -YV*mXx)p(M5j#_;;Nj{}y-MSCe?c$$K(ofez=z`T0L++V#UbHSy(P9i1NY-`jk?jNu8MCRUph2e -m55hIOY(D&D|53p9r=$KFvi*F-k3kzofFMF5AcjB?fl(BOBcIyZ?tZi(wDEc(*+};KB8xYw9fMoD6M^ -mJXmTs)CtEy?|%tg$)eLR+;MMfTRMbc@zD)4FV<_Zhh5x!2ebTCWiOCT3fY!FQdx5Z+q9*0 -3!Kw_JRLZ`04z-hGYlX0<51rPH_6I=F>!6txXrZRPZAOH+V*mw1|NH?f9~YfQPuZ^@ryw%3-Z#lNZ3$ -8~x6C*F)*Twv78%rc -KOODYZ4+SqGTQV{?*{lOsQc`0{t&lqA0qoTZkx}dluoX~UXgHF4= -+O1m)#S)hEj|K%}H8<+Y_wnobVAG%<=m~-Ksg30GxJC1l-L9<SLP -*XStgM<6C8&H(cv&&i@CDsAAdT){lKalsG0;`X#ND72LGaC`$>tG&B7EGkhS69Td3*+~Jkj>$*tOrP$ -)&tlM_y4>D8-yaW&E2?vAcWWr83~!?1mX^EE5<|_)B@?EACFMh8Nr2=qqh)lxj(T<|hGf1}hjgi*AjD -15@sav!*!QpQW@cg}O6r^PEzlmD+bq~mTXZs@(uL>9AO(YxvedV6oHiPKx+2yrXNfLYw=Pd2hdEti5F&dw!(~8p7rN}9mUiA+Qha*9aZG&t)C)<)$$af0@o -sybZDi=bj3XrfKLcY--8zjbBVbIZ?gkGjN6cjMK9IYdUDcs)+ziDH0!LhUDN$H!oKz0J7I@Vpgq&ECKxj4V6G$DJ$ve(zU0+<{0 -$m;sk<^t}`CJg585)Ne?W~I;>X~=ggP=&Nh30`K7JOYvAQTMta84-sh7pFM?y}@*}Jc?9&J3@Rg+I)p|~ -|aA5GM_Z9vK7+QbjX_(U3qtvGX)9J&L@zbz7SxvNM2jmui>IOC`#z{L5Iu`VI!4!-DzlVO~AAo``T5v -SsEQ}BQ`7NWmO=BiY4KLkb1|1JyO&LEm#W_49F6S#UI1rh>#1SK2l}LR94^g{-xjzKxo1tUkWi>d&5r~zF07p-R;-F?e8DL-C -8{_~tZ^KK!Z=RiFo?jPc0R;gRb&$_BeojI8f;<2)|apbNFcTKwx_!$@YYlS|4w>_BsPJwc#ot&bW3g_ -d&MPzx9E?8w-;;tg```Mh@xAP4?(wl9`w7kTLx|24$+=%TSLQe55_hT9Auwa-;I!?cq`+q7eK}Ri8UfksjX(Ui4bqv!+$L7Jhvjnz<%$$KeNV_&b>Z_*zCyNmAMGO&qwIw&pIFSH3I -vZ^tyhLS!uqPRla!(ZbLjX@MD#SYd*AF0{6qg@Rvr0X%mFm+USiBMqOJCL7S9b(e2Ql(_on4UJ@&S;KEBN^+>NuH?BM|>GK4|UG(bOr%KYt2!!h6%0NkrwPO@977C -}XDk!(NumPVrV*_OU(l5v?u}ioDQrNRf?CC4D3|=XX9-zt9QU_(xPH`yI1IKE`F`CQ_rcLjrLM@GtgxVYdL;smucLQ6w^pZAsd2l#Uy5E?gre%Gqg<{%?YGiBs4rEJ`C=B_w2P -3?uUqgw+W@;_9UkIpcHk$9OAg1_f5Ro5S{yw^1}Y&B89eIh(L4t#B?cnX*ezMfC;#nFA@;HWMBc0hMG -W#(vl}=tm|XPaX<;-u-a!kr4jX2juyxEXR4G8m>0Wd%17wMc|NHHPAL#UhK-xb;!oPXo&sg~Vpr6_th -^>1b-v-YR2*e-^LtzAlAPOZR6vHWk#Hdf(09VY)c5RdmZP1PSukeUtTfqmVHhA2iCj57l?G>fZ`{|Kz -gE(T(UaZiy$&Z4Yz43-s_=ZO4#}wEu(H5gy-$S<7qoCjAGf;RNHrq;1=(b->Zuqv%n@3wi#tNyM_t7q -(kAYjw3#N7trf7Q$P`ssQHXy|}#nMem7257B*|B(YcKUOd!)pga`x_WCkYD!3%67X(r=^X*T-qPQ>qaVf2fIrNRy> -y%K@8ax+W-w!x-mQzm8P=x4I##pKl?qUr=l+%Usnuu-A}o;itPsoYSbPx*=v^<4Mo1amAz6OFwZ1B(A -bnM27Qda+BdIY2j)L?EwaAPHi&S*+X^y3L5w$0J#-L+g-mYN%Y*G!=VB3%eE30+oc2?EkSc{k&#tKl~ -WI^g)i6wii`CycZfpSvkuc~srQ^S?1hqEvZvFy`O&M>T(2I$I`!~XH=8q2xKfYe^G`sfX$tHy1RQ@!t -p9Cuj`>xCKGcO_1w3B2<|Qdwi!yK!)|9LhW@th7gNIs`BZ%jI@DmsbX*706JfkU0R+k(Mq8R;L#zMNc%h;|Y&ipENw%K7%z$v6}9q -WlQ{Il9w{N;)JVgvml%MR(%`XD`hL>aa1cI{DIqbifhtJq!tyIBs=zi<6xDlsqUEVRF=?Efgjhz#hJbp0z4kr>e={l6UJRRnJ(AMkXQHowZ^W8+tmOhurh*|Nt$eEx8f8L -r6qDqx?F~9)CZ!qESVD>;v38LhG>y^4;8UMQrH=N)jg%PfQ*4H2NnI1PY&aQ)l1Aco-Py@UM)BPUuOn -gTKbhjXk>AZ%lHtKBZjcP{=x$(p(7tCbsj%CEWbQtEEnZy(2}bi&4cR-H{miIbTg}=tLN)TJqh~Q-@H -W#<5_Y;d7{WU1~O9RT4ugGMM&dFj|La7q?zHh#>;NNS@KgkVPs^Pr%XA^^D&H0Q)U@bA|r+6k+Ju;0g -*K{gERhvAnDf~erLz~KRUrTQ{Ml0@XzySD1xmpiQp&+qtptO6uQz$5(CNg2Z^94j$*{81pqAGM!k~F! -EZ~ZZxY$@mc^$&ym;eX!35eEAqoDQ_`EF;$G6_S6;r9*O&8r?MEJ(?5^QT<-xSpLZop^waUQ&pyFA#GaR=|YCK}%?`Y=kRdx=lMM -vG%s%LoWTu-i!TlCuwZ-x8rQR9kV~(8U&1&6~!*k$#qVb|CxcdlMk}BOrZJ~{b~ -KlXYW+aQLh?NOc88EUlU8%dcdv>OG>ec@74v -i?tuKkTo4{QpNz_!Zax=E;5ncnDufF9oek7^b%2^7a2K<`Wo(BP32jFiH~ZhELPO$W}97L3pLWo0@mH -q2^|IPwgff$(G0b8i}{EKaOq^Yn!SmwyTV%zf)7 -gHmKeliNWo>D~%<>{XXNZU33#RB{l;P2;A-}+^6N%kb&5z;wfU|_~3rA&Gchu(_6o2vbzLt;l>{sF}8 -JOj`%l#cgb;yNF^4Yfg}`JRdYgzex4pKKV!r{i8nf}3nnY&4Bw0s!85e0i`}%`VKGguwI{F*fU94p$= -`_)TrFfp!n-W2Ymwyc>rVXsi+lYyc{1R;w3@$3P0c?^vaFPOXUvrb8O{Y;8FTsT9RvULmVdot;Gf>|u -lnJCpwwLbl5krS5fPTrJ4>tq)RVq74p&ItHeB4hL=4`B|kS*vgEg8o=;KR2&Pzu3X-_R -)w;!ePCnQ{;jUI@>WZA>xAPe=*hy6K29IAEe&JMf>d#IX%GFYf^I|xzv_w`}-O)-smx9prgo!vA{OX? -cxpClo1GJ*v+q8F3U*z(5wpY))p~2ifYgqIw=IX?E`uGxiir|Z56&v^A#@{b+!(0{{8r7QsF!`My`73 -0)(6hX_#Mv3!YK=XyM76R=wxILF*UD3vYq$7lE`0`}TtlJ+&DfHH&?qqY*mHa*a4SeTlWN5<-`&EN$y -4No!syY%*(Sn;J*%M4c6VNrpPciZQpX#mHh*TrEI+-oH39%}AMVoO^k8r5U7)ov(LR%I0hr%uw+QkV> -TIbRUr!d!2pw;T<}W(oz~&PZ!_*if`uGKlzkMGr>-J<1>agW!xjy%&@4^Si{wV)7?p;rT2-_Qvs_Du+>L19w8czR9c1-93ky?DZQxqmb4s3abp#5 -Cj`I~bQSL;)a9XvzrxwImDGD#IoWsugPw1%%k)%)qw3Me`^k8}_#j#k>G1-lQ!0~3N>Xo=u@7tr(%#q_vOsPrrLcCsAIN9z^Y6(G* -?j%P-v5+J^DO({)9HVHl3ytGe>lO9sEa}vLBiyUumni101U5SyCN9{Ll{XBAVDD{NPRjALF{&~S+)z7 -CL24(kWDcKCpT%+l@t@?)-wmC8*7Gt(^j`1jiy^$-R6ar?8@T%@ZNg9l^$c@E**+*nP@WIp?pW^-_~S -T2E6so!5h}D&<&y65Z{WXD{l^yZ89C+HByn?XExi9r$nMz{?#gWIeXKaY-p2~zNwnS5%J;X!i9d -lZTW+j6|E`55f$3{#WjYp^i$jZP3md_yqEAeoub^8Q=PgOLuuSo*b@o4H)E|9${{u$7Rl&YZ>&f34wP -1qU-_$Tzk{N>LjGfaND=cY-^x -n9yMj9bX~O5&Ck{40^9wgm_e`|kHQ=-kLEGhz%a9j>U^)n^7|7-NlUJ5Xt+B~MZZKOpJp^ty09Kf>kg -SHb`)K%7}8iY?5?8J8syJw`0~V%;X{-BH!BTRAPTa`3v5AXJBWw$)Aqe-RRc;xJx=la{;%$Y5TwtkEn -v!ER3aHyXWwA3D8$epDyD+hqIbil>Q5kJ90dq;&s&pjCDxTv4Fw(_!FRATuj*UFj+DR4gw$I{EZFS^7 -3xpy{j`;z8h~fv)HarD&ub#!K2WqDAOcW*L!lPhNn?rS#=UB(gz6Z53JbBx2ZONysCas9fi{RBIcF|M -v;gz8L1WaGOvr%J$to%JJ8=_w!x8t*l>u{S#{;w}d1JuWSVdDFlZh;?vROa8Ki{6a|a+{*v8o585Rg6 -LJ%egz-(s8QUGMv(GKEH!ibM7-Un9M6umD3Eh&0P`Gh~m6Lq*3u7O%Xy`^X)|2gq+`sb-M79NR>zRqR -QzyH;B}#5eUF$lzE}`3?8oaqP#l$wNws9(STU1v{ltsJZ7POTOHz$aF0BwC=8Md`Yha2zuBSjguqFv8 -^9~?UqF*@<*_oX_1IylCezYdNC_WE3J)%ZQP#?cKQip?YSC@{QgGw*zaZtFv?v0u|@Fjj2MDFC+>;yx -@BAkEPx{bL0A*KL#jOLL0V$TPn#3c#1{6pg)DUFbnrgjjvN@Ya{2%o;sT@|Wf17PbG#@{g0lv!pJDc@GW5plAnrY#snx}vQq6USojvt#jJ{6{UZIyt1HxgloW-OdU>!IqLG)07}LlfvBgqjI -&Zq=Es68(4diBgHWWGlXX#Mfo%iNxAoY=+i;H0_qVss2a`4o;{0(l}alXH0%PT9R$KJAgnWG6R -W8dY2&n855#pV~nS5;w-t~Z4E4WDInYTwk+d?O7CzKR*WUX*Cb(Xs+z6Mzu#EvlNTio>z0$P4e+Id_X -M4+p-ma)u}t;f^g$6CydBSoe9VP~V^9=Dc9M%tSDtjCpg<-P6NJD)nq=LZct9=g|`Lim}>LCf}=h>Yy -GuceqDK**9_!O;aA#6YzIhT*W7HFrC(XuvYNOrT69G3RKf!2*Xc6K!0g*6~M_u>Hn~^SQsZ0{_!#1o? -_sC`WWYVO&=di1<>YEvCrfuLLa=StzSUaV?~=5#RI=RTsyC!ndL8HXy3DZg{h_WO@htgSah(M;>R=19 -ZwCU4=(ly4Aj2xxqXGcjU6FLrVxiKqy|! -rXz+3P7iE(KLA&{dZ&&Y@JT5jNTGuikAJV3LZ*oeMEm+4l$T)pf9=4{ -2%>v&ex>fBVyIBGZZv-Nax5)mtUV#+8 -TN7rxYaz0=TdqGp27tC|9JSTewn6HZI_wSxNw{Ge6>n)bJl#!=pzV_DTiDzKk*(im1$KnmYIxzF8#cR -*9CYnhxKr0Ntc9hE-NyS5QmO5{ci5tXj9eIKtD`pT}V;@ytNVG1S!CbyQeOcKB-E&~u+h=w -e?#VcZ@nl?>YE_#AnO0&`;wfY3X^)@UOZHw&s!Fzh(y -n>Er@??%p{{$zQr7zE5a-CJ$as=T@275o`@w=98ex11cGF1?+zW}tDC -Sn-m+2uimFN&`za4{K>;5P`0nf3OZ?bmu3+T>o(g%Ir5Jpi7pSUPwRepkr%*OD%>BDYKWy>5<;4{K%S -oFw?%GWy>i_=Niu$`BPkKJdUsR(0j=j%+f3mM|_{V*JL`ImTFdQVw6%+{sgGdU;Fc1N;b+yCR4KRU&D -Di14C$!V96^&LX+XSL^)x-@;lFhbAjw=-<)jIkXk0R@efQt!I)*w=CKEb7h$;zg#hQWr!=z -VtWxQ-ldk;)30zGZ<1KQbL0beyB~D#D@8VXxY50Rau(Z7hTy%?WyQ$N10mcRYLfIz`wX&Y*j5g>LOZr -+;nJPquKR$yj@I88O&vE@h<&%z72=W#E=hbR$}24pHS;);x6fNS(_af$7kA;SHD>{8!UYtyNte -KU?uzahcw0Vx7p__tuo(mb7M)KO-F*=s)@08$ZLt0(AN8k36JYN&;rRFFkMdEZN_JV~U$7ZTRN_rw=rl?{bJOMv|jBKD$>a)1qQ{lZZm(^up&$xk!Gj>#Zb -BBC-X937ZSQphe -YtS7;~$3OCyhOFd1=o}3?TYlEyP?De{)-BY;ic10M4OhMfV@5Am_H<=f?qW*_?|@*@5RI+D*W8yOz+r -ArlvdJsF5o0vp>L%PmM}HlcibnX@s?>^CQE~ -AcLn7+t7h(12+ehh45n1HoR!Juxm_sN&7p&%nZtV$D$%roO@rwguOSCQZQ{o7R8}-nYKHPa4!K#dxee -haQ2HhNT;q_th=4hXfeOv$JdK0LN&RG!~=DF#0U=8dh&5ZhNJQpFp#d4J)oD*(;Dkww{+3UyM!u!6MJ -|6RWZ~fwe8!bt+;}_mO%$qTkoOsaR|tN1PLIJZF9sAPV8d -_oKorY(S^~*n^9ZB6t|e8=pY`iAx>rrl!b)zkGz?Vukm&V -%0%@WNRKcmc#7D(@Y<@&h|X+}fOQBtdJW(vqu -07}a|`OJ>X45Yj{t-& -!YF9#^{&g+}$9pQN~4;)bF=0TRPh+TpA+Zi0#ywUMaLiynDTPtt+*|sd|TS)fpwPhI2zgYU`aPAL#ev -Q?B{ngJ}+b{&eFae_!gnimO1ntfeYsiz}TS5Q{w?WK|+8hxO#+O6!-2J4wga;r+tXOp@*yZ3 -36KZI*DP403tDLqkOy?crHRPY=-vl}Xv$#Y*+Wvq5A~vpv$;=#ccHU%3=14S%$r!r;XpZG9=GM1=*PIw -1&+I2u+DbrlhLE^2_4m{O)B~E6pHBuD4lGgPrYgAj-Lbqxw(v8=BJ>K(-n**6eqPSiNor4 -$arC-|GLMN__x}w*siul{`q-1iM -j9dA8n;R*%r47$gM-=_RGA>JilwW!kpsgtIBePf`JKu|PqVf2XkzXg8CzMf7|FUl(d7oEi;kS4pb4;a -oR9;6X0& -15Ct>OmUspqFOtfT_3)C<#<3Z!LZ}yu@%JDE2JXN!EOko5zcBDh>^wWH=sXoK}{TE`uo-1x9K=;=?Ph -^$94Cw>89?oZ*JsEtEL*v7e@%H?*vc0to4wsz|SdzBpa(1{9tq1yjl@=7xdG(Ojbg)Dxe)8dEI6@~_~ -D`zJlFY8Ti4qVM7D{jdM)!(Vrk@cdu@68`rSgfQq4-v0NTh2#I8)8z45e?qq}9jg0*r|hv)f9I3k>M~ -5$jYz$o_$Q9PV#lkD)A4`(wL$j(=ezl#yY_!@CqLpM31TP;ZwY(|hdy;}im`nrlh`|akS#5q>~MmhTS -a(9&6UfqNQuN-vVtT(w>rePBmt7`x(a(TV?8B^Zq2SZwHL2m4X`rC-9!WNVoLI3X>GNLGh;4nryfpliS4UiiqnyZEZs629W4JcS`-)p;*{5_^)qf -6QX_LM{H#Tzb+)~wBn*H-hTB7lwIe0d6?PC!#B6~uS^po=8yVS8pmTz!p|=s9&;NGz?J7CfW}?E68)z -u3eSn?mkEc!WHgHC*4#r+s3$_d%WU!2G=;Dm4Q?CU<+N3|IqhqadfZxL9g(WSH^XeU4yjFdCOj80qJ0 -rGt#fPiEFk&IRloHB&Dw4Sb(suKoxTsI&K>W1D|;+oY -pm)IpT2ojwzrv|zW6F{HubMaG<%tkg!=l|tNy9N0sL6vm{}{02~3SFhwCz@YB1)x6_TZDc?MKdqs(y1 -$ZmRf=StS3isJ4L&ixyXBO=ztNVcYHa$f* -CpsOQ~PQ#Tb6!?52Jd^CyO3~o+LFl>TisKZ7SM;P<97{y57lx_(plb1erTTj8u -orM;xk;B^yG}az74!#)@`TB7u9iP~s%g&*FXUhJnoH))E0(O{Q#FQd@s|7b3=zgBhY@>=jaY_1*_Uv^ -?P;wgMHjw;_|k9&h13Ka&yaQ?4|_0HZwahYOR?oFl1y1KmTEMH#Bw>p8gQWw9EAD5#);}T!H3i1NEDMd1QU5N*?eMnnF-SX -Ye}XQtOAH$A|{@o(>=o(lQk~t@d0Hl+1apM=f12`o-|7$j`~h@&$GVloJZ(Cj7tzZ1=hkWaA3(MR^>t -&h4;leXsTxLwWfy^aX(Y1r0LVA4`!n<)&V5g`VgURrODg`1!#7%78Z!soAq4S^p%V`UyYcbOaRORl`KRCfg7ts@sIQp(nvvcf>xfy1v+~55ujv -4I}@KKlgRoz&YGh$zgC~f-8ouFt`FPy4y%>nnT$pwy>f%o^IV@7`gpiNg_u6PTh#w2qdv9nXj;mL0g? --<+AZ^LvCefYdU{;L -wC&5tu6RP^PW6y&9NMQw7AU~J_<%xi<%pN#|m^7|b4ZWck$Ukc{S3cnjlY<*#~{!uXhx((DF`E9t}x4 -Qv;yq*8a-2gw{&QEvqHw);Sx)FUplj!5wv~+!|y1P@<=Z3D38;0H??3kY@qO@(vR$;hv-(f~v&(rOK1 -R_NDiwRWjy>MgQEbeu$tdIbQ+#H%(w_ZSyOUUb&$E9)o(zD2Q`6u(U2bcOCtvXeBJ|g}GoR`L7)8QUI -3ve8j<|^aIOw-XqgHz$PKx$3qr@YLP<5`eKJG``hY70cg)8Sd}s{#moG`iY)YY=6lHIiHmLoo!{G3gz -)Z{LWSaqgS;QS4!A=u0CU5i%>y<`j~53ZbTe+sy?ZkDR?$gPD#DtKZZytdW=?O&Xas--XCF)hds@HOT -9jz8*-3_2!coAkf6yV*w1<#k#^E*+H|OL$CKZx2Iel)#B^0=i8qY?yvCj1zkAcGkBRl^Y#4Fc*WI`dp -;3mK{aXak1Wvgyj<~b5`Mg%%KT(8xEBZ}R2|gINkW-k?%+Mg{u!m1Si;2ZRN~$^*9%VA*`Ue@K&;5o5 -dQPg#y98`D94q?2G=k(%VR9J@6b6aq2RQ0oW%D_T0A6W4chEG*!cCQDv -5V27IrP9zeIR!22hiS0Bnfy8!XFma{apE&fW_hqr%yPSEy_s$YjH|BKW7tReK%DSiT#&k$uLQrOOIQg -|ad>$3a*Quk&{j%r)7=sRDrAI~vv6@9nO&I5=>AQ~Y$NpJKB0g?dGef?z;nYl7^MefX9RmZN~hqWR?f -dDBL(}#~a+?_?YEFm1sI;AAQl9kJ?KLX=C`g4n*2rwgVOOnr6&{bS%;L6;%c++$wn -klo^meieiSTA&$dldgP>OF^r3MImg{iU6?LatPPkW4R6R7=VH+h^-K^l1OUvN?w74%m7jXgS^eIFAM= -jV)Q?;-y=!{$XoTz@j&1|-$TCqw1s@|M5%?oCgSZ1CpyTeuK5g~t&6CMr`1H}-Al8V*AVz>3vN7 -6YT}wMV7Jo`nKnl}k4pq_HG& -;y-{o291?0W7wa+?o^{x%@|lZVjP9nW7m*+KkaS6ITe)?Z1mpOsh`9tjiOZw!daY=fjLU1eAPxxy?@>MPVpE>FKx&y% -Eo|sadGP?_t=jrkM+S70!(^NKv_d{@tH3!iniyZ^^|yBz{E)xoAy(BX3{%>SOlr_VcBFX`H&#oEk>rC -kO1sjCz+{5D4)~k$7<}zYpHXaP3ukU%0qLt2MdB0VoHN+UDCl{7&K0?`_V8>QMR7{1yBG0D%g8Wa?tL -+5ExA5&Msq_=YM+XCl*Xz&)enLdl|pSowv%YK#iZ2Dl&oYf)IOwnME!G!GH!AE=+gKf>3=xO3J;HOtO -G|;ZiSg|2Y?BvpMdG#e)3m1`qxkV0hve?BN+^57#ia!h9$|*?Ri!lA-PTNPJqc^k_4QBC4iC63O)2@+ -7D)PNziYMeqrv)fqo>=@TUMOz{#?0{21M!g#fx3W)rWhEQ!NFz#fVLIAMRK-X~&UdIz$Q(tm~&*inmE -phyIQ{A?pTn^G~%!1i0pg5KlG*RsuwXXRkwHlCbP;KJC4#}Bdj@kJYw{=U<=1M2;d3n&kdVPQL@}Ph9`u^nQLC -L?9g=;lriI`=ZycGP9<@PWfp?gBt+Hku*19NPzEAo{lp9Odjy(H^oF@;3oo?RWn;T*`xZhnXb?)JrvD -A(^7q>rIEYC`77JhBIJ*C%{_?Snn8rv>E>$J%=;sA^oZcvncp>_=XU;znAzuJJD1icij@2W2Y17CYkJ -4pQ==3Wa~Tc5Z>>jZN<9xTXy_gGbqW==_0yP7T)dlkbhzIUN%fp&AA<$;W+?8^aTXvV6UsO1qPkM?{^ -O_v9(`9kp8nJInBTwiW$-H@a!rJP$RtpOUtZcF@XXD#6}g8Gl72$E1*Bt7@v{vyA`p?7f|E(NlWuDdl -lw^5RW(T81Y1WwFsZN5(e>!L9erb2b+R_?Kw+sdNV23b!w#&aN)9QD)zd#xZ_l&dw^7=2ais4h=;<+c -4NMBt_(d{JUeJPY#Yg_@X4qD|gIK!ZWI`gS~$8;xB(#dR<_D^N|35=O5dXqK4X3s`qE3BSv^eMh+R#M -|Cx%Sx3Uj!&H#*f(21wZ3?mj4Irl+A*R4qJ*FGpbWR+f$RwLb$mbG_MXhz>Qtm@_uh%wSw$?P -VaTLQl{=Iq@0_e`uZte>A+#@CaiAZC?U9Rw&60@pFfS)nChX;AhIQo5IR5Rdx?|RmS;sIG)X&msS>I{ -D*)@`;rpn?7`xhQ7ogp19bazQEsW6zC^~mM}BFhErajm)E`=rB#G!sMkvBni#8+Qwe&z?RTeL`Q-$iW -w4t?wGG=9Vas+;gZrx%F{VT7MWv|y@eoG+}A&Qx!Re7`fbL -YGdoy&rI=W{UeW@Zy@SiB+^N&%rMwP`u=O -_q;sn{Gt9jkJ8e-3@dFuSSuht#^q7wi<1= -us8!Y9KrZ82JVpE6t(N=)+;4jcM;!AW6{lD9XR1_9=FU^6EK@Yd%VehfU@=Xb1-L+6MIkprr1&a!rdM -IwzPxZ|LBSzR;|#NDUpT4#SclR%P8QBB~M1J>6FWR?(?tF^~o0_*y%fl_W^UdBRK1mT_79$Dmps+@Dj?8Uw$F9KmG1U$bx>vIxv$)Vmwg -PQcP-ut8K3G}&WremTwUl>Z2JWc{xBq~BBMjV4@kdz_x6t2aBYP*j=2%1H~U!?`bAfx+vdP~hZdgsW# -Fi*2c5i{sd8Sk!+HH+hRT7Dm$@G5M6YRnhrgeE>vGwC63G`5U+qZsKj2*Jft#6aZ`*~ehS@Y~fOrhT}_M#4 -=?Mtpo5f-;-ZN)2(I*mKXm{{7H&rWd{Eo+{Y)Tj!5a%iUS{fL7lCigTqLLc~Ew@_TOuP3IxKU2|2>ZO -mLdngdO?2qAzfA^2}mW0ValJu`$V+?wY@~y4IXZy&~Zv7EBldJMScp9e(5~bOnQ0l*Pp08l_Up~k8a0 -XN~I78wbgM-izf@MF|)G!3-EHWQSx-r;w0;Z}ombDRKPHt8wwX+<|^Vz%hE@KkHiapd|XwH2WQpDKc1hk@;KPn+J~!GrVU<{S%w6=S5MhfPV`RamN3+bwJXdef&#T=R+lJkTZG#GI8&+Tw4^{oP=%HlTP{qhSXW(y9^!kK@`a)FsieBuwm0bKa&&8A+v -?nQ@p3lHfhu#r(l-eDseq}Vd<>-iaq7?mXG!lv1wgJUq$9ZS+C~#aU_{(#kPQ&QbyHR&TFA{~L{_aim -8P9{Pn>l%ZF^E{~GwvmeXqRRRq;qN?Trhffr-RT}cds(MFvo?KRl_hRM7x&>f{{{B#STS^B#P7-daR= -pik$cbqi&GNbE8m_xgxg~N2rk&%x)q$?t`!<@%7qEJM(&;d0FwYK3u_6>YfQ=UfS-dvZ{d;(3xiQ;U( -Z15mk)vQZB9mQ^XrR()_V}9jLH|sbXH4P%x{{O60=Y(7CG-C{umx-gpGnY;AM4WDwJx6Bm+H+51qA%Z -VSz$WN{os-kR*!gm&-heL13&H1iA;*};vPCRS78M-3&MSdDIB4ro!d<%>dOdrN6J74SmLRO)+6L8!_x -ED&->g$!9-S7^=q9-^GV^@^9|QvD`V4RqF`b7E@m!V-kPxM^%ahg84pj5ydUm* -_8#{Uw?|y)pt4io5AJLq>R#3l(KBOg)>E|_K5+WLHS_HyCcey02E9*8K2arZ=PR3mUG-Is8hFlnjRGVYe=4#;?U)s@67$V6o<-42zV0piNgAORbFPrnk%Ep&LryKqaeL<{mwyQO|dm+^@wo -{9`N;kI4>vC&C~ogAQ~PF@O1H~9W@nJfFa21eqyk@(GIwFac&x_W00zgJ89xi>z|MRz8~yEuSw+fQqz -m9N(1`BySuUwcKY%LznDuZbsgt;qa^wm9$k{fpyM1YRx*``r_ic0B6)>oRrC02HT+E6%lUyv(bylvD! -kTDimXKVt{aqG?W$J9ecOSE5E0#+%(RL$3EeUoGZ12van?T$nZ+lI^CoSr)=vnh>YBG!qm?2}_rs9$? -h)NncgRWInV|Kq^A`S45KXVk)uVG*_FAH^hTmTphs4X?fbaaxkh{}!;AOG9?0EFn7SFc~5y_CyFAdN4 -CdXYzX_5kyV(#&cVqO?=KE>@mO+2@;8{+2UJx6FYd%PGA`muA%*f*eSERT7VlZa$Y-Ea|2DV^J6cN4l -UB~HnHFLU)fF~UdxICY`Js%&!~9vSk!Iyfw&Ipp-GjAF1yil1ls#5mUL-qT6ZYKQq8NT+@4-!H5qSL^ -#?nab{AANR}g;EIQxWgxeBfoyb_%gfTz<$H`@Vfpr+`nkBOoPJYmG|>T>HJ&BdS --uhImZ^DQa5h2)U$pZCh*Jf-gqEKHFcC4@^IAfKh$!srJ}l|%`^Q^puD#Jk9w8Nqx28wKlz{c9{$B)U -kk>+4*4N62V*FjKxvBPP=e)f5~DbZB0zE+$6+M8)`g#Lhp(kygl;mKO{1KIV2=RA(2$@uBsfq{;Tfp2 -Xy%{H=OrQV!(u@nAB4AU1JEI}>2j!TY-@|J!DFy7z{Q~30hHSr*wn_jUzCLFiPmZ^j)7#G+e*LnIVcW -#EeQvnyq2hIod{Yl(KZylbz<=C8?d`Ufga0R-g4WfII(#o0v!_xL_pYYOG23i?P>0t8;Q;9J0e&=H)j -SZW+c$Dsj0KSZIO2}FY@HiGHcmeB<~Au0GYM!_)pob1X-1hmFE7?#xQpGiiWNMHC~-9SL;h3BL)-13+ -VS-;+JHc<;!DYKAVe*1tm}ZyvOlxU-|2WA#i0-+Wlc=r=di95GT5OlF>u1{TI~5YIJh7sY)7Hk_V%O( -<-%<<1IcO*vg+cB83ymUE-JBoy=Q$e|Dbr+BcfUA9ht}K~s{~m9@V*mee2n$06?VvOb-|JUr~VegD>) -R0|G7K#%impPx%q6H(d}0&U+^siF6fUSCcZeDdIBPU3FI>I1UtIn+$v28|&p8tU|oOD!SlWF^9IlavU ->mjg{HjcM1)XXqVThuS!H*5b;QCqJ|1ygTkh`4T-(Cz{XinpNDTVR4&vZStKEwY}Poc9?rYm!Y#oXx{ -nkcoriIu{GVd8|N?%WbAauuGH`M$+g_LWK2VVQ(iksn0YnCLiAsIwViu~KEjaJdOk~MY#}sH=AfwkS` -QcMak(WjD!dwF$3;8hmdf5PI>=0^rH>@gg@>@OXIP?98)|j$&bhbKBf0AMpfG4p?fXn&^Xz&JS0M1<+ -LYt|Fw@m=vb@NSk)n8I2UK}wgEX6vP@3W6ajx!SkYB1Xa0jt`3=dIh?JkbYi@pNFj_X1Z9#ZwhD#>^e -JEIxuY~kM?t3$=DgweKnk=&~q(c~$;#b^bI>kY=3*o=4B8{aFtyc2dOBY{uqt!NdCYCVx{=_FAF$d+G -b8o~O44#!s!303O!uKIvwYVUP{UghtA7Y??VJ#YkX;G0M#N6|D)n9EMoS%aayQ4k3B4n-z?3tm*QVID -A2dYAvI?C`a+Th{t&{JQgyhbyxQ13|>AH*~EzK0H6Gs1d5op*VDeySo$0Qr;Vq_w|np{^^ekGz&$?yW -~K!HyMjTtc-5deQo6dTB%pp=LbcU`V%gLfQ)B)6Fg&Od8Q*ME62*cC7{Gfe>?j;2<4h_$bKNgNbzy$M0g*X~tZTi99 -e5Tj-M@m;Fs>S?78RgGrW;0C$z-c}F409?7Ukhxn%jGR+%ujkEID%Nve)hYmh&j2Xeb=(*K}*%p;R -@7%HPvc3M{{e+0*zYf>CQTbs8_4^^OK=XxUfcBD{St=pdV}&N4nA&3vQsIe3eg8?84-e(-O5%F|~WW8 -V9cztwR^ps`& -1;>PwSIXb$Z<~Td#1m3F67TcTG&S_bThqaikA?%vT?p?%>LCBsY0d8k`4t$6$YKfJarvQ?|t7rCOkdK ->0P-)dSOK-Pt_?}w^uE*Pn}6MXU)FQ;mQ(1&QXYqbumX;JdZ-6yawwS&Ak4~j`$AmvgTuDd2EGLV1AJ^xJQ`{!A&uJ -j2H)W5k4;1FjZIkQ6MsG6?_lstwrwnuM{ey?0CNx-gudhq1lViq6YAoXmPx|JV&9hfXS6G0{cw($CwBUq9>TmzYYxGlfZC0!d(13$ -bMi#UvBNFVgC0nZST1;|V@D#BPeWENV6zLQ+DuLM@{>7*}*%By1P?&?Vs}@WWkj6N7(W$};!R4)U%qaLL6VmYCq=$rfd@XI_+KX$U1 -`+EuW=2ChBGJ$CqPmoRL0+~c!mk5-EBsf+k^0 -D7t}=f<(4F-VG@Cg!;|z(J!C!B>S_V~bKE{)xXKq(ac`ueU|5z-P!xVvA8SA@GGNWK&hHpLEan@qtq} -hG}DR)OGck|PxE+s*abbftUhuDi>X_f5TFl|cyV8zUIQ$@zX2*Y^K^5t$+Ca(qBsGOi)?8igNy}R@l- -*}m8IG4gPv%hFKqFc%3#T40TSgc9t#3SFdjw{EZ7cQjqu^k8f+gkRYDiTN*UtwX#_I1AD3=(pJC%;ug -@$ei;f4v>IyW$u|VOf2Kf*=+znG9RwWl9{pP*v)9y`(5dhw)&RPn{E^;LRKpby|9{*x|X)z`TaNk9gH -3=^NtuER|TGC8rOkUOB^U{60)dd)N6UKQ@mBYo8_sx0K0s9qFALm$+&YN8R0Xa30+j%2lhabG7eJB8q -F`Uf7#Fb5^Uzq0ShMZbRX4_1W+!l@INyV-jLte3MloN<5T|TRtFMaTk40ddP~|4ND*bo}cmgU|TzkE) -&^fgQh$$$3t3T%G~2nA6LYq1?g*9;~og2OEb%*OQ}Ymp3wP7mUyZNt1;4$=k+zETyF>6- -Oxmdz@PXVUAa~@H!l?;-to!R&H;s$tQ9bh8uLK7FbEo|#IQY|3{uWL_ -xFH8-C}VolBcxy-j^;Lpl^9H!&@_;Tp%hReaG%Q%qRrw2rNIaXm_>V37;Nap5kS;z)5(DmQ4F+RxJ{?N!Z(fq!P|PzF#&XPz!WwH7fVubH#ECVc4D9|0Lf6;Hdq ->M+kn5dw-X5fr(N({I4%FD;8b(@@*AA)WfQ%ewK@#^?z#h}a^@qdxh}$!=k;u?oU3|YW)dj3$FFlI2m -i0Uyd^Lm$6q}agnuY^K{8X|>j*4Oiw0K!#Ag -a2AiR6jiFXSjuahqu3jTj&FCiGH+?EcZZIzLkf7;#B;KQRM4D@VFc(hPm(i>~M6w_su&JkD!i-$0I~e -Mh-RlNlO;>=?^%?s7s;_&zaaIMFlf>@Nm1RsHvN?#DzTg$+2(GY@j_)f^+k(KBpHLw70GZB|C2iDg3y -KcS@F@W4~$lw|t+hQ0?ec*B_mGcQo%NQ9GkGDdz?$BHuJFFT{lQV_&|PR{|pw#cM^87;K&2g_32oIV| -obuqPFpvtc4{{T9pD^#+hLV`f)ZB%e%TLP_;VlZr$~>lXOW`p_W)@-%Rc5Y;JL`8FxDCs=vE9Dh00SN -HUIn`JOoc<*0tq}QX+{u+5&Z>xgb*fmdDRL`&@JZAT!wEGk580dF+EAL0cDBuQn1!US|j|YBuJYu>Pu -&x1c3w@5a{3*yDl{=b4rQMmjfOm!4cu5|3=eP8@*_1AHvuBQ3yPt#^U#l -ME06lfYu15bbSdYtrKiGqRB6p5Fp`6U^lY7t|s^XaN2Y#os6EWz!;3V&sp3k#{%S0W6YU6n+8}!9Ig= ->tvl-Zn8E!<<*+s7-N7}*h&CHkVp(Pbt{amL -=wOz2TWX6dYEpGDeI3R{&`pv4yYuX0YVIf5DA!QNzu(qWwUspL!kWz!{(p(Y*1}U?k$7bzKOpgUa>IWgm7#yddcUFaHUA|lL;s5P{s}5W-^23fsN4bL%@0&wv>#C!_h% -?sU+4N}{o9QA&jwUY&#*fC1$)emi^yw*!PZT-ru~BG3&rLVjy@r)%a5765Fv3PFWeo!9B$FM8u?sThv -1Ch1#+)!GHSw8@6=DRh|}Xmw=pY1BGpN19Ac+u(B9s7BDn2>bTv7>=>7Hb4$J;hsny;e*y^E~qemXYV -sCpRo~DA5(*#NJL4h40x8uaSJ2ZZO;bWtBn5O62vTX)&Po1)8cQIHuu+=sD|pGKyn)nig3#9-SpvFy_4<>4)C8LJGjKPF?8&cZ!`ZjyM+yi0;i%$|%HMCAqreqg@)y6-47KAlPi -MIwwjI3e>2Sd(?H>#w&oWF!|jtex61Bv!{F;JN@+VAAHbglqD#NV;GJmF)& -j4sTmskA)^n0)*#&!e{2YlE!>oS)TUo&NnrDt02i~*O+vS@uW0j4At?ZvAXp>?GpZD`X$?_exDG-|Qh -=`@aSPx4V1AX}g%X=~BKi``6@T2u7E_#VvB4{-qg%9ax|zbK8}?J%pejKFd=5A4=e8a<&|0FKI3fdg6 -aF^qP;|jkSvuclN)sYh*4qEsoySjw5hwAdnNonG@)z@rEk_HL>veUXBmW~jIX)dp(g2dI5}Y`YWaXGH&Z?*#gKD}TF_f5)MPEBfPgjN8ZQj0rb -g-;c^n5~eKNPnxdxns#!7rL&0FJRPU|%1q^qmR?CDz7I!eKhcU3rmxcM+N1k>|d$dueu%oG0BdT8PrG=spmDywK?^rmbGQobT9dn|+G9aEbEE>q$g0B^9OPh7>2E$2zdi!D=jXFrEa(2&^V5MNJL17GK7$~$%-`eigLjGvn4m_qMXP7vcPi!EW -lHe)I_!koS=6X~-vQOt1i$Kjp9;s{X4?7pHRQBKM&Vr9Q^r`umeCs&gww9PTk+J14{Sln!noIH9HIN# -o7f`}Iv(36*E6~|HD#eY2V@9nr4|ZQsBpsfwR=vhvm(T`Ezcm?*gqiLnznIHYUc3xbEasF8-PJ6iAzH -}GWf!jW-Lf0;_Bx}Ac1D{=Mh%Q^q?);6IOc`C*kQdmTrmtCk&EQsvbE5<4Lx*a9c%Wi`K)yQ%|5V~VY -1lPTVAa;pyTW2Sx4N0^Y_WnqicD48rW5312i&7)GS3VhfIoa=Nf~7hf70f_-%S;@qjs|C(HcvGVJw37 -^C;1^Q!&HOHl@Ypi*3!bvIpm?y`fL>S-{;I?|9q?RdjR+n -Nxv?je+)7I;whg&%wLE905KfJ5G>B(3=SsO7>4>(wg8;ID9{sKAz@v#xlLU_ZX0!M3=$ZDE-%>zyjCL -jc|hdKgVuE&-&&b$v_;e>;QNdMB<&;1F~K&c>jV(1k3C3W>-8&!hK8Vh3m_^42)ME@W>ev0oD8+V)FUJ2<&@^*&h3!hnOGlOx$09@e{Dh}v_lz{|Bp_k)vky>QKSU9CxMB -%JIr)TATrZ^R5;G<0;kSt?SsTP8Y1%CI$v7(1CcRuvD6FXm;Q;CU*ToAB6>%+O%-C98^qB&gzh`D -t=m*h)Y#ZpFxA`$`h^)Sq4)UlX|C(&&Dg)xa&Mkqt#hDjTY5*qSs!kv+ILPmWJ}JZa9J@v&OA}@$T7v -qwS1mb>=+MtaE`c!viFLaMYxX{H2jdsM&%gp+4HrS^G}FgCWRHTd{{Ue?Z?{ol~fkjxAb6+KAlGejP` -Idyi2|T>Q3y2XW#G$Aq9BcXp*}5~P6>occSD -JP7zXdVhY*=`;I(NmDei=H4^>tJ)?yk}8S(fnTX(HY5N?}+!Mt3)}pk7qpp6rFDN8vUhQxA7-Yv=o)X#|)k)~&tN*;+pUWGvFX)8BF-lkW0<&cr!yWP}Hs+k-1LJj~b4c^rR(L`Q -O7lfkfma;!`=DMk`Mgj4E2QTSBr?krmwH;SOe+;@bJ@f9Nq`f9b`6PuaQK6W*60l$m-PNFqdZenxtbp -SqbJ|cqk41gR8n_YG-J6THGCytY?=$plS7>;iG(|SZG8^8%0u2N@J>5MvXo7aGTWc7BnwSMI$RGomq4 -!1oYZpmzH$}|3wha*_re9g=Ti*7!hYbo{~E*`+qAyD>p#YqUmgD$xO_SGdt_l)j$i<$pbSBRqyy~J-d -{|D^%>pVR6yEq45ViS3i1HZZP*2L09oKt!md2zbMlg~8`mKKF-cLNfLmEYgn@zTmGpo-;tZ&+=`D19# -W8%NIqP-sUy&DP^GG2wpkgB!;NHBl4|H?9T<;Tr3k8N{)@xuSkixB~2mHfQ&@NRBA-AbXr{j*OLQ5%Q>)awH~BNN7yw!9p9OKhU(f$?)Di{rZ^&ZDwcm$lzyBMmxcu@ -#TRq$cLDu5s(gMI&=*ws>n?tg1@&1J)FkK`^zsqdPKboN!UqxwJJOxVx?L{po}(Y4@gh_-4`{ -NF;MB$4>loJMv8K$krnpjD%vB}4%)FqcDKSFbZ%E6=<9T0Zd -Y>-Uyqb(uMk|8=}C@#D!Uo8S*c`>2HJ$Cvuep&l+v%t7Z%>QIWvALS4g{sTs^@Ja)9q_Yqk#5vBx6RW -)0pyc)w>*|LS)z^Sl$mK_^DACib^w7lt+SeqA1>(eqc@F!JXBql$Y?DQIul(}DA|K{gzjny>y`)d?;Y -h8@2(uTGSs{$}TXD)oT2>_khDWze4oTezUTP9zM(pcZ1TD2X4dA*>NBo{H8(s#wTL(V=r$vY}a9c4StR2^ -7X;XUj?ul-eNB&|BW>~yqaF}Pp)x0WqE7tFr1AKv|B@LI;mn%5W%^03iFLG#;Hu6d?z7zd)>C`rFZVP -8(Ff4{H)03*R`v?_MOLIzw-pS8Uqk~U>`;(UGXcx92RJylFsC$O2QAVq~p7WOa4&hd2nnU+y5SV_w9QyC*%$+fB2#J%Gt0$ek6sD@@1|drW%KNro6zD$g9Va~+?qxpqV{_xk>S3(>tEX5Izq_}CkD1 -S0eRfgS8cZ3OA#v=bsW(NCrubU<%Qb$R -6}Eus-;b>1-)~)DlvqX)v}Ybw6;SupioeX -@8{ra_*(98zf=vyWKRQ~qDM^|8>%vo)agXx~Q*{zDFk@H}ekm6&3YQ3-L -WgvRzxnE+oW_g&?~Db1R@g|p-M?K7efFGxQ4oRSf1ukWis3lPp$to7>lggtJ{K@CL%`PItcftoA^PB{lR+rXKj;gJhGSt*m^#|Fz^|xGgP|-Bf5Ppw+ej_bTG`_+zo?)nT1s?XKaKI^{U`fO+zLl`HG4VdXOnIis_@K(R1O&OQ0{!?ivdVQTfMLiFKnx -54LxPZ2@jr1rnDM1cW{b;a1$k2Lw(uLIUu*8lOE+miN=A0{(4R -VuLK`trm-2qFHzdX=xs;4iQ6Ln<-DG32LKU+YSjZey7w3i@;y`(-ARqyfESIbg!Pt~h80+?APMMEfvX -+71r>k8cJj>r+9-4+oaQl?1NbFWelOi4Z&(e#~Uj|Kxr(mI)-eEVu{uV@HhwG>_aCHHO^gNO3T$8Ep+ -q3I^uWO$uNG|_r+}-}?*j~Ya3`ZT+Zxj -8MAJr9oJ)ee^XI2-yct`j&T=5EwX}J1bdK*qWYW7z%E=q*J+(HtdpIJ(OKf^Gs6fOTc45E13tOr=7zV -JE4zpEg+W}ggk-2pqVztVX`bp|tz=3)?EJi?|eiTKN7P*4i+PpV$q6{VZ8R_tGFz^;BkJ=5Apqxo;+D -mmoMl%SVVW9+=I0ek88a`s?J|4kAx5Gkl%$1kT!{Q7=;tjMS8`dtGQc&B~V0EK?#Lro$*_aF}RIEb38 -W>`4N78y{yz&v{m!w?SZ`VMRNkmtkgQ#{m{ONvK1te_>o63_A+`e~tvRT9=GYRctnD~^*>7A%^(jx(1 -JRc0B1)(yx#A0w8gjuRI7!7B}JS-ZUO(o@OnRbyFAW9BYu4&q)EW&+;b=tM7)y>}g)SDH?~2MNM(wwJ -p1ol+)?7sDCiy)6sPU?;q|veYR#+?zpNf4e6$%kDEI=+1B#G>p`vQENbVPb1vjS-XqHfFq&-lVn<9cN -hxj}6cbRG`11$`9@!wwiQ=E+Fik>&_+{9o%nDG4h*Sdj%w -!Keff^o1b*fZ;wx#ubRT6tq(5O=|~={up#M@huy6g{Bq3*8i`Se28xI8{7t$#6QVjCGkWVn5bOuD#ZZ -sVcT|Kn1VS+76oF&wfbL43q^w&Pc#MK4m>0_I+i3g7%G|1~;(1-s2a!Vu%x6p -aP?aFXD^pq+K6%TdUh4wp-cC*-^ -97x-L123+ZvC4>hme@jZ7 -4uvEad0=f`EA}2Z(^gt)f{%lWVC@Wh$J>&dOy06c4quw}pC2NE$+>2=o=4=3JWUs#d7TvUzU -Nmi$n(kvh59@U?oWX2^s?lxE)!Z02JM(uj;Nf>eIBU@~uEOyPyi}-h6nc8|-T@K0~Y2NNo$7Wr -HV6H#{Fk}+nBV6wS;#Lavu&|ALD=PA2iq035{u8w|2mqnp2^z5;dwzT7jG@WHoBEuxAcR#WF_K%xa*8 -g#&on?IRsed^0N~-GaA3N(m{~z5IH)ghF@Bj<~Q-ts4E3kaz@!&tEmjAcU^+WK_FAx0f$Uhx#EujSTp -H@&ykga!x#w$^Ykw~?$o>vPj1YO|eWV}OAx$N<(wY)vN?G@9aVcoEx_yEtVw -!%PROcBmYA3Nd`~_89U=E6^cF77hUu#L9ug>r~v=!SB59LGugTFN&1-$L0__ewrO-5=lbaEzrO>AuUS -Cf>E^t7^TZVS#qu9#NOaYo9~{p=HR7C#|3PT5mrdD5IdkQrGXxrOs%%n#Q$;Ilvr+cFGEndILn1=w?k -lsp5dKEaR-7B60K-;NTS8yi+ct0y0PqhbZy)pYyboFaVe(pwp8R?@$(YlYPw{#Y^a(ixgW2H~7uHvGe -w+uf+!YH!P%-Dw41P|>qW6>*TA~V0o-ho%ByJvfX3Q>=v;yya!OppHrf%2fK!yDa)6b!OjYW)HN&zHkzXI9>ETh)mwRSq1#<-c!<(tjx$2xXxgKLvT~0}<}MDCto!BmmdwB8*Ey+#Yr)#;o) -oFLgRG-;V8rG4uK@4w?ZLlF73imddc%5|Qma>T7pHF4<@)ZfXIJYHGgZ~?^p|$V+k^8A()7XZr{pO^@ -k5)$vUL^(Zu)~%zLCqlVY@T(nCqp}9h{v_WWQ(mxe+26T~_$VukKrmrUFlo=NNk2^GtDJ>kbbSTx9RX -V@g5LeC&O`6Illf=jFEd8Rf~$XBW?DF1^2ZcMET^B)-%I3-t(I-X2cgDhK#5)_e0Py!hxH9sJ8$HSok -Tn;q-Xuf91cQazuT()Ahbb)rxQzKiklNSm2;;D-&us4Z1{8Djd6-79zXTlL<@PJJ3e2Tg -OFs|WwAj{IId#R%k-%}Ma?`jfd3dFUtQ;jx?_(jz-0oCbG%`#_05CSua=KlqQmlRRzWTlkM%*oRfvgg -=`u|KoqHYr#M4|3Fz3!x;KM)c^f~OYM-}x_=zsQFV*EpPz*IX`a6SQS|+@`zL~oc(KGVd}ViH|o~!piAVVzUqYoRi|)L=CYS!K~zu!na_ -gO)LjOW@8|QU(W%$r!+X~zuF_7}0K;BgWR0j2mhw3>ji(KG`npKTGaV1p -_Gq?g9Q0MUAl^us)x1GfcSake2Oj{EKSj=#c=AbuO)v6)-P(VSfnh4Z+)Mv3JJ>2sdC6Cm~{Wj*k={! -3-|aE-g%Tmz~M18Ie68NCEk5!KS>ByDf1v+taw)m>o$3pCF`SKbK%i(*{&?oY#6Ipnvz1#VO7U4v>-W -dVaO_4K?D##{5KuBxppjaFKB-<)RkGvo_4^`jv?|*1kITG8 -KzvJiU(1iqsr~tK)}|z=3P_dw_j*-yJ;|afKq&ZKhc+;u(OSn`K)SgizprOw5*P4hel(tiyDzS+SpK? -8H*WNL<=gAm(GZ0X-){HIW2Hx#ZWV6+bKg}}&=>PY#t^?HE6(=%TBF*0UYx(KzJC9SyEvOe2Z;h+-6! -4USGF%r=5pyB=RO@ozW!FnU|ZH<6#|Xm-oHgkRjwu4c0%ZnWK{qOBd`!+5M`uAv(T3+P=Yk -3x5NDM}~1&HOV`vk==)~gBmndff0!so5ElYKw`BzV=8(ES(}Mi>7w80XA=5X=T-|&!P@Y0ca(?0a444 -GftEk}W<-0w%i894aqa-hmezFBn&d$s0-Jkrjyv4bD7csOx;~ZFTXqlc9^lqETFlwqT}>`FZ|Zd+l=H -B}yIZXnn4yJ!m4(;n$;c0h=ma=KA6zOC_)Qe1>WWTQR;~x{6Xr}9cKTLe>Sbhr-7*-c*r#}&>G -xQ@xEuN;IKtgzwHC#FiXnM^Z^4+S8V=qNxkX-6YQi1XZAqGvBhZRkkz@&EYUsJY_^O!H514KtyW@C#I -&0ieg8RVgJH4Vj=ECl2n?Z%^l&|!Pzyp<%GZ$UG+0^>zPn8wwG#0?V=Epk)2ddM+Dio`sbr*jg-FHcO#}-{06v8iC5t{?UMQ2T{os8D* -R$MeKwEhR2^|7yGNR6=T&kdGbOHOI>Mv%9sczG1fjQ}rgtE>tP-_ZB8q+FXw* -Fb80xt8Cc7N{H2o|0#-y)2cIOc$Wb>EwyYOSW)qCBkcQiedh|BWDn~60IRJ&0F-IuopgrQ^NLITn0j8 -$yLvZb_Fjj&?#_xQGDF*&JY+6a`LvO@O+o82uF9fjl=kVpfD-rZJT#>`Y(F-F!KBUBJQ$(Ir)&sueAD -?+zmS@nvST%@HH&)FBtqN9RHIu4>H}0SnmJw{(l*sA&^h|&5n|iBXY9CAL65dddbj;Mv7&}nsj?r@Hh}k3`#mybWfgf=%nE22fe)tY!>PWw1)PV;NHBap5mfgonK!2%vsUO@(% -l*HEXTCtV_NSPK1IY{Ea18qm!kJ*zCgRc_F$+iG_{*XFt&!~z5IAtn52O*EKf -f<>x^oxp>Yq;R{0w-)m@ZD$3YFDtu!iy024t7J;pZM_P^wz<5GK7+7x4|$eLP)k%w)(u-ORe>jii*Qd -zl3(i24PqE1W32P)h-de1`}pFwQYBIwfXi3bfU?J$AdWZv)5g;3g5Hdr>-Xi3vy&RO%E)`LIf*7N>kS -07<#Y_a--}GY?<*;jYPQ4oc0&*;LaD{EQa2m$JwbY(6PbR4;>YLyCl$$dz&I_Q?SRX5z6EEteiH?%th -!BpIkZ-u_~bgm&g+c6?B|{fuwR+NzSW-F!3dDPaeYF7}LE3lMltLJWMcLuVM7dqUrhQ4w9s(c~5%uq> -mOMsHW6I{l%JS$ix~{WjS_4_}eVuTe?g3+&l3Dz`RsR<(No%Sw-7t;u%%+4+J>a%Ml(deWp$hb+BfUE -0t>S?ark;m^!IW3+p4%RE{v%L7~^O1B@#d_U0sb|C-9fwnz;Iqp9Tb%3vIOa+0o`)_QVEJD?*CS%mkTwjk{nYI>RlGRL`QB?&Zrv)`0C4Ws25pO^BSh<0{a1E`0iw7(MV$y1IHY;d}Rf)68=#>f!9G#vAaxsbJfeGT%y+9pv=EROed~N|R5Y5 -eIrmbIn06nAa+YHc)IO*Bg85=@1G?Igz^vK;xB^b#*~y!>R8FneH`C)-&x$c*9MuvjxqBSZ_(X1rp-s -7)y&qU-KE|^P>iC6rid|SwF#g6s~RaxcUudaB7BPPFFWNYvNnMP&`!7QKoV+Sm}#86{XN8<_l5!RXzh -Hn{MHZBXzlC%$CY4yt(-;tsO2BHhrF&5quI!MqNv&xCDG1IPb=ptj5Y{=+eIu;I(*ih)a6yy!UT4As@ -<6lyN3Wv-f&zcbDl;SCBY+_>HnVraQHdG`GYEek|qwsatTgIF>)F;+^nHD%#md6eS&Dhi? -uFWq$giah8IX(wqr4uEb~Nr#iTXC0+8fn%NJ@S-js?0pAjxk%kI+S*NeD5n$DuO?JQbvkqn+%n9J--n -p}`aeGh5{s$6nF6;3nGQNl)^-go#+REB|seeixMF5gW!IrC4;?Jd+{rCRrc%I8rVHg%}r=j&*o0`SIl -u_J3}%k2VMQjv?EK_>V{%)EAtFxFQE8c`)IXUvv@8T7FiD4dA8z{Zn?4oL?TJH+(KkTwMR5n8nCOMIk -juX`?R23SEGykk7+MKyVT -rE?vUzET)GX&w`y+n8%sBd{af+VFF>VXm~s{#wC|6#Yz}pii^&Mog+Z7S>#w%V8YBH{#qeKqPVGRJ)_ -@#QrCDU}uD{BmdC6)k^{;$=hlNt*Zlq^qBqU|t$TX5%de{@nKtgP^>BfN$SQ8-73`V2X#(fa;Q$i#(z -qRG_DTB(@fcz2fx)?Mov~|;P|BRViR-Wrv4Z{<~onpA)HXwYYReE8J^+coh?L?d=diK(j2*SQCI*rrL ->Y^eF8>?&i_OTeQ>T`_M3~0geyF~-Q)j>x{I$8}@5SJUoYT{{xHA=c`c`B8*#8(OYT5qP-cSPWyqP8d -?+MqpIyn#2W0s6URS;r%3wPQ=ku(W}pVMicbv&ZRy^YFD~l(qurl-V&f?6n1m70K{K&oPUxYXn5#j-I -a`ey-OBmM-zc!>h@kl2ZlA7wL9K-xl@E*cg{s%Z*+kmVhcZ4kL-eXZ&Q|fFgZMZ6rP5r$H{MhGvS)i) -cLbtm4^PJe+R}yqzasA84pUF6U$l?c~Ycm8Xho8sPvc8c{vm$WzI`hHwF;q}tlNm_{;fNt)4VO*xI_q -)C=V`wH4lIV~g2^?u&+7EDGTxU!p~5P^2?X4S#p|2N*Jsw@=Xi-r -oQR~eqWFNe4vw<=NX{=#{NlDvs4~G4o-Pxe=Q4+f3ura%VmmLLde7p;Ba~~vrzb<&?-j%zpmo;sT36` -ifc_h2Ep5e7?Ln-9{L=9xAW!g`acWex9;t#D5za(72Klv{B-(R9AJL=i7&knkF9E5!o!H%E`c?2-_7x1qGFer7j&=VhK&FoY3xxYn;59}ik)~ -y}wrJv^ZJUI$lc9;dhM?V7fJ5ewy{)nf8N7Q6TdPI7JbauRkWrr|q{*7-e-H|5piTRMn{Sy3WoH&Ym( -vKE8bo7M9=uu$`=10>R^m#y({7c9d9$q;p{tC$k1@TC&j6hnzEw~9b{Al&C~OBr@mCNt -Iepm|=}m9aMds-InOT+KWv);ulW&zc{g&AJUaQ --lP2(g+|3U3(e%nq_b!{h$;i>42K=c(jm@HVzC^|m7a2Z(Ir%%!q3pW@|WmEc|kigRz$ODTDvmjV-R#x!M63283?ua-*iYeKRMwNddIQQ{0AjPmw~SyeLApiIs#?9CkeK- -?s1w|OCnx1dglK3@wf05sHui^=M!EB?Azokw0_Hy -G~fJmj%dMTHY63~r_VmUXL=qqKQBFIX>jh%zrer_3U8|@!oC9bJ*%UxC?9P!jAPpil#jnaZ*+$+$D~J -Bm;nUgFHOmdBA#Cf2g08BtjjGLCbGRTK>SLLX`f`pV?f2NxwCD4wQ{t=RSV*nB&$y|*!1&LqUk_Dp+` -w6bGMZ8<_HfcZC`*tAiVQ9r(S832s1e7Gs -q>D|y8khSZ1$j=`lC0EK)~Ki#E*9`(VeH>fi`VaTc4F$=LxY@t+kokv=`)3+4;;jPliB*u#pUCf@U#z -+6+k7=*=%J@Q`9-3IfWq6l9-dV?TNmZoO50!Mov1fZr@B4S}|g=2WctAuYwVYJ)RrK{%ivwdE)OU89K{hYp@ip;DtmY&Hjf7z?7E~g0w -IN4=2nR(flhdi588&jt%KTIkV;QL1^l}cH{t{&kg!$apa!r__*gX?kWZ5zc72c7r?V~OLkzVx9Gh1(Buo4jB9mHV=di@Q#Jz;#wchxr9Ss()%)63;@n&NDs;9hou@ziq>={bqbQVFSrvl~nT)kT?#=CEFvo_!YL -2+oxZ*)O!&n&UokHk}>g#2&7(%(4w*I)_x+hA!&9f$t>M^ftG{{A4PoIG+< -B>4#-?< -rm57g+7D66{N64pQn8*$&1%U_lIwf`DO$n -L8PhjcGw*S|`lKy`QmeeR_dq4C|Gay?$%bMM*?w$$hFz`gP@fgVP<~q`#kQd;ptugg}1$kE6m)l%x6p -C6|Qa;UFsu?57@6J>#@Ig8ho4)&v4*}F7lZlx1F;gkR0|t^lJg0WB*E#Ut(4s~32_qA}o~&7=-(c3)R -gGrB&^)}emY$R?H|5EEcro?jYV(RH?%BH%B&8j^yE%te7&RR4fI~2<_4 -4v{0UbyVNx}(V^kO_nAuchtbadA_$uo4jSp%61=1sM9q&JenZ_^pJtzo0JYjZNd5J|&(Q0+;&HdaY*K -o@Uy7dQ!QxpWNaRGZQha?8#I0Q3IrUKU;2anC+Vw8LCeL7E%8f?dT(K6h;3~XXOu$5Y9c{VPQd7aflD -NctO_Y_6@j}zX>_b{AEeVac3z(2^JF_2LVdW%>=G|#Xg=_6KdXdJZdvpZKJz%QUnD&8$b!4-Qf5~u5XKu`h6uQz4wePuX^jTf-sbAL+bi^Ds_Gmrt9h}(%mY;sdvKC0#u4y;A-2~ -$wqZUWn2xWxr|WBLG`o2MUshw`(q%J?4|NoP0-Ciqjl{yH~hd3T8#X2u*B!+R-D`9M4-bozX7i#FePD -4OMG+x6fDs!-2k@d^EX|gFL&;ZHh~UprvYtTds2mW_I`xI(~*sJ>uIy -=3$t-{NYx?*!nR^nHRky2i))`xy&*<~bK2!GNQyRN_60%I8i1Y^+#mBK1{$%mZm8 -X-vCnbo~@VrgwTfi2j|K0&oK#DIyph!nEjc-=ZRY@@KBy!@um;%r5ip`!cei@chS3Sojr$7NZIcp8E3 -4z5_nFqby~B1r5C=hvvPq!!;&7N1inRvxlU^U!-;n)O7u4R5%QONyUlSalN!9vhzQf@(wX3CQk-n%|i -Ah8C#wVD)MWMd(4AXF(q>W*J_bXKmdKV)NQE5ML1axZUZn=3G_YaQ(&uw?uXfhCksW!-)>b-7s1|Lp& -9n9VVG1?V%B`HMEQ*-EJo?e#Xkak_2#+^0qOSCGmf -?fm4;}by7B{5Np0m0ttC&c~v9An&*Cg!zl@5QLu{wz(`^dIyX{~s@Jb2m@_CAN>?G*8zNJnM(A4h#hs+X4HptGC$~%i2SG_-)~HOXoU|V{Ol-Ihj)DUmspIOKlB~be` -!+wDpL9r=y7;|0o0A|zo8zvwZ}g-a|uc3v}wnzab&cz@Tn&5FL1B}oou}9#!%Ym>?`xopbI~q{GA^DN -YGs)`A>aWx-JR!7>L1e4UG1v9Fh`|sxRU44a|LmQGcTFII3po)%1M$OH;G{l^^Ly6@I#!fBk%a=kb95 -_I!Wm@qqvKeE;q7zI8zZf7J#}+boq`t@%RF-q@&?d&zv)teO4E@@UVP*&gk5~2=WKUh^0H -!xW5ireI^MJHw)#}W3A~E})iZ+Yb->`Kz6C~yr_~FQD?hCj;Z?v%JHtxK&Apbe7&cWLS``D7t~fTDLD{)f2Lvg -=)NK75F&FiwVAbLAj61@`9q^j59<o33r_$S@MEHz{6KPv{@=7R4mT -!f&=_I3HwX10A^}D>~VuQuQB8DAT=7vs!-|88ssMkd+f5zSvoZRqn@tbW``sYp@Fz|5VX?x{Pb)L_smZ~Q5J-+5l5A9doRS -}uAV))1{0V9tbX%Va;X}X$`gi&_ifIp%xO_K>9DA|0U*)HD9{j1O)DZyP@n?Si#-F$F+E*68Z)V|-X5 -oFMzwIl%paeejhgK$#eBv4O5fEUG%*i1rYI6bsbNR}) -%2(mEp{BqH$Mc|Z3lt8Pi!$<27#LkijkcC%1ZrRU)@G@v-e!oa@$f>|GF_>0M49!NUJy;UdcA#)>LaH -;JE%v*LtK+9DEBGrC2mg(!{rVrPVcvbdEAXae^cx=vCsnYz!;e&ljlkgio*aby&nf2U_77v_@ -~=-_p5v$GaYTim7si|OAY_f2@*d -R6~RIsp6gv4i!*o{g0y%LqBoyTGI!@~{%f9XkENG#}Xp&MxRb}-f~*4Je<6x0&=-!K}o4EUwcqhMr7( -`8B4Ws!bJcv;9-JQ1BLEe&u_uSn)Lpk<$1vO&K&6!!5b$dxSJ -NPfR_?y3XIa%j2m2dn|Wzkuk)wG*Ofag;fAgSvz~V~5U0hK8bc{u5}Pa7(~B0MO2hyIVg9Vmf}YZ1N`Bsg|m#a@4~*x)ZMwm|=Li>)^F< -C|@i{l8(co#65ttL@JQcm2s~`!jHC{MFF@?#O`O4ej?M`zLCiO1D&;Gf(vi(N)%bM>D?>Sp8Nys_Y9l -FZ&K?n^K`F#%x_>yI*$0;vyYW%-3{Pk)|){H#bu+RTxgx6;u3aB=zM6KgyGulop%mAb@xlbQz^;bGn| -WD$5h<39X2b6NVrhOMKGp%atJ-olv1V%1ll>Ut4kTT9|YnXZ#90ZBG~SrHr{`WTgqAl9NY-m!(8EG4) -)@tBU6fot(y5LOVul140q9Lav|-8XDX>Ku=)z{_()o7b1x-sS+&qWL7Y?LDLeyad7Z2<>Ni%Cc?dMSV -c9G8InWvt|E3+-Gl_B(mqKlcj3sP-mgr&X%kO=wokQJ9t3eYHYXuohkt`Q -u-(e}X?6L8r5-%Fs%WAl#LAU_#7?z&a46Vr-!A+ -|??P_*|-KNNq5D$Nx!%<6_f>cXelmJc8Xbb~rJ5?_&R|9Vj*>9yf|csQ*f-?5Awo_a!94;Zl$u=e%a^ -^GB-{!n9H^iqi|4FO=?Nz9_Hh~PG&jd6n2>nh(ydw0h#!FWPMi{fmEo${MQkW%YFpicFa$$)20@rW)4 -mOd9jS>rHX8P6}SzVfK4Oc`ykF~rJU#EM2^vKb^b)-!oFS%55rF!@nGV25#}5|ymvJIs1IeI^gj6Mn8iNUANr7Nf`_?3D8i_Nt}`nRz -7eSSc-J2#R2(@%-aGmr<0HC#Xr_`!#}4?HGMD+xzRvR9dG`yLG?K!u*XOnc-}g`F6mUh^ioer48fv#VK9PgFejjw=cTh_U?o -HxN-J>3KaX`@S}G5O<9k{D&5zZ?9MO1A3GoW&SOX0?d$H8uIM6qbo7lKgg=Zl|8@UYDonqT53?Zuj_# -8E66ckVIPamBQa?zWX0up`Txb-mgq;yH2aHr%f(U0$_$c}&#}s^vV$Tr^xh*~;bJ|gLM0e#W26-O$Me -1GZ!@fwquZH`IVnYI~&`t2Lj+bFhEaG_@9s@|%_$6>UvJp?ui0bw226A$2UBJfmfS(qL_KVZF36c*D-yGLQ -?uYEFzXdACv!3`i7x&8Bi-5-aRD@6wJ-Rbjk<@wTj!QassJ3#s^Ol*nspYMe8}q?;z=I>OhDiWR^vF8 -uBB=+zK15y+-CvCU>qtxK16A-m4^YdOAno*24&^}5H`8~eZ(X)Jx81M2g&3-g9yDv?6c6HVW!) -|RT&fjIeox)f#fzBy`@RM%IHw69<}-*88~Y8a>Zh3O1Hr|G8UgQCOu&2mV$hv@uy81iYRB8z3{xUlt-7Y_7X3SoaGu>|ZG?9H0jHZv-FXwL@P7>r=U2LgjP -ZZdlmw&b&Q=|ABPB3eN5;5_{QQNv^wkmR#P6YsaLxi1hcNl;E5j`=~xY&me)Rx_MFWf)0$V?baPM?n@ -ogX_1P()?7g`T}Wro!mMOQ(w7WbDKz#IGKh^~71;OA*!qYa7W-hE!^g -8MYhN={|K0Ssjv7LcrM6;XZ%uyhrlL|4{^Gd`4-r?4tWf-lT*k-si1Z?vlih2x$r#-Nrj=0Xp|RGR0MN@NJw -e&NolmK=jw-#u%p5UT~b?-tB7&+!+6;}y6}`d_!C5OFF?dfE8RV@;qlUE*}ves-5kzY{IK)$(^p**rt9`(`}_OE>k -_)@5UOV*$vGis2{8v?)6EtSNww+6c5Yc=jLp(tG(EUv6^V)y&tI#zZ&#BpQ<+Yk6fVLgIyLVbSg#Yg0 -2;au{0n*8Y(uJHUJR9CU2Bi`m#&xUTR@zy;~4`Pqw^^s<` -RQY)MbBFMMJ@R`8@VBFW2v?9WiGe#VLJ5?_DVzjZm>{&nmEIZh?wqkIy=KCzJEsH5E9!o?A(pr9k7fPF+154zR#kQKtw9f|$EbL8L)0gn!o{KHqCeIQ -PfAJ___j;hEVYVEiuBR(?1pL$LhK5*U+fS}K?;V-%%pZ?$sI=YsRa>{)ue+iEyaAR2}`5wEFqrpV_n{ -n<<`j=9@cJl|_HbBp_a`0LOt-`p$WBG}0+jha0u|Xr$ZE$A4M-rT`+lCqVQ+cJ}f71^7kfi#v>FJ2RW -nWmyzGw%1-=_2O!Z)-He2a{*2dx=&b#1?mcPwLnU9ov~o+Y0jzl^fsjjwzNNBJgyYy*Oy6Ys&1+Wy;! -Cjp-EbiVk{=(V1rYu|$r#a-JsV(q^Eomj}i*(}ykt>QkS#Ta;*5G}po2=9q3_2&M#0Mb}nuoII!U&4{ -aGy}%O5o;q5ZiVaIE>poX>BF6iY&1$8Awl@8oQx;TJv7(3gbzT3>xY#$pd+r-Bh@sTz;&zA>B^u~SLY -hJd_8b4??irGSQ~7Dj^w@U7FA5$u0~G-tz^Bn5{oR_WXv8^A3?^&@e(D`-OkPM&>p|M`PEQWv3uY-PT -$ZLbe|Iuqn!MzUVu!KvnsejapI<}?C)JB&{~MrFAR)l+rNVMGfnnqG$(Y{evkIk0ms`+Hcv8PI7~YOp -ehGcpew1fogK&IvHF^yOjefY7b(2rM>@wSDVjX} -`1IR77euhnLN-;w?LuPQfBO4A`x(VEo(809c)vERc7LgHF9XXW=e1g=6i`{snXs}Tp?FWd}b-_xC_2l -e9lun~Rs%{?xY1H51aW~SUZtM%1LS4Y(Cf`=!Tx~YdxM6RKmn^9=`}NGjD_rx`g3opfm3^G|Rtrr25Z -Ux;8DAZQ+|DVgnS;6P-iPd{HjH0oo$gcnMPI)mo_#v6N9b}$=fb|ofI43e3Jz)it@EkJymkIhjAI-9i --3pFT#(B_H2A*W+X1+S!Yv8gNdph6nySz4)TaG8DH1yGbfwly*y5MaXtIW%E{Fo^4Eebc@bykiRTUWoi8>hh(r@f+`|J21P7W{o8X(k6=>(EXe^~oM?i14#fj?2SvBwU9!CBg&Y1?KXTC(Y6W># -9f(bxScY0;)-nz_-UfBfZ+zd%|t;OBsn>1k1?FZPZ!_xI$?Of*Z=ez~7$f^=Te<#BcVACMYU1*)A7n?FWNHh-5R7ppO!z(Azpt<7{R(9|sVf$=KiA_-$A)8hs?Z0dP -Azqfm&l1NF02bC1X=okva#T_&3FdV& -sG&{Z&1$uWp#-y0#Z;v(@5tCLt_p~VO&}BBQETV#_c2#ukeE`M|A3MmN<ytp& -_ct*8dy+N8flJJ8V*J&gqO3h5|82@E$}p6B2gL7_xbwjrBac)^?6jdCfjKdfDAcZ6zs7^?yIRxLb!{U -cU#l@uc+Y^DxXgUaV=8B?od(_gv#mGlQwVVh)lHwJzxpwtsu0Q_^Mg%EqK}BB=X3bDwmR($`+T%@PhP -wkvJSU1_pn5*64-!XG%t23UdF{QT2DVi~jD3Kd2S`c+#JJ5d_A6*@;zrc>aj|5WLF|IlZIjn?SxcKT# -hI^+V3DIO=Ap-y}}cBP@!Nhf8tr5uqQwQRMJ8_SZRiG(qjJ4+T^3&_XS+Pd|C_Zyfs_(-3vAI)Ax)@z -LEoBI=-}vV&F#k7{{AFzR(_yrJ= -M7{z?LSo9KMX_uPrQMFkS+lKQTO~B*bJQA0m&GzhB*7r@PE;zKiZl2RA(OFDDievFJwnN_G^Xf{HMOB -uZ~DV!+Mj>MkAy8X8hR^Sq#s|fDb>i!2hHx=g?BPD*;~fZjZYqria;@=lXz8vl$i)&wo?iLfWXCc{7$oZ72+ -Fb5K{&0IiaX*H6o5uwAOMvc{N9(!r=8C#BbHS4&J2BymTAg4FGAa@U3g`f-mNJVx$X8zVzk>`OV`|o- -0BaE|*EvLT3$~?M+|#8$4dlk!K-6p%!cf=*;|lDD_=Pkm>B)VA50_8$ILec(E0*)d>n=$V)Ka$=CBK^ -xRp5#RRc>0}T6p-Ist(t{~|(4$S+M#2XqBqWDasO_X8{ar0X+dRzDiC5SIlAz2WLPl$bf6s(%;%YL1t -0E(-RTlgsEUJzBOv6zA|k`Yz(&*6XTk=aq63J>28gl&066SiYC9y`&naeLNY6_ZaR_g^+|>#V>h5MrLOJbqM>++^|HylXF(q2JAitEQ+Qg+ePw5Utokf_ -Uw$nAAuad?kb7SWn*Dv!^9*p_e&1;oZ7!2dr%=kGcFb<<1+~LD#>-R?I|^JRBP}%mT1%>qr0cUW7`(E -Rgx#^1xUgb`7jZC@_IRqn@x>QPrZoxo*ys7whd2IiKi&R0bbwa%Mwy&=NTTzXU!2bVGt*DpI;D=lvb+ -l4+df5PSU0M-p~frviYzYP)ygqp^UT+?=U#%_bu}p{#ip5cS~v8DTp6U*N;D|k&Nj&mBThh9w ->d=$&V@PWa5=476e2cQd)WDkEe|Ivg|=+11-;88d4r_UR%i)^)ic&yZfKC>%Yxfk78Dj1R4U06TOMveEK0Uh$G -sdVy_OHtavG8CdBI0p-#jF7Flh1V0MT;GQ#kEx@eef8T;tK2`Rn|=ue&3j*o+STgHEfwtz-Am<*)cVl -zAVa)IaU`8U6gdQ@-P#UmpHLnwp|O6rymPKp~IIJhXs6@k{6v-Nf)C -Z4KjJ5ljjnUEp7vr?7vczwT2!N07K6k6LhoIIajIN6a|>2wEL);X^@VM^NwwejSQe`GNOh;&_TfJ{kE`sl!Je{SatI{!RY1ScZ -9o6jt^dI&x?wJPA(@(Qe6-Z+A9*Keg@pOQF>nFgD)T8TnhVQ#)FE;!yAsc=uE9{Ga-RbX{njHWAy(hp -Fk{c|YI=@D*>AS@TVeI9+!1gMPS;Ie-4r$|V1|t)Y=`3huyH33vY*F8w7i-pQe3;qk@S_|3tH82&V>K -OmUPz<~an{_|h9pX=%gUyE6etu1!1LD3@u5IC_DS;$*XD6f}_3q5njg;a9Q^!j>H!j<3=alSmyBZ0jnm^(rEewkA -QTnS8T9?*?}QZxu}pwWPyYf=_E8h7dGS>h|pjmGjvI;KQgqB7L6X+DA)0eQ&8astjxR1pC~JU!MBGhJ -!!RTm{B*)Ekgt!=%eeVz`I)Q(=F#VjbnH;yo0wQupdWw8T5Li$KD8$^UNW=1A043)e4rv}OqDdH{CLm -QxaQZKPQov?&jiBZG*9CV3dy@=}V4rJr}a2L9i4C5?x=}3epngZ{{{%sRc*hr9GK|*sL>oVtxCDS&z}1lCguw#haq*)#xD(bGdR~QOn9zoRLhoi2?%+cTk`&Goth_Fe4{f?nP4T&cvY -Vk&y?RL^x@fpJTjY+8lY=1$Np-_JNoPtKFdqQ;A{#hL(3*NbkeZoB4`My;&vrUVdLj`yBfQ$z=%{N--IC*_3H{1{UhfB<6HZdwb9h?O7D)u32?M;{knC_n;Ync(w -YS|*fZpe=ymF*r*JAg{!g7-zz+hJJ0f~dPP&`08?~=4=QHPVVspEJx7q^jE+NJdc^LQY+2~xcLH9Cgf -oyRd9D|q~+plOw=QArYQNi6LC`YpRUPZHfKU0N7fkf_|8${w0I^Qp^u`)z5_XPmOvC}m6f$ooGY!O@u -ON!zYn_GCd+K`QxFrU4z4uKlp(!I`HdsVatg+t|bS0JweJi9N`yJp<%BJ7j1d=KB_!gTj1c*N&|x1KA -Lo0jM7@&AzbW=oG+TesjnPqFWbI-+lU2ckzHdI3>4w4xURB$}r`ptN(_=}dR~-=`|7BGS$;`Osp4q&d -gz#$Z}z4SI}=5xQg@Y)NOG%~Bvh?pXwGoD)PCzcFv$4fOrFdLq$>+|VSI8OnoiOForT+8$Bz;6;*6BE -wir#}bwVOzdVZK_4uwt7byJE~RELX??e8%m8s!NbNpJFnG%LU}t^d!f+2S%;?|>!qw^}H=xHuXXSN=n0I$2AKlea#%kd7pe-+Zu%dw^@b=zA>ZAQxCPZyr@N{naKIo_s -J(~p@8PKYd=19=VJl>UZFxw5DrP3O=AVr>5b^|7g((Cu@DynT@r($+$Sw1O6T+Glj?MTbjE#>-b!5$k -;+uOk+%c`&Wt9o7f+0>E_kS-_QYYZYle3M${tMK-QqA7GycWN#mn$CM!s1(%h9;vDCcR1lI48LL%$?j -pfcne^w2ue&vSn9U>YB!$bM6clIx-+}dpV{EPWtpVP9BF6b{8vcFuYj>dJrk -d_T~ww2#%r2W5TQ(qO&{&+*bMt9_a>o5|52ogdO97A#Zdq^(+$XFb$e(2F)AEyUGB0uxtRCMT0>=+G= -K5}C84*0%5(gz=f^+UnpqgJ-VCn7r%P4uTvA5T8~i0Mc94E+q|<;MU#z9YFG(K8-9$qz_~eC7nxPsif -Ew|&L^IG!Fw_T)2V7<~X-0(}M?u#Z664ieErts{#*gNgKq4jezmANDizgS_$2kep~AMRxArjM`Cq2Rl -KGSNL~n`%k{>9{;Wf(Xus@I!^xdAaYE|#_d!bNN#ru;4fq1I^<5CTPu48q8)%OA>GF|yM -wXm6(h?#YMXI~dc7=#jm#H)*1}CHE;|UT@1{)(U5PD9#neCVVALe{SlR17DD7)0W8VS#;jY&A#fH%Df -32U!aV#xdJPQWG&vt$@9Q9En{f$z`moCUnkNe{2|1e`dZ+yG#)>nnfK6iwS|M0l0CwYYuCxuvxgBQ#&0%-UC{mTc& -q7G{bR)Oxz$p0-IyXT*yHaCk9;@+wH3eXUz4Hgz%N;t7t&vXAhADLC=H*2ZNw;}EgK-)I(N*u>Sd+Q) -Tg3xG*qnc*qQH-gxRhP()Uk6Kz;(2e!jCcd?bPq}1-PJRgR -v|P`R4>ZFp?LsPxE5t$t$6o3@5ZLo!*?P9`5D_|6(%w0iBq)Osi;$YMC*ix2Z#rk5#&zg%>q5Lmcdtq -JhX?WG*ArMn`qp|*94?|k`tkbqU)WPpPb9VXqmaEXDr{ucT~39%OPe^+=dCG+>E^N=*;GrdG$Tf?Fvl -;rjlqJqs9|1d^h9c#qpN6Bm?1_YK78aT?x;PF`xZp_L6Wqy=#|wR!>2^R$_o#5(R9RN%3|_{Yu%QZRs -Yy#@xE$u$>b|530&?%l*y`9H$bH>uPUZAvDEvI*|+Nf|soW2BA-;-6#8kfAwOsOuQ*RpEv_m*&@6?dl -N12fPpT^$B(4+l{!^wI{DGTpw_$7aHF2)-ftPbypK#lzD;NACrQg$qFGA1yUAw$Ls{4tOL05q -)qU+`&UW9FE1k=>(1_g)a|DzneAUZ&Xu@J%?p# -wxm#c9`Y-r)_j4HE)g(z2dd)P0wjk?U!qPuuK_&egA@313*Xbr+P2a7u!t1b1z~?H6#X3V%jNVUnAge -0wc=D=ef1dt;_TlUK2-MzxY4k24_P1#qEC(S>Oc_g(jc%5sIhN6!aK$TWTKdGEYgKdT8Um8oB$-LpDh -X^Jxdcuri)KE1Gzr#YI~zWEWu(I>G>U{S!d}*-C(smnE`s`*`k7;x0k}804T;%rZ9|P6PLl`VRz0R5m -LTD7o*rU*3XPLZ(My!2c&_Jq-BDrb!p{(MHeoIZz}Ls+-SD%xWm#M)Jss(wCc-?DTB0Aq&p{R^VCe3d -sP*?I_c?T=ehX21-7T9MpLnj5F4jF!skRDPpUNu&7_}#C$=Bq0A0Wtvq<0dpK{9spo(dGa;(1c%nri^>tz+G>$-&APZ --+T}sj4om5zzq?5pkGd)j3+2mb%bH=A6e3guf%nT;W?eU-3VebM4DluFn -@zm;t!~fkpE-UP5%Q|`U})e$e&U-9VOZLsM%)NF~342$7~7&9YjoU^iz@?*pVbZBP{t(MD%@7`;h@g( -ofx#I@(4Lw(#tzo08d)N!)=fiX0O=JJd|_`kca<}a~wXHF?S?NQXf_e>X@U=K6p;$FQ-RS -DgIHTp+0m?NqP(bVfaz1jXw&)@P}0s!am8@561 -7e&ic=iPw2UA%zz(ZVE6@du!otlOE5vuA&r|Ttb`eUQuvTWMeL#_ar1^HW@@;@I7_{og0(e_m -kX^SwQN~h&yo`+SZiJPx}F!&yIm!QL*o#bWth6>`kE;C`&2xyh)2Q8puBZy1#NyugH`7v3XF9nP? -6N8C=OuqC#Mcs4)ZZ?B3Qo92IWvFz24N?`mXs@&V2h>fPc`XBT%+o^vRUg9%bdd}!)dv;`w~a!`4tx% -^RbvCuQ-X_b)AOqLxqYQ;yYIn!(L@l)>`=Zmm^1KtFNF*STf4DDw1s6xK)j4uL6}`)6&pIm)6k&N^SA -zBgR(9AGF|ZMQRbuYmO^g^Wd3m^n4hQR#^CYbXHyZU{w!o=Bjxq^p;5GG -L*4VDkvCKPJ+G&ZeWjZIjv%&vJ*#a2x9bk#9DT4F17Pj!$`itC{yX9u%=Gx#XJzTfC;hB~ -dK~|DLgJ54{W>D?z0-a(EmqLEXSBM_f!8rTW(y-(8(Pv~dMvu -MW=;7k%&#VMR{H(mTyF2PbRe^rmn2s{uhokWD)zrbKm}7?|9%#4=jKJKn -B*(BlHdwZjB0o93#UvI{@hsfMcMP4@rYxm-)f`n&h4xF_zT{{E1Z`fuMhH4y^a2@^?<&AIYo5_pHqi9 -5d(^KhQE~|r}kjXsPjkG$7e;xU|OxUlYF7$L|B6|qlxxC;Mkx69!Hq21~RbXlf>nAhFM3LcB`#;c|3X -?GKy?{^ZS0AuV_3Y;9BADRJuH$74u9G6$I>1D0b%va(C@XBab`E$R6imUi99o`zXRo2-oM^THV6Cq)< -uRlv9yP!f2i(R1azsz!Y-yA|92Ws>CwxXrTxsU7Aw81&?*ei~4k{8|E6m{If7+p-enr^Q3p#CQ=eNK^ -GwDk=?upQsYn$?u`nmOFM99N7LDSRd?0e&{${}+)I@OlVfSV -&-o4Q3EBol2!vY^DxY$c-vyx`H@WMw=MB%*02LV_V?yaPNGlv=hvGX!&n~+jlF*%XObDy+Wpq?xxaiy -2N-d_m>R!-_P^t|#NWizz&Er8ZMr#e!<(|L%x5I8-4OTcgR`8V!sS0?9`4++ -3n|#AeKY>L}@M`hg-m=c(Oblc6(gn(n+xA_xui_TXYn~uHWo&x@d%|l2;Cf+5t+TBs=><-+(RpxPk~FQ;jCoV;!w4181p -MqMAt;~D(s)|nJuisZjR*%cZ9yb~s9@&1APPQ?m`aO6a_nj`vpBiUUaEBNc$xsMsMjy?lAt2mvZq(B8 -d;-cfkJ=5Axiv$1l6%-kr)fZs~8l ->=zgH-Ix4!qK|~IqkDuv2!fyy93s&}(i1{S0wQo6BM=NCDH4GobT<{>twcm0=}tWU3C)iN4)jBXkbjZ -Ngg%i|bi_J`$mj0e;_v#eJtEp&M;0A?2Q+bv*&XrTrwFhgjH#pLm4c6h -1fNk7AeByms)?g1eFMHLkJknrM1Tgras?mn|mV0X|ZAjh9Qay*2);iG|N_k9OX5q6X?_B|a=`A_|q5F -9-dQ2C4haxnGy)HY7X&zl!y-I{G5MvBZw(aQ% -#R!TWm?Y>ykDL4@7~tCX0wvEeeJ$IbhMW}WI=V#I{X_1e0xyi@88fCDf+J{7gT;>t@K9Uf}3@yZfZbu -gZsnnY8JMe&|i2NykDR6Rh2>J_`9`ff2-s6{%#nvdUb2epCMLRg_k3#=ozi~sOAlJ@_;!@cM}WMlV-e -&l+Z3$i8WI2jMv~~Vd+Q;lQO%5_rgwNK`f_x{}f3M;#yB;q3V_agFehI)wG6gkIY?)_B$q@j3N$%nP8 -`4yGapB-wBh8{duD{0!%^;Pp-T9ed@ygbpZ&<&tEf<%rYWj*c~fwIbn?1fU(zvwaQ?_4R{Py=yW!mjr -Ms3>h4{&#%Famf%c;SFk{A_jS=Pk?#u6u^A;e1)xg4jfS2?k>yNy$m&HPFID@d57#sJPa2vc@qJTL){ -T^_O$(}+PPwtu$49M;~!dE{*&3xBeG}m~@Srdymf<`+@~MQ?vq=DS^zr(6sf2666Ajyo>P -h5Qx`#HK^Mie9B!gtr2fZnd4aJt63^15l~;TPlwO##$s)Tl^>A=1(t8Y(0k;mZ@ix((!O|6FWa%BjEuX7FaLN0w@&c4U(0XG?;0!`^B-jPu8v66D*cb9}vuY -glSH3*Y6d%;(c2)c{gM6EygK2zT(6m(ziY2mCTD|tEQhmQIK5V(3 -)!(Z=i_F_nhR*dmna}GP(40u#vW{FB`)11{yI>Hz!Q;mE`+M4%sRN?yG_(+Rle7RO{Y#f=O))=Zq!MSz6UoYPA%`cg+l;}KQYk`9y3Fiwsl(A%((8^fNl$tvkS9JRPQk)tsgYIC8HbLr~9(;v^0+L>3Fy38 -_97&oVQ6UpQZu)gT`ektVu(r#mNSI-9VARc?v+iX&L77Z@7Y#a#A_vwaIcj4Kz_fog$Y;q7zj5DxBJN -G@5Y;1rYgq3wYS0K6&O9!RPV-b0{^ThcL#E6e08#>C_lF^2JSzEU8bYTe%2k)O(R@;It$+hFRarX5cdGfof={E3r(J56pj>glg -Yc4P1%qg23>%!Lfb_HlEZz#Rr(Ib|b3cMPfJ=nYPzThRkg7$Zn&*&~dEELGch!HnFKGavKN}&=r)ZR -_`qX_o>g=-bQ~=rPy8DH9keH@N`!19njV0J;Vrs`+F?QfGnA-30Z=)( -pNFYUcv1K^I)JS0JHpTrKU?e*#sNzeiP9&c(G&`7NsY<=}rARmp!FRTV>dTN$fLJnVk5UT=H=FN2 -IqWjhH)SPD7td{Uj8c`tE^-HQ*KqoAX2|4sx~rQX9ss_5T0@j*`C|Zv#%&%u1XluMR_F95h)W&)J$$|kKhG=f%Mt}2`-vJ6mY_sTX8I_N)?$N@ -^7C)_1(CGYUvT&wGeE3kZt{DfeV8;aS{2m)8~iNP4df`mWZNJH>j=BY*Jw+`O#*B*Rm<-EU( -bqe9c)sGjgy^@0179=TLTf#D7;6b1CyRy4ZV@zm6JlR7L;3W_#|^qj4&X+idBK^Z2*;-=4jtuaHx_z#cLSXMbmD@r) -b=RXt}sz8jNpJo|VGu#mRDRL2GS!>W+R449yDQt-F?p?s#&p?xy+Xh-JT9)eMnH9R^VD+$I>9$72-E4 -*rJZ^Lvfu<#D>lRYAKe+5lu&Zq03nu%{ZPbx>Yz3|x;a6TDKwioJql -5J8j{+kpmhd@M_HM0f#*+B?LAF@I`I6qnJ|c~@oFrb_L5lZbizo3x8zE_b=n2FA?+<&8O?;m2)LcXPz -Au07@YE`v;7-6tbugBe$Naj7CwH?a`9Mu -OOzhPdg4oE0zTjn^-+$1(4T1XAn*KiuYQ02i8@f%m!{SoUPaWWcJpbU&B&in=%a%1Nn>T7+3_U*j4fs -Rm4Ae)s6zniP>;$EhR*%e{ptrG!jGT#&E(XPO(T(`C-+E_<;UgW{6nbyq4PYrvd|AtCw-KCQtHT=5%e -(vg%N)nRbAe2t~+?Z2_mHIrYhNBnKVo{Gl(G*AQ(|4BjWlQBOi|^|>+KJe5CB?5uAg^c0$=2zm4 -XXYEgoj4z?mGazaD(beIN2~MsaZ3|t%O2F5(cT(+wOLNwd<$^N>q3q6;Couil3GZ?-x;uoyd-Z;4yo$V)7mB-SNFL>*-A#KSOMJaV9xR=gQ!#A)6cgc|)TocMdj;T^X!wZ&36~x`sM4S`kJBidky|jP|qCQoLo2oJp_6f -*!n!$$C=?^-_eMFFvOsH7DM%_=h1)#RRHG2%Q>|TeLuCd%)Z78RNZlFRLV!;-T -JSP&s&6SWKwSkr3)ldAvx8*H6_h^m$%H{WXYWNKp-=P2~;mnmPmBW3XisEV@+#EW45k$O&wRJtRGFT# -|PVabEr<$!YAf^tJkP>CbVAwRcws}6zp0bymvJ;6B<6f2UDOh+7iw)!yOWNw4J2hpR ->s4i7c_S`_#ajzp<~elL5hHi?zGl5_Pku6O-U_&{G=ujzq(p?6WG|#3+A6!K8KP^!?Ue6?5BEy;wTa! -Ef(3@L2pObHbbwacYQ?Y(jQOJLOHsZ(4#FXj+CAsbkzB-Li1ecZ=c`?@J!me!7T(a0gx|HrTCXaz15- -+_>G$k8JF`LM}Da7SX#4&Aqs!Z{Lb#^iOm;UGZuVl2d5=#`uT~0Ai}sOA6e0A{zx>2Zz{rGf+E^zX%d6trm>;XpdF(UVGbhlPC -w?k$1dd_+`*DjBjO-TCt%$_uD2T(+{Bw#xsFAnG^p&5V|Z{d3$2LJ -FN>$tynTWLFsz`zE>BY4%VQMiU(9}N$kKnw+!xp*e&H?u`L}YH96guwJpZqBKbGvr{#Wv7zZEO_PZ#? -tUGn3Fe$%;$qZE!}6ih%A2JMiGMkxYAArvK$9eiOlgkv!MJ@j<|Ch9<62cAKXR1k$6MYv-Gh&<%Pvd; -+k4uq)uCp!Hu`r5%7_TlE)fzOVN4vNv}$S7gZL9c`52PDg&5AK2vl295wn9cF@Cp7;7`q}~E4xf(174 -ex;M-L_*DmmJ2_E#PkppU`!{e*pQd301A@y}E`jD6(Nk)t$-(Vsu~F~}5sgw+p~(fvm7KkamB4x+n`_ -yv8LXIx9JQ-Fuy9%7IuZomh~{vPE9$2Pz>(&?VN@t($NUwTUlO|!(Ni`=92(Y)I?y!sUmy6iXz1wM*M -$4R%(Nv{B*C(S-+xPHMw6Cd5u-aJNa&McdtI-Im#E%Ockb$s30es1M-wE6mJ$92Is$gh*ppB^dcZ2kT;f{YP@g -U%k5-JKlJE2IR$TwQT~DG5vuw{WZ*$Rk1CypKNr1RiJneS#->K$91zuA0`xy&o&A?2p89{WNjr-ang} -~2onW7VOVeMQ6t@y%YJT3JAf3Fzk!X)Y*HK!!)~7CMnX!=`#_$fS|hWHdR3_ -Vd(DWgX$skmBsfHds&i)oEIWNTh)k;7h}LqEjnMjb@xM7*Er&5Ive;ib@1czH0s;#m|HQ*#CqYKgo}d>B%yQVcsFRH0Ty4&*Hp7fvu#cyrQE?VWW88CuwCd4@?QNS4?)(O>I{kjDS5UQ??uj}lepN8EPi@a4xXCl`3`o3Y -p1z?~uFWX;WX~B1p&1rTqn|7D1CR@Si#4jHgGZ15Epg2sx*zm^(kcGM#s1tW{#J<^$MA!doTgw1qHr7 -~QJ6+3l)`9y$DKHa5afq#?sra+{PcV7UJ!*p6`wE3i1aA#pxI9}eRR+qqqym3IQRSQGdX??6ht2}utS -H0I_7$lkDuKSLeN2uvOBdj`6L9W!{P2u3WW}Glpi?7FBP0m(FjWpV(i@uLSJ4+90epe{<))bcdhYJyE -!r``|3wciuka=;77$OhClNg@NtjWhtnH9(iVT_6wlV-6cgm%PLU8Iut4+2#ZxTxq$?^rk{qM5_xt{x- -oL%O&1beRo(1?%9OB_wfd9lH9-aj_G6etBAs)L1{u76I>>BuI4pE!a57}Sx1im~~nPu#Z{+2>IoGK2c_>fN;<#IS_VOj;C{M`SoSfLg(Bgx^G_gSggyKWJAvT{%59vfUg)njU7|30ZAjJ2bLh^*FEa+bzN|c>_crR#EE-b?>s%-YPWt(q -4^($7yo3T-&xV$F7j&&iXk*gV!Ne;AdH~N-GY9i43fqn6oC)~-+%SpoNk64mWYau!ZnT^P3<^xXdPk1 -r%Ih3dDA0j`)Q%au0yGg~PkFs(66Q2KRR8f7Ris- -8WWwv3#pNaL1&ZH{Zi)3~L{@p<`w)k&F70fJswMOZ$tNOJJ^GASK#V|(%rV*5^fYAkA<}S%Xr0WbV~i+8eEuado -U8{0x%_(~pr2_2%nxk<`=JfkA?It$bzNV`h-%kJIZt-ypRD0gobRPhTaCNsFBw2i*B4bimD*Vd{5gtZ -`!ovK)Gt}ZHKlTxBh0$}%sw4vQDc6aLE(>Mq;IQyuMs+H_q1EDh?W<}Lrr)R7DK9$BO -@|v#NFPUF=r~SqNJrfWl1zU4?OBPd#`4&QoFhnr}xi{xVQ+1b4#Yq`wSW+t_ypU7!B7IlBilGndA|Ry -Gqdjq(rn{h)sd}N1RXGh&Q(-VqcsXS>vn#QZX$ijQl1xQl;-a{Yuu`$IJ1~G6#A_P~Z=Mv`BqwBbEDZ -k4dhkhet<1k$hVv|Ncdcg`#}-{2(rzPI^LqKJR8&>#E0-$W0eq9umyeh)&S-47x(OcHw>fgU1+6 -owKQjD0tFIOuWGBiV(27#s8WBZre6gIYTlhtk7W(ZrEm!}oyT`vcSDXC`@fK@@#BnLX&(eIJp2dcuf< -x^eeZ=tpxWr4K>Fga0}``r_&A2flB2cF|{kZFjr*XIlDbq^FLCu>JoB!BBSW8AU!4iRe*0JvzpsqvpA -982V@f9nlW`(;gimpGkl$Kbp+;mD4}%5<45m>{xR9>ibyFyS)u1>p2&m3Dbb1Nl2>S1rNo4D|q%qD>9e2LBesZ#9{jtgl`EE-YHj5~&zz|!}HgdR=q>W~4ptp4Z+vx;67+|m!oen7ZmfZYJ__e1&nLr|9zwR^GNH@b_)9stmZ#j>33%Hn^k -^eH!&Q>AqdAQnj}#YB1w`)ce{D0lR+>+z!-vlKj}h!=6sSv>Ga_6&yRF{j2%h-W9sM&u><;P+Q5Iro| -dMEy**^j(3iXv3P!e026qo=8VTXlZ<_-yqI%LmYfHyHj@TxJ#DWgbH>sE&qC2|&c -G&NMhau}zP^=>LPniu^U?2Hvdd*#Em{xGa!)9)0C#?&4w1}=7r)kSN-+Xe9GGnMELm3i>G;!ntuQW{d -ND8+7N5~dng)rVZja!~XeLmlm8%oxi}ewi4rGv5g!u{I&Zcwmk=feRmarIqE*#yahGOJW55~f&CN$TM -H0XQ+&UzZzk#okM?LDNCS|QiM*;<(Q?moR*iS?jsniBd_ih{I)L*G@90&D%q?`7(GE&;xn+O~64v>r2 -j#;f5KyCkmK*B+x$hN~_6rc8a$i}nQ0iVZ=nFcF&(E-8Is8BPOoYxQRAgvznH@^MDyr;Arf%xK=u&+( -=Omf}WftAO<5JcD3S_myFlW?sDGyfq0k0A6Z{G8u0)%v?yZ;;Q=<$r|r4lMr5R(SNBO-{F|krE8ZDkMzkuHa1ixvK&Jh>k>oNJh>!H~1@jk$k7q(G85ylY@osn{ -jSd}$pTb^k<4(zh-82ZN8ZXS=yN?K9doUk>=c!YIAS}j;b6}jf!2{el7*uVn37ub($HDGF&$=%$h5?vEk)O-jiIp;o{Seam9?oNROmv1dLVSX7_kiF15mYoPWNESm -{4`84=EEcB{5XzME-ob)pjzWmJ@l<2!7-y1cL$4y^;p`6_b!klfRYru8<@nRlgG7Gu8RgDjad^kQ(GJ -qxnsvgd2zX%oKKY5fpjR>uci2O$48LfSx@al!S`;LNkfJL`^C>QiD6?a` -SBtWt&e{Tq3%65CgVnDRUNx)rfnYaNO%iu5*;q1%nthRWC>(Y`uBfU;02hrzPtJU`v=fJ98`lQg6rwaq8ph@4dS4z&sew-(l410f0-vq -Bw;e<`9h*9iRW}XX{NO^oVEpS)4n>1W0Zl;~oZl6SJ`(3Jc{&0o?#W#vi79=m?#)M -zOzq`ZN=^EaIgW~J%+=LyK70p3tl2l*BpMcK`5A`QR7{iucLynIQ=8B&|lx0G+?m(9DX`cO6@9!_nUB -D4!%C&AdF1;HD)^;KOZvINcZ~yxsc4brFdc>&Gr&9eh%wrBnhXV(m`v&J>2XWXhykUQKi@=}VR(Jx2MypFhD2VT_yIh5btPtswUTQrbPbNxq;zpq4`3%sk1%ffj_a$xQcypT -arQ1VRnq#L`|CxSYGc&5KjdilT=xkhSz2!_G~PT9b|#T6+}+O-OYx$BPL$7CZGZOm!eACM9Le9X`I -6*HJ?e)`(cp2v9`tBYkBxhw>^6O$ONc=9!+Zn6Q#cg2Cv}1-Y$=(JxLG&|>3c$YXrjP((&Q`RU61yIH#Zjj56DQPoRBg**n8Fh-XCjG55`6y10O(QPNoSBx$1{4+~w{alVNS!tzxwgH8X*z -yOgS+@Vtx;b-fn->Gdr~f4JesEqtC^WUNe-N;3X*IWaC6gVXMs2Vn_>8`P!Bk;-ix3?rb3DS%~OR;Ok -`CCq+YMu^dkn@f0u)S-_(P@0}Ma7kQo-(jrfVctrxPcj?24@qHCS@Zh%>EjK}$QW!@bW+gH}zTGwtzKier$Ud7|d&$rQfhQRq%I2UE#pji -j0xinAdeUZr|n#)p{m-h)ePbEoM;VLt%BgGmt59>`b-=8ig}*J4HD)optxWgcNt!^p}C5Ywy4RFtq+LtaF -O&U!QX=*LMD;XVd%`>TmGjf4bH$F!3*~^V^9p4Bz1)Nnj)eLpViJBu>K=3Gc@V0w<~c#~ -PKafj6AxL6Z!zKNPY}yMbXg@kbTIA(2u$m^`W`lPoO@yl)#RP3yJ+iCr5dP`W%Vj;|P%+a56!U-hd?D -0qvjSMD%D0pnla7ki(N^tkv3x50PAIu5!7{k8oo7&4`!hX$!wG)-NFHT;ol;vTEonTjr|U2q%A-v4%GeqT&V-$^?zmx>v`;zY&s9LrOFVQ2kYSn -mgs6ZoV1{4017_!%%VeCzu9o#c?Y+#q%FIX|8@J9iuvHt&d3K1)_V+i-nOw` -&=mG$ZOpdOWcJYDmtQhNQmUEfvb6YZ6h{qJ?}SqVo``0!T&Dx7uIVmjzyxH*}qEvcf3F>+(poIshg`a -)Z{1b3zlT;2uOhQyP~I-!zSxX^w*1fP+EO5OYd}${fjFs&K2t5?MJ@KkXQ5TNI8q#AC -4wBoVcW(c%5`?zR=w;0ugLt!H084NhO{dcpbP)7%i4nllQ5j!k!ijLL9MYs6Y*gk@HD6_D=rA%81J=4 -GtW$?hyXkitw?TKy%mgwvZPXorq+icdXI-Nt9V+qcpra#k`l-;Kq*iZ}l__3Vf*9?+F%1%HL!3{WVuy -orO-aId58U^8-Ko635D@HCs4kI-g9BW}dFa(@NKQSe5}UOZ%RB2T#6LT}Z(U&91%i^2=O$Z8Dg~f;)m`2?yQ* -g58|iaU#I%l2hU07ps@?fOMxT_|=rJDa<{aeEM4O*gu-hHNIa}qFWs$N!X3j-M2rHD|qI7%HCm90Z_NNP|fg#&bF;Z(@hhw|J$Zm -om*v2`NnsYTt+k@2!1vv@2Im>puwA1zVm<#7%mhx_@MIp6^Tvi#Q?C1G}zMt3T;@J;T7GWQqPh9%z~(CrgEcXkg6;zq5|P&oBW;L)H5kyr -FA_C)d*0AC7eoiBUbPVO^pzkKHDJRBkfe9#Aak5DVmhYg!Wc -A{z}Za0#*;LtRe)Ri1Uzjtf0_H2E6k$F@uL~Z<^p>5s)AM3n@$wuU5xx`V?m6$OjcM>lWm6nljd`uY% -Lb>{8hi1s)ydxbRN3btt_BIy~PvGNgR{sbWkHHce$zmo4GgJb`x#31@C!^ysR3kvqj(hcl1IC5TfUfz -7dTiI>FNyu)LhM)6Q=1Q>Q#UyksOkKCz^T84)vPDB{%7W7hBscTz(Vg=^>r1`Ke)W%fy_y=ue3>!NyxCQ;ZAiQ%kqhP1R!}}DhVCVMlv>BY(>cA`BOu>!Fxvd&)Bza=J4)_bih^8CE9tXFz&~ -OKV_zpDFZL8_tHV45QtrXoSb$#eqBKx!GPEifx4P>rot*^m@t*{)!o04;|Wi|hjNKPlTy#s@ZzeE8MH>#(2DH68Qof~M&B7{^UD)5LF;h5T-blF@ZSG;k4X|CGwQoYU2J}jwuJ{F}}ChixSKLdXF=@06@j-z@lLJH -|m4@Mm%koh4*WR5{J8w^k)70b>!zej8uoGCVb$ecO$>9g@B`{746rQ2Q_qEadxkAdmu -`vuCQj+&J|w84zqDh~N$0n=+&c?o6`gy?wQwqhvuuE<7hOXKxT*d?c^V$t1$9;{$IxEmTRw}Kmp`?8c -zgQ2Iy`Xhv%gPu{NNNCMb@ka_1phg-~Mm2pP#ZS|LeCK`h&s#*Nc2w>OXnaclH@YZ~{U}m_jKS -rf?J`K@y`t7>0HoJc=Mlia?2k#rilSl<8CcWO4= -golvN-+@DD0Q9)o&%d!CO10&{=TRthQn|BEJubru+5EpmI|tUjO74K2E@wxhH1e#KSjFa@&*NJ@+wka -9-`_#%$besv@*5z?sTUbJR?FQA~YOp?2Te-`yGTtF8UrodJq)-}(f;r7wQos5RV(I6b5JLGdvVrMk0@ -rkIsP5J<1i2iU;Z8PIjv4<6~*lxY9>5nRIpz8$~;kQI6e;`?Ig!SG=@_2v<&l$|~# -c0{E+Z_IIP!zMa)37*m2n0x)Q2Iz4uBI1UGeypt_z+6vu@Ec+A<1*LXd2xhdxTdbsW(fuiAJRV+Yf~( -)G=mPg^YuPi%5!(8NgbDd6zTr{6B*P?SXS@*3ixb*iP?Go5=wURZGunODNiF!N7Vy -evo)eYl7)s7Ed;Fh@hMCTjA~|g_Ukp#1z}{C$x1v#rvq%0Nb*cDZa5Lw|YCc_pegxUjGtUp0-qEV0<- -Oo=}}p)*A-M#n>j1qqLG?ar%*5`>)Iv^#5P8^@pwfeY5ow*7dif>l9?-n>-RxqZYdP1~Erubk?bwrV>hcMB#rQHUZREsea2s{sLR0dd0J%2antN^Jm~ -rB`r(o1U+Ss*e`f<**zWgOm^NcYLpF;ysM>_+-d7zwvF2)I<8af15A4FA -*&eFPtn5>4$uZP<^&>r^?G3p=VJGD24D~to7uoMLJ!LUeLfe$)61kOie;#TmZx?Q)(@rA#=b<%!a^Au -sm83_vf6ogTa5<1r;YHAH*TT)_tdG -}|0(@g+uMzqRp;qj*8ZMaH*N(TB{Sv03yYMgtZH*{sv=xU|q=xT=oYit|ZI?$YrNT~Sz~`0NDI+M(Sg1)t9kldIOWf~FTEdV$r@mOGunvFCfh=8x##McFWU1K6 -K+34Q5e>rgJRsL^B49p$1-Lt3OTTvocqpgsqOXWx$_+C?mK99v%TMq<>$1cZL-lek69Q-1F -s&1NR!Ly_L^#_}?N)nG#xvFQ-HzN3)Xo{b(G=!`r~B&e^vin>*xB`0gQ8lpg~PD-y$hCkCA<9isYmtRXSrACHXZL8Unz!tfSN=i -%@Ib5d`wjgY?mN2aSM;)b=QE9Yh?)$iA%b791JQR16N@dxlq)g*AOTI4WBo`oaP_AxUBu}RQ-DrCa*OFW3j3xrg_E#j_2$FQ`;`UcBi0z5T4BWWGo(J(j7 -%Xg(sEEdn)HQw5Xg=3OsRLoq^B#c^WpH?~zu+p7iGkx6KJ>;(On27-!yM#Vu -3Mv)@-Da@Mmy}9{rPkin;e>$Gda_pix4z<{ISCSY1^uS??v$j5^mHn|FFsL3%t8ZQ|ne=^j05pDBcQX -5-$#Mh1TLUsvGqrs#UY7x6@SrZ)@urr -d`44Hj9oKa$SHhg#2@sIexJ9@UE}#Z*e^`$h+wr+#_LAjRj>(9`GbrZN`w1LCjp5}J;1C#Bjr_i$p~A -}XNq}65Zwvc93FZ6Y$lMZJ^0xUjx}Dq8c;w^$P!+e!=pt80FG15Vbb<#)I$UoCkz|qu&8riAbtLr3{1 -`)ttbdh+p0`V^Lbh@NiawMi5U*>PN6(`9=1*ST-=DbC})L(@B -lrsuWQYR7HhJ=bT!{%O5fx)0uX6AgkV*Pr1z&XHErn6T^tTwE-~rxz5wMX2w?zKPrDD(%8j~W?Y219CrRWHUh>xe|)Z$kdOAL#9`6qKAr4`nE8C~4gb -@Gae&z9VMJeKzR3x2>@|LDX|RQ2a4eTS(Km?B9S0uhu#aRh~tHPnPxn6-_NBq*FjC=~u_FI$LgoM&Rs -8?D$V*%M`$rb<;`v?mR-2Sd{@_GSck89V9ttYO~Yu&^~q6W7FU4m -_8m>~J5n#MYt32a8Me=Mt~%W(mxXg;~4p8y=;cU(4U0gv#~;yu)58$tAdKLy7JF3?Twu`6b5{Zeb -h4@ruxvaXAiEvX`0<9UVW)F=vz?%R?HP#mD2K2XwWC%yMrYo|I-}-KW^#o?C77gqBR!NBv$Ry<%2YcH -7H~-Nh#i(=t;%B*YM_NtT7Fn#tP9eai!>E{=ekwOTjT|%ABhS((rcXc)N7G+BC-cyfE9W@t69Nb$U1J -}S&<8feOt5`L1CjE>8hL_DpGZ_R43glQ=gVEj4st54#O4XARx3`{urU)T$oYJDxqtxzn2Q(X~TsT6=Y -rim_b%T^;#FUVShH_B6X(AQVjb2uH6d21dq%M?X;LlTF{lHplKN+n!#Jshyrm6Y6d+y=>w8HyFz@%S; -pb@aM<{LMlkEeUVza{v2yN8woqa17%gaYr%AZM4E&OrulM1;)NAA%^U;YoFwm>&g5@SE%d95$+0aWFu -)8~Tm`E@6I%2wAS<^e7%?U;D+ac%bSLzkSMb-UVVl2|xEWyKlXbO9dz(G+p&zFJtwAhZ9vR4pQT;T#< -)7Z$y)3e3bfY{Un`KmGn*P6soCP(ZZyTt2)GsHaDOg3>R6NnD(I6k>vnfevj(b-#!JCe@0q;lvK^tI} -lf~S(msXjZSrh1p*tE*IadWaZ;L-G-*h}o>iI^rJ;nzZamh~s%HK*p!yJq*GS(FShfD;iW(;<%iqXaT -8;eZEo?zm=y8L)Wpva*Y~YxmhMmLbz<6ac$|iVK18*x6q?$2f1+tj!^54sz>j-R-c -FwH-Oxr>pv2b5YSa|ze!V{+9BWxkv}mCmF9mk6AX(|F=>*_yf!AKIS8Bp$DwVT3YrgdLE71`QSteF;V -bbQJ6%kQf8DBvc4?J|2l9-gkn;fPdxZFmCke@+^Ke8%*g#Zm5wo?#!t}WX{)Wl~W>krSSZAKpj~14jCc`0zI#&{Iw>$VS0eTucH)cy)+uw8G2H#>GkmP(D=&qe(7FM!o`2&*DmkxkUMuYZWG{@%HNgjc^f@2?RSL=h{v#Yq&Q5Co^lpCK -%S*mGLg#>-9j6gRc&Gj80BaBB^N_Gzb4yzw-Ck<(gVzU7jj4a#gYy~$lu1>Fmc=_W?7$vG%V0u;1&l3`mBG2Z8buLmJ;%bSt-p1}(Cd0)|gh`o?NVH?*uM -z;;Eh>Y6g5dIQj`CASj#D9vg7SJdE1YxQF5Mi~G{5h6k7QRPV*2ykHu_`4&tvuv5M#Z0@tO!yTW!_L0 -u)(bF4wj7iPj&_TY+HYKR~yp$mF)13NDG!8Ci&ot5$EFT3?PiV)7%~)=g4v-csyMKS9(Q*Ey?;@7GlpC3{}LQuOAyPY2^76-p8yCNAE?4{VE$4O|~3L_rXG6U3ndEaT+gz*P&IiZre9*4uvRxUHYLOeAu?Dx>4UnqX<*JeH;tUdiuq2pk8W4a+;c+b0)C7tNwmJ4vzr>Md7(bj|*(g`fXhaa020Ev-csJ^Y -pZWDzG#zNSWWJ!KLZwve-&Q;ub=Oqt!rAW4*v0IbaG;a|GY)W$h#)^XIGQz{XF3M^he!+fRS^h9P -Fd3zjovq5a7fgWySNk}W&IWq#KoMFVhgq5MkDH=2=FHb$Xty9h@@?Fau83Mc0Us1uCe!T$xAjI;*|d^Up -KW_<5+f1dg7y(dG&q7OShip^xLVQb~u7z<%GR%THpSH6M~QsmyS6cW!szXp^8$4Nn1a^Zu>)pz)0oU`J*&nF^ -Czz)itH`pxc``ogo@FqnBHSZ|_weIk?a~co>J$@Xki@u~$AUv+iVPyT2ne>e7Q^L&>3o?SR_-&tA4K~ -==n)~AuJr;P$k`|Ya0BI#q>P{V_z1t?doo@^WM)xlAwUor_K}Vd53Q~}^pAPc61bt7o2Rb=Ke;aA>P5 -;qFwWFM0&};wKheqp{*Zx17;L?9>%m^b%e%n-+l8-0;t4aD}m1IHnzxw1=cgO!L`oDjJL6G|G`!oO7Z -|RDwf{*2wB>rvalHb~|`}&?wjIM}~LNNGes1OaeGGRpRQzr0D!5fS=00kyHB15+M -U>nyf*vgF|{8Lm&?q|TnE>pV;Y$L&j*l>L39LIYJbst*4qO>sFR$U<-v7)+Pi4ma4&Vt+7B`aLrGK1- -MGPX}{1>WUdRt-oL=fNVm2e}xM9>xK&FuT5sWQE7 -_&2hFH%Zz>zh@9DF^#-9~R+WMRu###0de?Y2E;;X@%(f7Jo{V8B~g4D`4z6dcn7zlh&V6nk>^)VFf)y -z+Qyp=~?Wg}}KpC>A8UdjeND;$kU{O=b5zFZ}{7)%`^QnrS>U?5U+;65s!WuOSQvJl3)WTS;}AIOk?E -05cH_3h$Yt@RV>RZ(+W-Ah$^?(Gr6Xr_N%?1o7!`1{xl?mnptjjT{~22|4mKhl8oru4TRg!W_QA8**t -?icV+ZrIQ67w}JR*w5}4@K0{o&+ZrSXE$t{*zs2bJAjXg9W0(-{29hjB{;}FZqm#H37^WVu=U&P4A+9 -@oG#bn7_do5EuQYl43~_`23pzyCQFkwz|9J7}Y>H1FI5;()0KJQ=%a7?_6h=J;Edk=t)fEF`5T++Siq%!E6?)ny-n=+ePG8Q)ti{| -SK7PZX|dm)QQ_ktLyTz>5rKWOLJ3aq%(;FpvMJ<>+mpv)XsZ2?NuQCaHc -@%MKJ98Yksza5lSfAENS-VH1Y#t$Ae=)X2$0f;x;<9I%_M$cOBq(Z#N>hp`cqOaBp8i!p*6#?w=x -eB5C}eF#355Jh(NfuRsI^H&8)^Unayu@b+M1nL=BtDkZk!A{lHv(2YI6UW)hyIsxJBa^QxC_PiOobu* -#ZrlN^OTfq<~HaJD>-AwpCfGZ|8TKxLd`bNTC_7d9pwB$Y|b$w-mZN&O@f9YS&+F}nIUB3M)m88;ga7 -m2&o~p7`_I5D;*vvF8A5IteGq3x1oG$QZUiZ(PE1XVS$h7^ -*p4M)}mJJdQd?=kF6+WJ2qt@`4e4YZxZN@^|>=$At7u&(rA|tUE)D8vgw?CYHL!a?^s#9;*?AM_2v#? -y@HDi{H}ah=q~O?6N47TT*1?)DCa`A-h9vYPXB`W!$r2Y18fRT(i-x{Vm?N7h5L@4tuLx1ks89&ePjB -*KU=Hh``4@gPTt*=kORkmc(Abkqy~xqx)_*j#9rR!6qA9s%po&>aPT)$KVuBc>NE?0h9#PjbaRebJhT -J?trs1K?9|Vs;C#J>g;SkSqGZ$bG}E1vwpTxdyiOE7|7yLN9$;p@X#)<0tjPDQH#I+$25)qQfDaCvVk -L26V)Y3%F;1Kt_if*}XW>D7N+oIm$<+y97DAEaFC3$P(SHE!9HUEIV((X6CU@ykIov~Io)#}uymdbR( -0{Ct}+XlC++X -K-rlJlDg{XUFk3#s>ci)%;`cyEMHU(o;;gl^f -eDCpKd4B^+x!U5-gXJH$_*Xjw{%VVVwL@Ur;@?-M<6PGnRh}bL#%^rJ0ui9oiLPb`O}HWW9qin!5Zt8P6FzBN(R+!r^eG@O-|58N|A6&Q@kmh|=kJ#Nh{+!Pt@7DvvKG|AzKu7p8p|<|*3p(#?9rSNhPL*_dOAcTr-&bHxd#F13NHeC;bzNz -`kL9`sB@NhX%j>&>oT=l+>n*1QJ(bwp(i1%lTx?=w3aTrMoL5#ynJ1R`hs(B>>QS^Swmq|{#o^oV&v` -8h)dR;Crbq662?y_=7k-?A -C*#fLVg7?E$=5;$K(nqm)Z;5B$0RFj=bAG710MKjx+aa$Ux$J@j;CT?uw)XkWr2|UKv@M0j-6X*p$R1 -AvA5n=9WAJ55_PmRm-VS!;gDDJQ{!J%F6e#os_A+lz0-7nnI{Tm{n5G}%;)+m14?b_t(u)5boexf5e( -i9g&(Z|Wvyr3FDKm$k@=(xIak|S>|HcmZw}dNcNhX?R!9W!M%L2D<7mMi{jge4sD1LN*Z0>0Ys8vjSh -y2;Y?Hy@g`PdAh2t%<>sxhxLeIzN3|z5Owq#&PE_Cn;$-=#j<~Un|nTUqi9Q3r2?QtC3?i -&H8I;;ImR#1I9%rB3BN(j>6wObrS@lA##5uQ2*#svl2H1ii -S}@YCfmbG-$Gp*_p8$(_U3(DQ!7E*Fca2`9p?$C -$RQ>Et1Y(Fd?596?qiG`zw(%3|dRKDU^6HA=TJ?-x3bz|(T@IvnYSzYV`M$uRlf9LY%JWd44B4)f90^ -!eNxvwyr;Xon{LfKq%Dx3BIGa>xH;i+;khzgY15F`)#3li2$EfN+c?Q3yq07{Vx+ASoOtuoch|82(cd -O0Z9-2-hGU+9qLLk -V#Tz&bcF>jliiCpisw5z}6)NyOB2S4eh>!NBR2oS&cd6UMzOUtQ5zPoqk$=Si`6v(UZabuJ_C4%E<;R)3^F+4WkQ%r9 -WFceRDl-^Hv7jY6=SL+o!;Wus$9)Ne`dnTZQ#LnHgs%%4AESSOZ)gAL#qV8M>fVLLztv5NWI2C~| -1m$0tr%|+0!0Mh!1rBPs-)b$1ay0t9lM6DRt9WiiXlxB{TW_Y*Y&t{GZ+yF5qt2(&p>v2OwAE&-@l2+ -*K1OrCwyJ-a}*HpJBc`EzjSCZzwFTZVGJJ3BnWWg+q>KcZWq0Z)7Vp*cJQcB!fGCE21bYzN~$WUU*48u&l00q8zcN}_*C>avyih(}~7!W_13w`-Q!1y3vcz>k10zV^P -sQOV-Yc)Zi*Y05UwGd%9YtU|W32x2DXL<4%h89`%Z85+Ht%LscY&@P(4rnsho~Wk}MCdMby?8kxFR~i -GvK)x)!c7Ad5oGd!9g_fU5IFJ+pOGJ2d@4#5tnzE4DW-2`2lvGHxu;o3mO@YYbeSX!$c)v^za399xr2 -PB=~6xA^YM|)R|MsGk7%2mz29Qc)?bMw#;V_o&GCr%)5U39^rZp*XkB#@Ze%DXJkWnW3(SD8W{)~>FZ -&E?Twb|^Kg%MSp(hs7JwFsDKS`4DJm{xW3Itc7G9zpSqLk?ebwyr8mpRe)Ez>+mm5=tt$ea^ogw(@1* -B1*`hqL045y@Uy5SAi9jUXks*_QZVJ!I$-mwX^^a?q}p`e@D%4`g)$dG~bHpwP=ZoyrcT~Ln3-A5V;GWH19U-=l)?5F=yE;TdY|a_k^Y --ihDRjFewrekJI%3Jz9|NM>G^aS({C2bv3;xnUMq53;dVV@c>)JkI=J2+`@?!S~4R)u~>k=<_Xwd)g) -dkk??qvO-o^SEfH`(o0If4D`Igc@h`OI)1g=eh(0C{wQqvC+?V2==qb4gGJ-9X=42=&?b%Ry?OTmIQ9 -7{df9Yj+-B#=h=8&b&{6y6B6te9XW5JotG`5q_M1$0*;pEtzAe3lP=9bKe19;cHB@d0TvAW+t^gtUeD -}w`*SR{Gi;jNyt8v9+c_a1<|mj46yFh)!rMV|6c9+OX(RIIZS`@qYCI3SM!p@nO{Z@XH(H#pHPB$bc{ -}y1%cbVn`$f3v#Vz*P*s-LQGbeOBme`nCWhVwkgK6XxhivKnw*Yi0Jf-lFJDm?o{np9!VLwKW|uoVTF -+RV4&X#y7=OCP2s1?2P@4cGqmw6hI}Ol>L&s&Xo9?8{^r^#XTxK6()8WAhi$e@xj83gUX1ZRU -U2H!i$bw@!dl?4S*9PdF1lZ{tc$R2&DvE^5eRU0%I05s_nV9b<6>!Ctp1BDvt781V_Waj#z_+9}uxGW -ut3B(3@T_F<;$D`ky;^>ybdPzA;mb7t1TX@hwddZwqBQmPoi_yIf$~f+y6x@=t$>GT{W`7wQE!fT?bH -z_q9R0^vV=Zpf7+xr89tegHxxWZZHj3}_#V!^No(9u3#%tH5wFv2f!jvbXH=qiPriV!A(fwGj@6n7H1 -6FejtRkWjy|EU-kgrm!d(0ZhZF5}$|{?>o(u9Op{f?^(RIYeThAa5o;DA2&0c}_KBKSbnBVE6`pRA>* -BuZ`y;xrQvW0kb8#KPz+EAf~@tg;AC2=WHcBrCCW`32#8HhoQdw6-8*GYa&0?-vj_}Y($elCSJONKsr -$)(X61eW$w$=+w`h|xJMdM`T->e5gDp!U2a&4>EyY``z#kz4q --`6R0;mZWcgGm|K9+N#Zpq}#pG7Seq=^>60x|2JOeYkL3J*Z3~mzrs?A+JrYYJxhc{Kx*q_AaHc`o)` -pU6i)rT7W^RYR?>ZXW>!YpMK*7I~PTPgj(!C-a2irp1gv4k&6W=A!Q+yktzD+XR9Wb(ozA3s-M1;54G7R0}(%xuvv0}&h7h_4A8d)b^JuzADtvbwC#BJq7s%0r>SaDQ50G(Tj9fG@FSxGO!L>|PQ+YMom-)2wY -UhN(H2cNL_0J;CD^Y1)O1I2u;CYymXWf}K2D?H&!PTit;3CXG`tb%m~qB+@@b$GXXV69wmr57?`7U)? -Ae&Wz$RtMQPE2hw|Ao~2s6l=@M&-bOJ$n@N*zvQ%S -}Fk+{;Y0fxutTWl2B458hI`(pbAA@=G+1G&wjPaNrUs{S&b%LI=l92sZ{PvzOIrrLozUS0ij3Cta>Rh -l7hMwJJLhr-LOE{)opc-f)1Qaz~nmy3H8MUu87ON!aI5@}BjK(B3?7>+u*CrrPyfN`R4ie4gEDbwKngeLQO@tB?m~VA{_ -c0~A4}PfRyWHE%ExTvOLeBcCo62xLbE@k`*WzT2NI>B4+a*gDz#IvkjUN+%mAF0lJxtiHK`qsVwe0zf -4hc`7@cIkPi$%7NN`OU$QM{jQ)x5Y-pba31GtIdN7j5Y3LvcO#FP0ha68*TA>sz3QH+2@N!O`LrnU_n -W=g_DyIg<^j0`(Bq0LVS!rQ&zUl78>#r8v>40#2SGf}DhR-E18P#_*tJ -m+LLsux)UX$w+3!ukMjk4RV=UQ5&{XHM_zijfr*g)jxUDhlQc#&k3R53zS-Z<-^p$E41oV6a0?HJ_7AMe#hLhyal)bQ?-N!qvk6O(KyvGYVbmLWj5=VmgSa6)dEtLY^Nqc# -gM51+j5ODWsntG&Bug{i)Dr0brOIQw{ASZV!u(#^el0@veE)+4qk&53O~e@4AWdp@4-9AWaNZ0Gl5;q -r9!R9NRRMEQHrsCHqY1C=lb4wYLVBmz8?R4yiA`JN|vQ}w7&LIPT#4wgW=MeUAF5YG7*YBQNbc9_CZ>P$x0?|aGnjWtJrlCh3 -J`FI{_Ib*wuLnGfbR|rfoJV;|-iRP$=jHuYF)pCct!bdQq%QCKWqrD+cWzcHUlASg@FI$%nj)M*Ds;w|EWEE!q9(t{cj;E_%rmmCR0D;M6Djy|pwZJiSvdotbhRj|E1Vr$Gzwp0!rY+5DoJ|_~}v -qwn!ALK6!7b3e!s~@#b^vTe_=#hB*K>5Ex(i`BG?ZqE5;>>*oiK0D3i@+#v1>J8M~ -7wvv~yh*SDl5nmf*v_L`Z8tLEimDzfG~3Z;G=Ct_%lC@m@73!ZzDldBU|xmdy9!qbx5CrX+d5;N_3@@ -d$1*DvQY+6rm%+xoYgBNBb>4p!Pygn&9fL= -=-`CR`Bg4E?TuVTgy3yml@JX9t4?!ORV#)XGiwpl~2ua%a^b8-2uPSx}QKGfEtG;mB~iEbdS^@;ALtg -ELP;Blvlp1%g9NITm_=L+hX;tp_l@94J5pE0)jn*I199;8w*!GJrZXG@}lusW7L^Vn|IgM*x(-gDK25 -&QW5jv`)1`O5801OILZZFlETskD0U%XmoZGFKu2?W6`12u;q?|MWG0+2#TAZCad08k<9(HiZtNwl$6l -3}R&{Ca#Q|BGxicogZY-AY$qG@gYw+eF=_w?LdeEP-N}F8fgoI9-*@}UMqGAZ(TYslzsrWeHVg5xn87 -=3rLbOqGc_jM7B^ew;vV%075ySDWz6__s(I;);mKtC^i<1?V9^pg2AAq8u^r1vmH3YUX9(UejvhlHLwh;45X$o>S%0)Lk?^KE@ -9y4lUFl;_d4V<76+GsI_1BK+N^yI*o<=#xW)_U-b_8Shq_v)4zCT7eSa_I5W*3DvWhNR}rw)gEl`D~= -t{ImgHuTnh&d%Do7_&F@1ll>Pi53-9=DYNfLk2~bdRvUBZ#RYC-xJJ|$>$gUI?w~jF9a8J`Z+xTe;CF -5j@E2@o;GJ3xX?fX~-$Fc1J>T*0&mK82#rpsj_CxHf{Q`O~OU>?!hIU@yA!fAPHZdTdf)gU>`6c2&)@ -i4~+*amzoy?2rG8sWCYRl`E>dh2z!3p#QhT2wEwVGd_eG!u~Il)_SPhW@HvH!*lo@(Pm!pd`gk?Cgew -Ne~<#i-#_w@M7Bs?>!G%XAmbDB#zlDZQsTu4ZMB5kJmx9pX$5r;wA@Ns#r?l4myGHGU1sxy~{8TMsLP -0YQ^O_mqp?U>_z^{+hYq0F4xO4DqtLOdo*CB0~LrMT;<2p*?z9^8YjZ#nKE9{4SOECju6gCiPJ28sihRDp&hbXx*B_Z|DP>eYxI+bhhaY|~TahpS{0_Z;t6F*r+vUp6NhNQgp&U-Gp!( -U_k=gHH(SdE_2~>#>-|rO$#+99R^5o_*ZUk@~W)hLgjm71X-EBZI(LQ_siI>huAeU!j*bFP!|}0%bo{ -&;L)u+5hQ%{2NI7!yf(;){@XZJQCfM+>>zQL#NorxL%Ph7;n9^E1=!v@8At6Ch1RMEtPI9xCFFy`cnJ -EAZi~yLB#vaM`Bw(j5gr8Vq~1$@MainC>Q?~tVKWiZ?Jvp&nC~e5vs9ZYwcZeID~CU*{$6;+6s-E+Iz -I&+mPA|p23E&acs-9hS2unP3k?`lD^w}A^(FQf7xI&ZLs$K{1(=J-%ztcxaUtGE%(vx_wOO?hTwpI3g -K=X!+s(-ZAEavzlF3nOXb!bj6jIoJ~FObkIYBY45V<;3T-#iPV`pxo*8{hwr-s{pU0*wX!lws@H?3&H -WYoQer|ct6F>P(EUqB4QNG!ByKnp3T?7AQ+j~Zq{XY2$D3(6ypV`-?mwe~lZFO94W*83^?!E(UkMpqJ -dSdLwcZAu4v&?X?WnCBq;ytB{En6RqK7X)-bdcY>a@0TRi`m;^&Tw&An+o8yKaxJ7+Q#?T)Y$L -X^T|LM^AJ&iT{T`kO=Jr%s;8)tae@lAzq89**%Kn+=wdOM3xe-1duqeA@_vatxa(wNJNj0IC(DwN*Zb -9+@D#N73B8il8T!SD7e#ZjJ1T(rob9UoLL&_3>>-(b -J@fZCit$D3uIU5IF6q2gUqp4t~Ce__Wdd^7yXp7Vfr9o4 -N?hU|lXKLo^rt;z=Pot?qXx7;dm$gYvUi+e%A){CmGfQIXytxh^dA2;|*fIgdtaY)2Gc^b`MEpq&=UmpunX|jiE1l&LSWuh^VJX^e(-JtLD4 -ysYK#PE{>!93`dVP=Q2d|a88yy-?jkSRE{?|K#v0B9=Ya4#XmZPJb?kNn8z^?hWD)_(`qqxS -3hnwIZr#KDJ@g$t%|btV&qfgX_B#Vvrwj^+)E$=b?F>yV+ZJ1K{#GJsa)mJ$Q@eLI3&p(c|$Nw*#%OP -V~ihXIKn*UeYg}+cp7RM{F -cb8UQ+FB8qk|O;>Q#|?s=9a}iMx2luztAL1zEpiT1Ryv`G2WuktxNQtr|>H?7HL?e(u -z#V-f`bIQQU!ctNcBa$6?SJn&=TwAq#WF%4d@O0sF}pDklhgV4gwjNTE}+Hj3*pfq_z6_tj(U%v)W3L -a~yNysq-yibDkNgH&7!mTbDtOj_Uh-2`OmX5|>q}~5&lmzNC$+g*zZDVlW&;U~p(ebeDt~!z0x_}11z -pZB^uDe2qXvqMV)sWi!~D2ty&+D+jvDwy8Zn8it{&}5UEZt}w<#T+&PLEQ>e=wcC!1V?xesQb6IVfp{ -N~Pe5-R$B)KiJyS+wAudi|!o&>`*5M$?>5Nq}WG2Xl%c!)UfU92ZA93Fs}dLfnB^MyR%)0$x_@a_wsq;L3WBd~#K0doAkqtCo3z^n>)P>W! -sA*vM0YZ$tI+o~MQ+m(y*f{YgFv?V#q<$$`P@-Q=wPJ|WJjd)8-sqwv;-i?jcOgnrt!=MSp<5+52i?X3d;eIGOv~=;)0+K -lZSB!D@v|BQ4Ogs2J+^*1g4vw0hw2a-R2*sW#nS*EH)|&nb9cL|Yw|6&@_X6F6KMX?l+lIf2>|=0jC4x}U}oA4es*^4_d}#-vT%Y`iE} -NAC`UzR+B%Ko3h~K_UUNveV3_A)J&!0F9l`M9#sHNmOtmGlVg0(^bYU>;RVb*+k#*Dp%mkF=#qK;1gJ6xYG5`U(^p`M2y=Q$4Egv*yenm-XrHIhtwJGqZdG(2v9|>mOJDA -prg7S3fQe{OhEDfPj>CNyS#pD*ObjtTxW7M~e*S6zi-?lb+*Oy;_GOHC^=TqCZQ)#%#b-MqU-qE!awc#zbmGZX3d9 -a>TIE~g1iw98*fz9(85TFo{u=FnoQv#|uXh(FGw1;*i4~e<#Z0+@qn`)W-SM5bn$KGe<6JXAl$$KV+A -ZeNF@}9rXoa8s6p~B0Y9omQq~zs}a{xcu^4(oW)ryOo#TI--r8swAP2#UA2lj7GG<5n(#;K;)oS+Yd{@4gN;M$uNyV-}kp2dw}Y(k -@GT?NAZkc4_ldm3sy0#+x -&|tn@GATpa{5fJF4d|*h*pyR6Sd>I=h$sjp+`idX;+#95)DzVe#y4aQF;vBGH`noO&PhDS%QP&34A*R -rnZjcp0wrD-^U%*?v-Q{tB&FoV}tawRpVGXQ1Z!vcl)Q*@0!2XxsHUYNu4pJ7X!BfVz0ZGphI#&6qcW -l-rze18}UAd+=HDSs?&M -FRRP7;ARDn_I8Y)9yO8bEEo3B=3y?><*G=>6i$vPIdz##XmsrfMhD|v>+3Lhgr_2N(Nogyyi#t-~jw* -_*As!)^6*xrBk7_FkP3a#-@+Q=#>Be;uIHWr33aRBeri52S1LRhj#J9y>=iYh{`!SD&_*o3v~Yu^v~zRMtOQo@-wx$75ow+&8?f$ql2Q|*nsidX!wz40s58iy9sZWQw^8v+aej -Q!mDnc3gK?N*zR5Qk8t8U4SrHRi`XMlX4FeJBYihZbMpes?jrSc;G1Ukgn2c3VkS#<3uiM?f<3#TytBd1S2R+A}9pm7>-gXv74x$ipOH;r?DmZ)sK%{P!b>QGDz}il -p%hlsG}x;emLnv>era~ImKx7A@8J*#Qp)c;!pDbL$d`XNAjPD5Bjd;BV`zdN9O9F(~5tE|E$J9#YeMC -{J8-vIk?2A5476h)DF}955H)L9+%ksFP87pyz}hcie7mEsH3lR79Yj00fpK1Y$UR1~kV<`h8a(_+fAQ1T>Q7|Re^E*lz7Gxt_P8FP8}j5KDWo*H{>ICKv~>l^mnput^DM -Sk;CfYu2~qC$|^5_mLg?EcJWbd2$+NF&!HUZ+Mv+StC@lgVRAO^<9qL|PYCn}sWSmo&5xrhT-7c_ce72yK)v6p!rI^J5Ruxvr%vt4U=F=wtTc3&<7xBBcouD|K6$cul{j(2a>5In -6%kxQ)Ar?ARAq!U}JA-`~bwdYHE%iF*umaF1A39ZiB3g@ExBS|d3>MIDQh7?j5{9v@^C%`H(ivYxiFQ -v0$Da1j-X*=vpuQRvP^7!5kb9|1p}&;u`xyiPjxpTtFo0yZn`A+xGOML*OdMZ@3n>|ow2^)mF965X8D -nrRRYbnwahWjeUh+B16+PV=j^%*XBJ1eC=SJIwP^wwF97{?dFK3N@N -Q}6tt(}d#i@9VRa1NTZ&az5hNSQzoEv6*mCM&6vtpq=mVidpl>+Ef!Di>#d^#LH%-Zj2~)Vu3jauXY9c&M}jqG3v -$}1gyiKkJ}#i-i`NL!2i1r1^Jr}1^F8e1^J0Xp%99~X#&M@n1U%3+uaL9;1rGE5VX6S{dW{We;TnLZU -R55&0%~5*4Rhs_Rs@?j_L&-{z`_&cn$J#I>gT%3ho~Pb(|bk=7{{-j){K?6y)b6DmZw?jtT~T=!=BOk -p#ek-7Wl?8hP*$S3y2S>+oH?VuXJ*215Vz_N6qip=$vH!F~DgH^#0(sUe65e0 -S6-b@Ztm5^$B4cWqI`w!$!bxGIB7(e&(2H_9!-&ehC20>w6(Pus+xbb4S3)hYml$}!6FLYUGt>%vY(Wg~2^@T)UJu1?@M -z6g6x==UrN^qD*_W)u!kLkoF83<9zxe}1#6qBNC6KJ)Yw0D#!93iSCg}x{_8mJp6riov8rkO|C6Rcuj -J8`+~z?AH^Z-spD8)i(`6Qo%tJAg11yx9?4e2lD^%kp|tiha8KLti?C|9+pHzyv6rgto@7F@bDHp~_I -CMpVjcl{S3T-BTR+&`2MNY>_&docufFZH92W--EF`KM1b?Dr+r4n0!l5laP3N@FdG#x1@D<|9Z`Wkhx -@!eAjm?wmFE9rL>f>o1Ka8{b -SO!YtXRX2@VMWbgZ?3@PyQ%#vMy7JSl2wMkyJhJ*rLh9?yeJkFGY8S*O4lx%=$cm(GRkM`k@k34kVeN --F9E@|l`ocn&F -ECh`{j5kyW-xQ3JE0$^H0jjRq%bX6nQMm%rdDq5lHT`PJgKXb0vWNLo`L#nD;c@IorWr?G#T@r;NUU% -SyYkdo*?;WgNTt1&SMm7b0;)|#3xuidi8sUR$fHGXdIJSjGaxGipsd(P;3%1TBKu(LQ)-hBseS3$Ybc -uRel)~rox;3yeRMpF#o$jS*@GUp%p+3c|Ks6P*U9QNwrWFv3Rk=QT$uPB)hB=k@i8U}ATPPz8Z>0`-K -8>`X2c`q3OVQM2;a=bc$x}gZtCLs)Q*Tnd!s=^qNg|%Y=u+&eQ!_M~Xmxj}7GBS#CuKb}Q093Fp1u(% -7IlbCj_hXfAdpz|EfHlrE^EzsHF_^kZ6#`WqCB8YsW5h4*BzW-3j#`w$JO+;$KOau&}?J78G!;diP6K -odV9p>PL-)4*y>qjCroH+q*f4z!H9xO)F@xYWzlO9ce+Ye$>puG#T2p8TD -SHBv{eq*{ialv6WnY>8EStUO@3JzhpIZdx$)lI3@ygVVA9@UhVe@<`Husi8Pn3>M@QK!_%#sl<2gW(+ -ov?BAI6qOmt#5?=?#~3p^wb0J{ycE6o4xz#9FVI0*e4I0*e24%+|G5Dk$8fzTMagCBGcr}y9*qW2)1+ -F!?Th(qiR-lN-}&!j{L%!#4{vY_Et8MysFoI1ut2<&+4 -ml+o%K2|+uRnVg(Li`z5$Y=iL7=BJZoLC9`(c<{fUdQMGq5|sZ;D|qQ{~kJ{@GJ(&x!X;yjMh+j=j4Z0ck1pDDWiZ47ikAmDcxX#1 -xa=nwqd{}clq5D55SppD&6Qon0Z`_{0vhjJnVNes(XPD^=gLNQtermO%50##mjtCJ(^Wh(B;@vPUKd22&k&zjwCBUx_R5pD8mG;B%f%|-8xL;?I0N{gR(r5+f^x5Ks=+MhXr#rb)`w~G+)* -!*K3W~#}|nH8A)vWJn+d$Yu7k-U0+7+3y=Lx`(#tlSe>xbk2T8!J-cv@qP~WY2OWhUiM6ggEN7`jPXuiu@im##6EH09(RSGp}FLvynQr~A; -&b^VHZDi|NBPB4~fHH84v0(9!~zf@t_3qa+zBLwD-@~Fsy9Po_{nR+ke=2K>yzv5AFOH-L2ZS&x>dWL -8^8dglh~R8OR_mY=TLym*&;;}LkK!u$E_UGu|$HQJIfb$> -KqbzK%+d4I-on -fUBbRe$U0P9oUi?ThwYA0aFrQ5IK2A>nzOIUojPj*aw?0Z<@BBfHMvK7LCv{=|fi-$qtn^^6zg+!l`s -J?va=$CTT$|TnMGiWbmp|mc{Eyu(^;7jJ3_Pa(m9OzHY+mNo5SPEa<6j=}>tKZ`^4Bj+?+;_%SN<>8< -1N$v-fvgXt^f7qBEPM??xstP`&Yaj*Z6t|KUl&4Ki=Or>-&%H?K{)_Gjkh-2iyA&ruJWsQQPE0L6dw~ -PlFFhO!N_4gFl?|Kj(GZk?S^VO%LFNesCa_9D^G%cHpahu^rgO@vq^Z<*cv|v3`g -iQ0xm4e)O53z&^xrJKoyOJLZ3=XrhnY?kBHB1xIdehjg)j)I<>axXeLKM;xug`#J|~B#+X|UzppAdvu -vV)4v8S{=ii8Wqug?Q8J8|7T{krY~kY!MWtJ}<)MvnN9e+R+Jpb#4=eG_{NlD~uRc=Z-ks(A=h6a!qm -=>2BKrZ$dyZ3pEN{Omd+EZju5%0-$fJ8_mB^9dz_AXY63ez^*}m|KdWUA<`c1K8H5K;>!$%wNS5EqVU -jRRh`r~a?%ENWrR`dNathkZ22K&Qt4~MnAZ;v^cem+|U_B}7kgXz -C<2+7|b(lOQ{l$HM+_XR%Gw3@eH)pOeR%b)qna{?ZR{_)xP=F~=;x4FA*viFt!_E*vNe~$-uY-jj>Vd -Rwp`#v9^zwuEB`v=6YJ!;J=(UE{p>z=|&Xm;?7;xPjuWPQR%qkIrXX?q;Jl$6}uwb#30#Yc0)Dgzs_l -_I19&(|IBD%}m%K$9u5w2WO17Pt2)xEB0U2H2=0SM#d(!DXYw=p2$rPH*77+up2A6A}bVS`TRG9L{e7 -X28YS`V|^OXJi=qu{#!xzEEm)EAP}|pD;))D>C8$t{)l`|1^>r8|5v7P%xp97k0- -W$d*z?!t^tBCXg#km6oN>v-qiu0o-dkC6*B -G&<8p7xV0yq2$7qTt#0-`IjI0S*OtxUW-1zT@YrmV9^&TfNZvx?_L6ptUFnI1qD_umurqX{T1M@SY$S -)+XTyw&b%UX0~R`HAC40JD6jSS`2_A*4Vjl!E<-I+}69yn9s7Zpjad6EH)7=w2;g*!<|a3vj9htda4R -x}vxrpQv9;!e||+Rh+U@!kS*yncO3sBvG5WDaf#Al+^_2C6%HU&B(%^arK&Jei>3l#gDa!?LaS*ez2# -52dLTA^N`dy20x~Pbd3s!hmv{&qCjxm)6HkN5Tdo&dLHYW0YhhTT?R-T{-w}Ti(rqC@o&;Ig2iaUFK2!K@w+8{InL5e>EZsu>HqWpQ6A?XSz -rGn3w}cfKVR(kFo1@jJv^fc93n9YMj-g-3O9Q+6ov<%$RSaR9X+J@XI2tNk2vONPNk061*3k2lcNmuC -yeOFYz}pFE>NGLnImLE51fF0(l69!a5AQkD-gu7-hRzPTlLQbScBxq^z2|_kH8MO9rQCl86D9f=I!wcQvOODc*t>TBLjL+SW`h9raXA7sSbeCAW`7OZ9@4fS-`(&F*6rrITmJEmfq#0-Ki)C$w{ -KaJxS#pEFHE87$Ki1whnHR$#8;n`nn95m2jr$|`s{&rvcj%JBUvVMiS1RlCg(^;E`W1ODCol&n36Z=r -KX?u{jw%}KS1leTP4AH6hhO5)5VU7G<)W?M2~}9Ub(l@=MGx{kGkIR!bCSEvQV!JcO{Tk?`}hE^uzAt -ic<_Sw<`oyOV{U|N7~($%d%6ixz#y=!3nrp7(G_x&=2A)CmM6IM -0k>56DHK&V+^6I4kx=c_HyXyRsB8it&x!lcvK8@<3RJ-tlU~QG86TRd+4>5G -I6ZzC}(S5rL=c+dWv(|Rgz!McWF9+FZIH6>5VJU1Mr(e^bi-tm2y8)p9~vsO6 -=>0l(@GSi{wKvZCt4z43*5todGQ}&%;7dmS)jBaN(=UrZk`3SALw0?2G?;Bgb%L3?%y#L{}ZOZ+|*>2 -~8E|+^H`B`cH9y9=dnZW!>W6MEvv%hTK?IZLNtRr-CR@ZXvMb`By;A}=T&sqVjziAQg5vPsXacRny>9*H{PPA@SNIq_EQ_SXB=IE{s2`@z??BU^HC -mi7P>K;ibY|__Wrvow;JYB0%obZ&E)kymOyiq|iSfQuY3KU((9l?ruyM-5be#8B`e-5e=bD*f`RDURf!>e%eO~$wcu*}Ez91!(*6OZIRbxI5V!lgd-lr~|9=^}C -velLA`i}C-75O@~ZS)e5H(ShZdc1q4h=3jH)yH@0l%Q(R+?w+>M~&w!q5(u@ZTN=%iGJ<}1946Ng?1w -Dk5u*l_~dH7y>MTLF|H0u;a{To7xRBAA7bI(Uj7%gaQvq<9RJ5I@>{a!rx*A^^ae*T41;K#f?)_EFzV -+t#XH)VNmjzXU6cK=hc&A4C!c3C6D%@kUH`{JFG%K$PgipOv|A`=z -pYcKIxhr`R#xY`m;n&gdKXdba=dTUv~#~@rR2egpV<7g8nqmen^RZ@&Ja3e~20GCsWwL)DdDwY2eTd_ -^{1=VKCk|cqk1VbjN?0=ur@KNKxDvlJR~zMkEQZZ+Qyf8+Whq_+37onFUMWr#-;WRMVI* -U$`E`Gcy|Q6VHO@B{h&QOvAuIHT+dfF!)Npdp;CrEwtPs!5+hveJDBm@^(<9yyi*0hx_Hec;v-zuZ9i -6UEYSXes`>J}41cP!<-ujakM85L!_Y6=W#?J>_KXfp`?*E=%;eudu>k+;7A0Q84_e~j!+VUSo?xX`j; -r7JkAbgaQt|H}pz&KOMa!c74c`keeg~Ytdx&4yG(Nl?N3{d`TWJ^ITi)f35oj4JoSezMy40eanfOqsis1@Bb;X4@0KC8c?6iF!mb{!3oAC9i((UUyAiLR&&Fn*6=CV -U}MhcR~c>ZCl0lNA?~33dD(qM^FWCDW=)VPD;-`>3&@BM;zhv2)w@nsa3>)+J`E?mDZCAmFGB~nRSeK -F3C<$U=~Jkb_RSxWRDWGIGCYH~d%jxK8bmHqwWXFCkHHo>ne&7t(x&G!kqBo47Z=M?{t09?6UZ6~UZtjogl;~GU5+X;es0F{Jb$Md_+ -8qCtVa1JHYea5>kAje*Y!GhHL7lw9?s&ILd@?<>)`-qcF1eqRr1*G -BC7kB=NdZ;(M<)ZSl38Ci;1JGFuW$fD+bM+OhWSM=NpDsI=Lo#s-!XvJu8*7VWxZGlHf<40oO8{Q2dqt9t7U_TXM*ea?(rT1!;=MMqbo#EPeC%uWkUp)A`6!}8Nq0LKeAYJhdLsnVjKVkWZ(w};%{QPPU#`}ti&8nBfoQG8((CcU=O7;f!DdE -WRISJ>CT>HLxZH=O@};v)a0^Z%}886jbqpfMbVV4B{45cJR0?T}*}B>4!^e_^>gx{L8&L-MF+BmSqwg -*@n0v7@)W4zG{JhdV~GW8p(#2l=Q9|Jf)pL>|ec_#>IOU;SwD -2|iVS^vDbCcm<`8qPrh|>gd67kpAfOV4uqeAF2DJdjApS3%}?j#K*P6V;<)(Nn(|a<5)KSEtezvVzTb ->!qfOelkO*%bHu;jCWwKrk~r+|Sfq9v<$d$<3x8(rUyDF02Y>Z}clP{^fgg~5)6p5N-+7=%{81TzKRO -1IV3zxBPY>=pIRjPPT{!TYgZy4_xcouSAo<$Gvd5V7^Y=ay_)3g=3@{!I%HwbHK*hnZWgNbKRSWHlI? -y4Rw;l5~5~6P9Az|?Pv9I7C;~?N05c==pAmAGi`tRbPe?7!M;Gn;!c>sJBJ*Xvtb@!!NNe%qjoG~B0&m}KO?~>MD -=G(NoXeMk9TCRA-QRAxvV31xIC0$wtCuAl$8~J(7$NRqXv9w>y0N*CWwOdglAFdzVQGH~g8?(TTyXa9 -Twg9qrrGD!5;YNdgUeNNgNkjZ%t9dIg4A$ -s_Ek=zboj(jBGiR4$rCZl@vc}DA>;A0Wpkn6jTrvk5 -Ymdkc!Mi)2eUa(Q9$#&k)Xedns?gLmU!LfE>xeDQu%l3-r$q>Lf`yg+~RwhxeUVdHR{;sVtJJTJw7xQ -Na@YF$h2|_1I*|VA`w*%yV3M!zccu{WHFrB_j(Lq$#!KJIf2}m_E)CrGb58J4+W}1XKp+$+)l}P@O43 -@g2qV^T)VHGIS4#cKbb}!Uf}ZVa3MMcb$Ftg`&B^~5?i7SpfpJC?q}(pRxe1zFEc64_ag7Qcy@O$%w6 -dTtJ!=+CRX5y?dE(h#QeQpGLm4KArMyGuffe*=!#=J+?{8)m;L~%zMTvF?Bss}s(?Rbm6dV6PG@qQPM -JPr6l5^;1}YufKUcjUggcTE5T`ppbPWfJDcsoZoo92uBJ6dC{Uz;O;Cb;KyaS&_?$!~L^aGmI?W;|K; -o@-=t>sIlEf{oq;#1kQI751GWUdw9pv2+N -Nv7YN%{4t2SN1QDF%Kfz471K3fSygfg%n-@7_S+MyHGPqAbbpi!S6_P+xW{u%T$p$ud -|9fmA#Fh0$mfN6en+s-S}zjwABp}bZwZw#!zts~{R5+2mZ%%wOwR($K*hNC@F)xqkjpmL@sjc6V5y~{ -GI{k7+p9p=r#FsE{JE^7%19^W-GvOVJeH^10{9NG&8wkbrOQJL9V9>=l@UH{g_VSqg8yrIr{e4Ny^Q{0GbM(L_s)Jw$M6^YsT5)x+qe=ONzk3H~yI}? -_wnCwssu`z|`mowrtcF$s{T3b1?-Hsm=f`c<8GvnMhET-3g@syl)ygN{F_^{Xl`e#yC6+pSv9K3cM2e -sbZzVFi-8s!h#Pwxo5Q78gBjYcQV^m!TVaiEcc`2H9*mVwaa!EHNgu~~v#?LR4w)MdqP3GR!*D7qGRp -v2$0-D*3t%g&?Oe!yC?VLrJRxOhy#_2t<+dGYzVey)*+R)J`Mnirp!&dVdTS=LH!B4>KL140SYN`$$B -1icusoTpz=B8|RkDij-O)7`|=_uf$ExWhkoxUE+J(5m5>0Wp|;Ei{p;tTfMSev`C*$?b`m-z>y!T=IduRg;Ag~t*-wZUzR72@4-9r#d#;MWP6+q9Jnp(=@N0Hu$57YX!>UAUtBzX?l747weoX4+LODb$AAsjGaWXXLI{}y`3sAK(A -{Y!1sqLMkuRKoNFrQ3x3AY!O>ff+YpiAsT&OQKo)X5zCLedG&|I`CPGEb>!e24sZ`xl^Kzfc6R8PA6g -HU2Q0qFv_DdhSixpBzi9^89NO|kP4-s;P9S73*Dm@96ZM3lvC~N=FpA{U7L{s$pxVugvdj&^{!C%z{D -Se8SfT!%Wk7yPPUU)Pkn4C96ag{q)_>J_a_7B_br9jg;z47nZ5ihkID%Jknl?#8{@!vBCM)~C6|D^VG -zbpSUY^tvl5{?XV&l1G*b-rXtmVLtJX0#gwE>VG -txG5YB9+0ERaP^jQXGZEn*c`776sE332s7u8kfzA*?B93Ae86U?F>7nRoC&H7D$dP}PoQ -}EohW_;D9u6oJ==G}x3QKVi}X{&X<^_PU|I+9%=LxW383!=4qsV!>1=8FqTUTYV -$$C1L^#ABp~wp*|X{CvSh3apL$EQ@lZYmNMp~n4{a%Y31J#zf|pv&IUB;dJIRf -|Ip9aEE3p!S;)6(gh<)D7V!GGNX;3mNCvek01Llag1SwqSFrkOMdz5$vZ(#|N>~4A2F+yk%+dVJJ=Q|$YhgFcy`-KnQ+_zzN$Gj -OJ)>U27WUB9kH|^SVXj>DK~2bYE0wM$>U>2kE1>)N_9QB{AsXwcRTEE`Bs0+H1(3Csxi&l-cm}xYXF1 -(JCv{1j63UXQtK=#dc@H1Ux6Vb%qd7x2mlrW9qlj$NN5>8U>yCw2l=8#8s2RmEBQ2I$+;^1FLEahBiacDqvYxYk?#v_YrW-#c!}DR>#Z8~0YJt -d?HLP`gWgJL#_q0M&M>Q1Edo%sRUe9&uW?n0alX(pGI9i~@S=h0f9zPI^eP`Q%>M5L}4m6oMfabUC1c -yEFb%1oMbk<|Q_7n@DW=acQqMS>%jtAl38nrv$@aT9BmKa8=f4cn!L6Mx4`5OIxLSf3?n}OH6t8J?{W -8z~4qN>4lNl)GWE0lv^kiGkkUv!3M%sTjVR;yx?3{J1Q7Z=Da`5K9Zis&B;1isg%|W$iEEcry<{V1{ynb@@!{jHTf``7%#Hhntuu7Y{lwqKwXF&v}sN{ -`DY;@aFojwH&s10^#Dp=45A`(S|oCNy?VUEV{TiARwHWLPn!-Y|mYm+e`;i+nJh@v?p@u_D04#gs5IxdB?UFZ?xSLO;st2)H?#<*Q3?!21ValtVUmzfB>|D -FlfHIS3Wz9~+1^W$BH~e}J&AXk{N8=RX^POHW8spt#?(z6?|w#+mSJkDyl-dasyQ3Hn?D!0(g`eAo6H -vXCqK3G(`?q*c`0L)ozAc<-2!?*uJT10d3{7>7s6hTGQlt1sc`aToPb)HhlYRKe#*U_Ey>`dF*rOAvn -i|!ikwM-xZ3l?U=Xp+SGI6Hj^(V>?=z+e-@K^)EK4LrAhgvu|I)8T -5xT7)}Kl;35Q8eXd=hbn16 -J>@2?)}5MS=#(5(2hM19DO|GMVL6$jYJ?!Q69C=Qa9;+rRt&RptB2LRa$=rEJt!NLO-0{;#yJWwI<@4 -&(X6#_qjg}ec&KfuCt(ay~FJuLik4ET3o;eiT)e+L#Gs1W#Du<%GhZ9ktj27VhizSvV|pD%5$B6UeMy -B|=>Tz=i?g(xyquHnfcgD0KUQ? -oQQ*VvTx-VTQKZ*92tHp>Xk>&6eLiTw9~+JTOnB#16Y$Gyh4ak~xMvJPJb0%bz$@BDJXs8X;RUl-L&; -$Ci6qZBSZFL><7Z=x!h*bXsWgk^N^qbw%eRpVT-N5=7YL*bwBb*`7zmwgqo-2Luc0kr5PLHsCuAd3FK -tks68zbg|JXQR?7xn9Dt1$ge -~fAJG1PlF4S$hci*}p)Q**rs%eyT<((cHS_l}@nQRHB(Pq4#w96!MPeNh$0n|s3XE)&S7eYJYu-oK^;GTewDIfpSH?jx(d#sKmN6a66rizZ(K0fGZb{9ayM^gT;+e3N -cm;P4#eWcVe^< -iw2CCEbAO^N=wJrVdvJJ4elbG@ik**_EdzKV=!hON_;y#ihnLZ}fFzI1wm4?=7xH~8zR+hvWqA>V)*Lj#uO(#4zcZC``r_}JO%Q3 -MKp$qVLTt1`k7U#4z%ghz6VW{WBFq!j1ak+cF>0*q!FIIAE7xV1@C1gPjhOCNpMf6+k3J&S7cddyib6 -2e&swDC_^!B5pRnuTCuvH&KKxWvQjsdnOYhth+EV>IH~AM)G!L{-cR<`Bp%_rfXZ%>vVT_u*>=1kI62 -bzq@C~^LGA|&{7Jt>@+Ff0trqdXUFB_Iv<1<90kaKLw&*P|tE#DgkO2mt90}_;Tf6xfKpQ?I`_YC#Uf -apoKO8z@pTK~T3fIDN(Sxr=7Dz7(dCSXS5VZHWvx_P7L3Ff!y}d#6>e59{=TK)GJH8_xS$@KSdxeV5+ -jhXP-JjqUG-nD~BDx#t5uRURnUMA3p0Y-z0XT5qD9^MObUbwb6J7}4Z9?kQiN7lTPTG4 -U7l1>sq`n(PU1c;XRosyVVtrX5$R61$cce*AfHc!Rl4-%!YEQ-&2GNBcZeGl}^Wr>xh(99rRaWx|K%T}B?89M-P5_)-DagcPhwAO -`1ln&&mdpG#MsDQkT(&(eKtPrn)*Xvtm(YUUE5V+G9-Qt{RomG{msx!l6x@IBQ97JcPH7n&}o>ttd%Pbzb2RDrM#R;0hp|e? -rIfw+RtGz(5@j#yE?SC`(X3K7xYq#(@SFz5izeL}xs_%eCq6djq=#0JxNr?99H^`oj?M%nXT3_vdCyp -H -pf@6#f}+#Oa+SBFI~R1$_@#!#j11pgkzw9)|SWAvfCX8foZL-iSrJPzw(4;dKn}{T|4hI>Oja=ptfoy -+GfMB6|k~{BBz&_O*#$^>A$W6CisQn@wcvW{FW5N --j%47e&;U8o!TN_+#spnhWYhgGRVIf^-3z&MBW^}*qig`Fn|5O5$12v_wp^whyMukP1pb16`;>UeOvl -ds1Ljkmh9pGMg<7cc~$tx -8`vqhrP7{Han=wKdFM$K8jUZaTh7c>WNu(qC9Dw9Oy=&|sTPTfg#Kn*seHC=Cqo0`GOE3XE?YuSJu5R -g)YX?ZcS5_}TwpGM_6Tob4h9&3=jZYKdju1Z*gw=?i7fsvoXeUG{)cuZPZ@YEk1TsdKQi|)e}z^h-)7 -5^OWe^ny8a0*^=vJQ@S*gW5Ww^^I&d67nfd<55If -vc}eK8Ia>z(~uG@KA#@FuBNDwglsR3&~356L9=*d9d71Q|tKrj2{IRVUi>W0w!q^#W0NCmFPE6hEND5U>ZlsPrXgj9a6WTIKlRid&l -Q@CU%SQp|?TFE>0ipX#2JY`P|zC+J`uZ_k?EnM!o=k`<%b~;^6ly?567JuI;fWUD3A}$`<&;zk=mxPu -gy;n}Ozz+vYR?{qI60@&TUvn&m0$uptYzJQaO*wHFPE;+{(#xa -0y@lh4>X`DwTiBlbc#fHWe(sO*IPhEDdf5>P@vPj&wm)fi@_deT?kqs&rSG$+DFO5F3eO6`562BWW~& -O(!(HiDp_lUK90oEJ1z?PsK#w>N6epYh<25YbwiPg(|fKmpCh^&4EDl$y^u`Kd1Ub|-oKdS4 -3$q>?cv{Q_y#ci;m&|LFUUEJCr%GoZw2Eq+5dx_NKBY<^{rcpeico6o|C5I2jEkBhs3dfCEV;3dVFPw -d~nUjIiMmq3RjXB-LnmDip$?mXyl(S*sR6W0C^`pP|#BKX$Iy3MgY6C@rf}22G@8YQ9-M497<&N#C_AY@x;rKcA)>6Cs!1Fe9= -CvO9F1lS$)_exMH{k_Hb4H`dX))yhs$G&vFyfZ5H~t9Uu05 -fw2I->ckzJn3`Pr-v*k7`|OVZZO*z==hA#K-^HcaTy$ECx#s}pLGOpJaXPF67MU}iSd2jg4GmLC2_=k -Vac8kVR>B}lwo6;e1QXVC>->VDPf?@+%-B6bhJj)iG`tZBg?IEBUzmu*rq|ytdp%%BNAxu_~N~KG&N7 -+qG%hFk9zCqEQr=5r-W)HaD{DY^wJ)_RZ -*hnf~xB176JKMZHVx#it(6s9d8C-GM}oZdpEkHDJ^<2HId`yOpCx(El+}n0eF6&!w1(nY9w$wJ2L*~1-m_4>}_w1ASCyMsJw&(|K -`^|zsxc_1hg`pTuATWedI7Y!3v1@cd+aVl72^dFk{L=z@K=0bsTlkE;UFqmI|4FI6pOT<=9!9?v@Cj< -Cc?kNs|1Y$&H41x+bYOd28zy_OyAihV9oO!)9bzxVZ%lA6fTw#vygh~fio)%^mCzm(!_nS}iC}Lt-{4 -(_hu&dzvX^|~a3A9((!F*AziU6puCBck%3Ztz-;IVsbXV@!qHG-BCD~y5-&FkD$U4{y=n42i;VzPO_j -rX2c;&^IA@%GEh#>wXg3Z46NEG-k6y+`cj|$AjheQ{u?VIze6Z=?f5zPnHyV=|EUDeENXc!0^^P}2u+ -WvoxTD{)jveP*KgFK}l7kpdLV!tx8>6?L&IUl~~AtI4o08y7Xd=%bb&$DHk$9%usevw^TK -eU}hWj#GTKT)nmscf$y2l9&%(Sf97!j%l$%h`IxC6Fy>lcerfMoX&zPfa)R#%~&rx1b$XOtItSx}Vd6cI|{B$`xbkf6vW~yqM`l6 -*?4;fQfK319MDps*14@+^qm7*6@@px$-vMOCM54a=71u9#o_halj7ak8Yp>9X#pbsO&)b+bWTy!v?y; -U$f_%b==kMoAWhe_-&lI1_-zbz?pp2nGP?`&qgQl&^p8+iH)dNG#&~Yf&b7^*mf=;up3NgeH55J$nqTI%`A!{ovTPzQCHf&y5LPm(|G^amgL -u#v)4kf2k_>b42{Nulg1S|8d2Sp)Y|`BuwJ;_8Wmi+i&m&rZkC@6bvE6o|S~?Pvc(-*G;(u~C!1uTFySw>p`XBgt{Qv9pKXSY%syG;VjQ1p -}g3kGwxOa%5x)kBSE3~kkJkw*K0qJ4v5ORK~_vG4PvkF=x(xJ*GEpVl{8=tn`{qvcviT*&?9A2>3tITQM$T3fP|M3|R@r($ -+vN)u-76XlxTA!2ZKNp0xx;{4_oVzo#e&z8PT&~@+du?|HSI4rUb_i2THy$1qDjoYGGLhcims=k_MQ; -2J}Rfb11sB9+D56B@l=~S!>x~}VTJaiMHjw5vLp@}&pWb|C5p&dde`ph!7cl~eAnzDI#qgL>7lQ=>-hJwBxtjIl;R`gr3}=XD{$RPLKFBKXMd=fo!ly=>B -YfyiEW&X9*?vmT_TlSub^_|l4^q~jnyCNP{s??*khnrU<@OjTP}b%aZQY7>q@>D@Z0xgS+LwWTzVHBU -!04CkCsz=RTc%&zWz3clwaTnZFITHhmB$e_u8TPMs8cEtN1t*rq`E1*9TnS!ffz5ARBz~5S(Lyq?%Ji -gGakEQP)s>Qaj}YYwCE(Qz=YF#A_$m;!>NOFWN+hjB5uI-?5EKc9Z9qw)1gD)dpvRTb%5d73reHLpbF -#FoCY!EbXQNe>u5f(7|H3zJU)k;31rKune!t^QMgFnr%qh1!GV~YI)-2#UPp1`l9D7g~+7W-i`a -yb_b1m_XI%p6jwLV{(`Oo_gE8`*R3=3-oz&ap*w$$emKdD>wAd2ytBbGT&yA4@ZY7`}w-0kiEGFXNyW -Ju0twJ0LgWp%16dLQnFE_dBD6;`RPG%SpgQ-tqamjv4@3j+Z6J`AP_7ng>8X0lS<6G{ -zrVSnw(D-Bo|nmQW`FK{exzdU<*UZCEjJUyp0yt&|~YK^cn7B5O&oE`qL^bW*4t)cL;U2ar)g=e*?hV9kTjZr{vi&u!=`{*EOr%yq$v!CGYn*OENjtV~|P8;FcbNR%c*WW46yCZKSU)w?Si!nL5 -7b5nG#CvugrS{-7jsA`Os-C;&|Kq(CaDUOa3Kc=_va-p{_6_)J7dnC2nL~Z13H=)Ddwz=3 -o@0YwasDoojq5q6H)sGcl_>(4ghv1|x;V1sJ|2K$}^#2rb`dl{Y7sQD-&=F}saP3e0w)I -T6AUw6GU|(aJ5@I)a+N(WO6{?N^H{M@u8cw;-hMkjJ?b@DRAl5yNgBmxPGsNJF@s^uNT?+Ab0HZd|)Y -CBHjCHV4B?E-g@(I4~NZF|qG|2I$!SgMsFZ>mJCR;So8q@(h7YmBAP>(A;2A$gOC0F!;^=Nhgt|K#O9 -KjvV59-B6ie-3GLwR996Vvj%@Ra_9DmAZf=Qy$zv`d>?(!>cim81l*MhrX?vNErO?52q#BC?gU$~slt -6~nsJ#Dyk4JuOf@l?uyY{nTJ?8#7%I_(>Fk=yDx^^WbGEZRR|DQqMf7A;W2k^^yw|5uYyWy-Vc706SE -ig+ox0TGeHfeS6UM^pcv+f`LXkyC8aIT||tP(+xfngjf!d=3J^v?C07jp?I>cj#Oo-@>-t(`8xJdzec -t!JPB9`u9)(0s^y&mJ($dT<0+$A;D!AfQ^gM;4K>@S4qNg3poB*00>pg=@3mU8TmJt-!%RanaH5 -(u3nF!s&d59GNkeoHi=pHT^FUr{Qo=%ECx(yj2iT?(PrAmFFlyE`LFs)Y~1PlpA`ulLLndFD&Bj^Pz% -8hvOcge|+f&m7bwwKu!QTzuzlXgE^71mwOZnakK$x;E -&WPelZ8B7AwklY1+$sBrS&G%IXirFrBmMEuD+%AkM3>14L=@qL5(OAjb97jjgDrSk1l=@R)g4Bzp -N*j*2IY^7~n1!7B&eU^XNFTqsTcvy|_}_D-*a4U%YYZ#V#--ydUMOWNn{z#*LR5lv^^Gk7Eej=NrnBy -dEd{a(M-AE+ayAP^(IVWRtd32IT{<_xa0czjQ*K1ekTKiS%tE>%}$5wAWce* -WNaATxF@;AXu>+)XXw(JKK^VsnwG0uu;C5YCy$$e8A#^6UruCp@2C>L7-CoXId+x!_!U$L%H2l!pqM@ -#hGcuh`~i13oDg2kln#2+R{&Ht!gMouM^{+$k*9tg^#d|j1(OfttUFg4dd)&yLKL_QqsG$NCX>zxJC< -OX6VCxSGZmgIT}Vgw@`=YzC#4nr!7de9H(RQmTVa<}t&lEI`78dEG?8hCQ;i6Z@;($NpjbPRV;6$1Jn33KCnK+@J5+@kjs1u4p8%rXgjV#eHM4==^?tYe^XPn?*HyOjjJtqTU?-7V- -CoR!iw;DzF5wpFeA=nc*$mba+^lhArV0+>mB6p{gIDA{L(fdT&MsRkqOnMhL*!WHo?!0C&2hb_5YYO?JNcOHBHfeOPcn_z<)!U_PYW89%<61xMVJD7J -dC0NGzK<&+Kwx04dW8hPkBnYKYp>D4lO73l-(wQILr`h!to!%{i>ei+kNo(Kx=l)tKfC8n`h6qL&7SMdPD2Hx+?lBZr$<#n4i^LOXt-uha#97W)Lvy -Mt2H$ZYG(BEY)%BsLK+Jfj^1h8<-5T!0ws8O5T= -z&V3K}#)|erW{{#5K{v-JM-Bo{vFYHtJLJ%6oA#B4cn80BgBPbf7$-S8uCXr9$zFqI^t(mgn00He^{~ -d7Sw&Cxq -ja>O>_*$gFc@4$(x!ET-Rkr#60lqH(Pr=vackpGev1qh`^va%IOoncYxX5Z#jbuu6>}p{KKJf}k}q-jXZUJ2_^OzaeK;o>0yxc_P#M3(oY0(tAC;O=%UsI1scuhJ7?-(NJY`p5~x?Xv{K^ -Lkb$I}X7nDxAmFE3khuzTj}oM>m6jd;u8af1I{>W6@8b^}BOFpz6=({eV~qMv^o^Z%hj%F&N(<3#MTV -M{ttDA&4Xh6h_ET4Na)G+IRY9F4&i`)97u${Vw$F#k6400|oCAUP^tASk%tOwzJ?jH`>U}#<^f>N0#m -P;L8BOTN)~O&ogYVu%RRtZ7A~##G>E*t?;{aND}+S5WJg}6WG4Wu0uuaoNI5Oz~1ev?|zDnXKk+?#yi -$h)Q)x=NK(kgu-*4n=I)#^6See_$K*6!zB^y~0_~>&mSLw@C0sl=>d>=0T+P%ttX>s@>DgN!4#zQmk!*FS(8=Cobr -0qT1kh@`1f01P5kD*(y>r2u?wQxLVUUAg~c{ButHClI3RC}&RFVaEz-4l0v2x8_kTQ=y2y -BbWv?@&Vfmv$Nb4>eQYVDONxfSNz8A+JXkEX2`k;DboI7_e5xx{MC)q>Pq+YX--^%6cf9XsPdU%0fT} -dtn)9m9iC>Phlgct09SlC6W6(v*63RlOv;9M4Gd{MGyZBmZ_Y6YYITtvrOtHbqRwFMnm1%+z*m9@=)n -ZsE*pjNp&R7D9TQgE-o2F*Bbm3@Sr%SQYn$yTp`z;F#>6I!hdo^_6r7sGm?Dpl0Pf&c6NU4m0MA3LCN -exIgkB2I;dDzQs#9$bt6e{!LfN;-$kM5t>Ook|3_%A=sRUjPh+48E^^R%e{zNeKB~xke;6fB>+?gDvj -*aeKGE$2UCb*8?lU9>nco}nixeBTe@X$*^xseE;^IG+2Kf`=?xrNuq6@MW{6_?lp9^Aqjs)waavv=_M -CwFEO#lu38Sw^754fz=lbcbX4(p5K72jOXcK2IZ#>DB6mF9tl#%jJah%`m36fF7^ -`R?V<@6?4PR?c~U78GgQUcOv#MP{ECBW*Mf`SE+6ncUWo1MH)kS%o?+TM-bU{GX$|0aMzcTaMu^8!TVb5%5BJL?U9rgpZp#T`;zRoDkxs_GSt?u>!^<>m^+47g-W!a?L&xOMm -4J#qU;68@eX3CezmxS9F3!suEnSq?uLp*mZ6`cGC-Rlqm+@T7^le1*$U$EHK!Mwj8-NotWl6|&bSf4MVgEJ0 -J*WZ&{)Oxx8!1b%R$~}9I!GM)%8QmfcXTyUQqF;Ot;T8d%Vso^j6Ca86!b%g_fcnwur8c&m5o1p$`5~ -ElHxW;(#XxfOFx3Ului%x)YIx<>6;V)P5MGCw_q$gnxkY{HTa~#+7rS`6nso5Z^CtvyK{Vy= -Ui(auqHeeiOREE9g-)ox9Vf_FbK=5y1+kNaTk2C(ya>TM+WekBl$hSbTq$`LYa)^SvedH_EdFfNn;bM{r&;MP_Bc3p7_9xIi9ge^p`LJ=`DikY`I(+kyvjG3b& -tZz&=DdKL>OEP!7K8+>z%+SvA$D$6uKwWdj(4uesUq|x;Uoi4Zg5f>>{uN*WmM`$CZ3~uSz9S*O6!-5 -Jph47TO6U6*OPIR1KNgF5ezucfMzP5$_o*n(YtG?$+zq;ZF#sm>C1R*4e;{--+AsxL%_ykRFf!_uT`1 -SxIA?R}(7Vy~uuEA)+ke8!mmT-*Cg#w73k-Cr4#czQFY6Of2z9d0| -ujblCEoA2M+GrvSvU*kMy+L+S%W1N@0&Dj2`w)eO02KfGVes?$jq$%poU83$dDI2X|x1Su&pW6G{Z^3 -kI6CF5c7$ZLW!s|A8!NyZB_bUTGgt;yRI1)|+bP(Djt1nM-2yOxP^p8{1E*6{gCk4MtkeVasdnifjXa -W6MI3vn@GDq-nLpUf#@rSrh!N%f_=SxK87vu=BG{TMXz_%x3T|2*|~?omdENfu{p5nRp0 ->1I_q9NBiK5^eEE!L%KfB?_pj`q4-WAcD4+oJ#WyiWG-itDMl({gs+e{BWr6Pq?}cK8L1GI%Fc$C~$4 -JC3QHx_zACb>0Sb^<}t5#G{?w~0pbW -PHDW0L{Nf+7a9@*t3vXCZ3Qfh&B%~Y;;$xa4mMp^xdq^=VRjKmzbfV-3KclH3-13zv-v-m_F^|c$x73 -P+vw#Er-2#-TyO)h4=mGHExjt8r0ZLzIbHw|mG@*E5nfPO|?-;KSLI83Mdm3sn(IP6hHl<8TUe!%CHw -Awy&Guh(y@|f#<$*v2ebz-Ijy59pZSdR)VE9~t|wGI8e9YBe4h&piYmOfn{a~|kWZ`~kYdKzR0$7yUj -Zn-W@8sqAP2htyFdp|S+Bi9RoZ&4DTMiwgK0ehnoU4EjNdHGiF1O8L*xGzmnfezHv%$>Z;UB~-I)kE@ -`yLVOS17VEQ*I0@6seNpGKpFy48w#92H6HFclrab}jTpxSf;&~))A1e|=bAUfhu5x=6^^V}JXqSisnK -1Ox>CR&uP4=-hxO_(-0?6QA`>e8Y+V4Cjc&*MIg50p1@04xt|e59h{<7OU!5#t6u{h8Mk*F;I=Vfv%O -%Ozn^>LYXV$C|4n#!%fW1~qa@rrU3 -XVuU6OmzHm2WvFC2mEL%76=bzjkAV#LVMv{s;`-hxls&C01+HaSu@zNjt1XQgU;#^w_3&T2mq1Jp&l;!R6}6*ZEvI%<4Qv4mRDXXpI+kz_kKm&@(q#0W(Z5P9z}xx!e -|{U%;L)c#lCiU^Z#NNsLI)7QMmH-b!AuqkEciYL%F`gOcg<$4ov1@G2fm2b|dC0WSI=SD^}y%?w1%>% -K0^kwc{OQ?G7}Y!z|2I_mBDVrA^bHqu3hr6|8jp$uNNxfFXHoJ;T4tKv1(fV5PCVvFJQS|QQ)mc~^rK -d&Htv$$o6j?XyHkroc-zs;oH!V_5=d>eH({)f+tX6arv9BFSHj2-)<{pxaYkhdZ$i!u=7Fs-CGsB2fVix-lIYPCVs1cA$YsF-tn7qn#pL-yMVQ`d( -rd`ys=?qm-pdMl*BM=X+FMF%wLosILGV)55_~(yhrOX(%4S;Y6EM3NqiR!Wbj8wFSW_w- -#MmLFON5kjXqLFm#+$9ZP*;YDPVkzrMxJ*Jh;)d|eB5GZW3 -lb(c1`^vrIQy@8qFe}dYBW6)hEVOJ-ZA7$W9;bvf8exmE7kh&%iqc= -l&}}2jZHA}(xalR!CaAvxQkRs|T@k{up2!Cw=C~8i;)B-GvjYv@?LguZDPG}(_2YZXv*|j8-AyG|Dx= -`uvjMIIlCzQYY8fXlc5Aob*D}BESX%fc_q7X02R?BniMv&z%)u2PdqX36O&Gjj$o -7V7QVH~9PVtJ3rE(2%HNKt@ka*;d%Yr>bnBl3z%bV8V?3LsU9LT4Vo0)d5G5?_)(IVFPV3yVp1(o~+w -}yPGAuYZi4mSHj;x!Kjk-B~`F-&3sVGq&FU$#|!+Jg0catk=Ih3F3K5QA8y+p`hjT?!9kkzBFopXAIx -IT!eL^6~QJWadI^d_Nf%|G2$M#_-Hra+c$IVfz-I*bVl!$f1ixbBfk(1XMKM{*4Cz57ZfMtu*>q7kX< -J3ht_|?CVP@iaOp(w$eB-Qv>Fi7j*~hD;Whf`l#Hi%^#ScHn&AZ{&*biP$uZxaeWE5yXy8ueE$gqq6t -NOH*O~{A%|DrA=6u3@h=v6$G*U4a_zNRB=&(X>q}PP6x?0XC~{+$VhrhJbafo?+2~l3YIUdz5aBC6KV -Q)~Ja&h{v|aOj?ru!TAqV -WRYu~$dbMVTMU6s8_fm#yp33(W{?OUD!eqo#_^XM&(|mxS|OOvt&7l)pd;=IG+&eJ{VMeb{fg*9>CQK -!3o)&V3!pSE7LRsX!EQ{dA0bj*%$jmJOFSp(IFal{174#(fcG}1gLjXFkX`!&DpdS^gqrnbfLqiN62%2ZROWbZ -WXGHUv|_e6d2(q&HO_jR9I=Hx&DnMPCe8885U}9jla=)DGS%)GERYIRe7jDbC9ISx^!Jm0N%NC7kFbw -4CfX3^&){En66I}IcUiGFXcSHHO5&Y{e!c3fNI&t{|Apl~$m}Oe{lI5YoJL`aAP9;=VS1y(8!LvePo; -n9Xm6t5hs4P}T^EIWr&&Pm&GqzKUneB?ykY$A`3gR_;iH1R(QTJXf4BJu(Vmfu-a2a}zO%{TZSI%86M -9j)>%nbLQOUol=obWbKbzRa-XV1FTnp&Eb#50RiuWcH9N+or_8QyGM{kzBy?pw1qux%2-kf(^VegcC> -(XsRIHLAn@mpSqAb&?-`L~2yyL=E>MN6-hpUu5zpdRvhIqI$Fcud@CUgc5zxLeS%YWZl587k{iE`_{&-9 -iqMwLtZ2+37O~N;9jZ6oUY?pA -5$Gzs5^%=JGO{NUae^OBCyy#4Umy6*Q297bxT%4q@A}A+}E@Fi8gCvH~fhV} -WYxYI}lVywBzex{vCu^Q9*$-R_?5KG*oUk*wSEP{B9#?p8u~(;y{=JY94Rfa(Nj#&5>ctX!vhf@hJ1B -Mli9qIa88qaIH~ee^JYNUx9Re0Z(%k*qb`(DKlhAm#zYq%oeEV;>sGO_j!*-86nbCg)4YEQug`ynEom -hs;zt9G$@1bS|%L_e?o|P@Z=_*MOH@!6Df3LD~_nP@KXzTvTYI2=@)WUZop6EDtg@N)_`9!9Of0k3HO -Wf(tu=rM12QBc?e_(%D!C%QaF2|Co0eQmHP(sknH%OBWco%Zqf)ZEjTs^clxgBt5s~zVI*Lvs4%eQhI -(TCX7bzLwekHDGEHCSyY+3Kk3B*e^r6}JQcQaxKn$ex)sh*cm~dakqYo=B4cT{LXV8M;qE76aw=x%d? -TAUeqM)1LS6E-J>SG9ensQ+eVtca>BD2G0aGCz0((5Ya%b$;s}O8^kxNiGiJ=!g(tKX*88=_`Vy+?di -JgxFe|bEP2d1>tdSemCZ)wu^iBPNgjTDcBu!bJ&S|gpeETq(-(D=hy&3w -z6cQMtE@1Vc}2ILE05ipE$K>XrS7wA>8(c{9at6P^2<><5pji;Ns&7g#EwYYd0Dss=4H5m4>BKdba~b -hbq|Qe9WC|d@b`l)ux23x)0M}mpDn0_of=X+lSD-XK~w?wtW?+-hJ5A?iss}#1VV9V)RSR6=L7`l6OZFMD1JNE~g*wlX+pX= -WkGKujj$pXtK9SqHn=~y@{K8?~?JJtishS^Q`qAy3?FyyX_UHOhM$`8#@cU!skGH?yJ@DV -${(kqse{=i$-2=bsul(8_UfwSYrkX0OoQ=7Ep#_sGOk38e*6koX7 -F85aPn&_3QD9mo<(Em3?u7MA8E6W9Rc^L-4#o;`ocweYT1LkiI7*(!k6~SAH7kj>& -6a_w5=8Qfd$q)iqxV?+GD<*{Ex<_-XAdFv-a?D)bn+cE~00-ESdRXSDaW27fdN98v+UQFD#t@3kNm~gy_AXzr_Xk+aV-=8+oJT-an4RdqlE9;0CAAuMl##2qp -Gaf_L}#27nvj2FTts8NS;(DP(V|+!i9<2)W%kk?zf%@!P9(gZk}R9NS+Hd$$g3z@5C~1p?Yan%@Qs(J -m}ne-jo}8HGGE>hrj+Ds~~IPqW%cXpa0MX?QO1cUta}AFBVjN=Ud&`9OCM+XLeC@%mB;7% -fH92wGUTqOyi}E|8IY;d%FDe_WM}BKe!$Hc8kBeL*RF}_`5p1u<~ah~P=g-lgb -p`O14E>5}>b>FM?yZ7>-Zol%3!a5>rWQ`}$>K>7ae-t|XMogT&=qUlJu8mDYysgsF;7BrlYljxtp -0pZBVe|)2fa>tsBTmlB2UIsU3FHaP>czzz;+(DE4neOr0bf#)`l*Shp}j!cJ8ZK$8`B>|E*Eciqs|Vme}KbiwQDq!~-p&%)%N -d|r4e^oIRZ!+{#o8x#o~D&uO|%I%oDAvz!L2jXRe>c}Px*taKRbV;}?dHAj!xU%E)(m>2ss;n1$6;-# -mV?g!L&&TqJ=)>;mz#lt=Im-l+_0j{@+q>0;P)l=6>kqkJa~B(Dx+7sPPqoi^&Up%ef3a)JHbIE8CHi -t;9z#R8Cvx?d*V%@7=kTn_#-HekBPgaA?fJ-8MHbECeKh2IWdqz^NCT;z -zQp64aHL5M_9+x$}>S(l2ZyX`05kmXdVaX20goLMgE9PVk3&@qlFwOT~0~5h;ejpGuz1TMv(WiD7mZur_`>f6~6Zkn0&+kJ~sKeTlG69 -wKvecJ+8K8wk64U&s8PqPN{c8fpq`1S6A^}ZF^5eZ5s*ki@hs+Pe<)y+u?ozOuY-S)LydPRlwrCH!pq -r5RkjE;d{z7-U9;S&F%yIJ*fFkrfny0S3$|XtwHhL(HEk>W%tUycZJ_(oRkC=kgN`{W4lC;KePMbXLi -3&6a5uE{x&$Y(PQ;HdMt;HHUk^TBp+hfC*<3Pw_6STymzhtXz%*DhWy{{U4JNTZ+ln#ZSOK2odnV1Np0?-sppe14L9eJ+phJs+Vd=dwox<_zT-UPl&+%l|iZ)ixA=D}q{D0M~w`4cYvQ?fu(bdbmW(Z -Q86r1-C`nbrh|R{-QX1P!{pSD?g0FE?f?+NQCyKCh%G3@1;XrHRN&_eiU9N#!00JBdq*}m+ns};9*Xo|qhi4p3kQSpUNw6&JOb*95v>RdEBb% -MeKWP3HyEdz~|NeFE`qwuM+TrVmXMO^`zkk&S(EZ|yAB1EHn1uFzUy7nh7^QHWq7Vw+n^p*fCTSESDH -26V7@-h|`V{|Qc&FPN*$v~j77w&Hbx^@Bm4l}5)?jL<%hB6cHT^uhv7NH<>KNM75y;!=Z->C}?X`mLN -V@w4yq)^?0syk3()Ms4*+hPUe_QZ?q`U0@E(R3s$@&e6X=u-g>`9cjy5fHQdopUH>_qg||0i~&MB^P7 -cbUXwNA3;F;rGlIneJ(tUH*~yZU67O;~$xP;Ge^>x3s91nq+vng7|dSf?*?zk@vWK%2%fFCj%Gj*nii -pvVD@Bd<>f?vk-c?tw(FehWEL%CIgnR9b8y7MDSKK{prMf=()nuek?%V&tF1Rhkin#_w&H_Cwz}Y8xD -S`%Ocu4%OT-9fBR6E{dNr44{8GSQw1PbKU!IKU%%s2zh6x{zV^5B-P?<7;HUTJY3$_#@AdcFL*5r>3|H#)?t1@XqtE33i#fkXv=V>KRR654xA0x^PY&6s! -!#>vIvZgug(JnX4t#m_<=3J}|1bz%eCu2y*Qv&j=GjwS9NE| -)FFdV%%}2=(M3XGgQk{kzrl5PRqQBds7Haz~(YeNnpqLX)FQDn_U-t9&lTDzL{xRCu%A-9lhS>1P2uq -18K?ZzID3^Et;qz+R%ui>poCsiGPdMu)~QlfJt+0(YSFgsT1ITvJyNsF9AX6`k@hCsxXefKFBp+4bC} -P!5LMUx=Y*Oh*rOaj8{>NmQjg7C}0aw@Oe+UjgjnA?*4I6&|#hTZnR{E|7kln1fl^xA|=9;7xi-y)e5 -mDfqsUL_M>(%6J(mv;>6`5ZjJ+lvl|Pk-G&Rh-oK|PM$!jD*2Rh;ln-AO`i(x^L4$S=uu^HG9Vi_+@+YDR -dKrY@|%IF8AJtFXZ)B=n-}xvKdz`;%2=pLKOg#m8n>FC8(~PCgPt%EI#RDHhBcnx~Oq`#OL}&9|zAee -N@N>wNd?GFeh$-GKb?TK)YW}M&7ir^BNx&d+U2lwAb`9A0XQ!pdpWigNV{OcEW=UTKPjaf~2{lBzY4TGBw`Yn_jh$H@ZFhOkRBYYwj0k3JYeJvic+-!C&IhlD -;peA|ggTJNrx&@S|h>L=sTf>paA?L(Ctz*x%za=T1)s~pF@RgO6b(K30#O%o5#(}@g+!u1{#@^lrZaN -0ah&=+<)p1QivxPJRC5KRPMao5(u#59?R7%H-zbekM-g1JauqTAi{#D%!a?DL|K7AwV0#z9Pr_bIcwnPG13XtA`wU6V1^9CJ*h_0sT?@{eb8elpL8#SmKBQ!Lxu0uyYwLz@>TK*Nr=Y!oi)G+ds -mWc7DSx@+=vxrDeU6#N-9Zu8l{(qPTt;3yM&){1Jbde$BH#o?v -dns*0pVmEc#=zE^;CItkzm&k3{rxMi!6TYLWoCatK7yk@`f7|sh+oUUmsFwSM|QV`(Jt*e%{jXt -vCUJuNw=H&UuTm^0V&l8`S&0f{y5}{61#9H?6zZXZH*ECpYY~`vv@y8}`}#0{+Pj`|N%J|Kx^!W~u=E -%2i>=UxW1|uAs{u?~x**SA=`EPLQMzgiHMBm7KI*EF%C`4}9V>uOJ{+ZB*Pgmkts{M2<>jJc=YoSYO> -tr|HMa9}He(6&pLtosV4G^;jT0;2y3$+HYJgg^w|)FG$vaymmt4Xps|o(&|gHHaW+f+vlR_fU`v8$h>&A@DO~T%5qjE~vacbO_L-;H5}HKV}VM)vh9~11$7tG+ -GjU^%(R6Z{?IMoF5nZ*2T9M#)@?8f$T2_w$1GBxyBp2ieC9a6H`^aQ1MV< -!EERk`D`@aoll0SHS>UIaTKHh*zbG%cIxiiuh7Pl!x_cAFg`sta`PJGp~?b@B?;#7=0P-VebgEE)t$2w!wF7zK8w^MDLQT}#RWM~H@<{}`?=yCHL3^!31sKwK|g -ibU>kLQ>JM!-OSm`1yR8Ptkbb}yn?*5|I!#Izv>B*f7TI2EFgVCdue=(-Ko`8F+29JcXK4bPYrm%Oc* -!ymYljoXBRC(<4@}n9+{;f_->Il04`JE4fWS*_R6q*WKe{UXL)|&>%~e4m(OC$q*Lv$ZNf>a!)-kihh -lINQ3sVILz&y3h(c#3Iws=pX(vovs3Gnf`;pHc9VYs>Gso|uo7Kt$`D)d_Cog3lg#n2X|`q9=r&8L -+mnkbxTxG^|J59l(CXA^qL0G<1!$`9PZYq>&Gj)i#|t6n&Uf@%5@WEqTn8nKK>~A0!s3QHjYdvo- -s9+k{2LhT8%%>4hRc2zu}QP)f6*yOeNe%D^)nOM4p3CG(`<6HCJwA5qD#PxW?PTz4V#`kPw{bN#qrRs -D>`#mXf_3R#DLb`x{sZ?Q5z`%(}LBT0Pt1W&2Qh>pItypgW!Ui7p -s-=In8%t26`Nm@;A+X^rN7Y}aiAxOdD7DAhafIY$3>_H<^h_rkzr@LR%7lPxdD86%Kg%m08UY<4B^4v -27ZpV{cT?W*@3cVMd+&aCBaMvOy-{ds9EUqZ!BE?~l{)y3)eoY_U&=uo%xt(w5G5-!bZn%`#Fzuh?f8 -8(pe`?MTZ2OPq`@x|B!AXPyQ5ZuJf&y`vz(E41a2$bAd;_=JFNpx*Pw{Q`PVDSbGkn*0QSau|8>&o-H -yWgNl$a*Ft5m*Qpnr;Qskc&k_nSiYeb55k(^FKjcV4C5FnKQ|q;^kH=nK-!cL%L)aQZ6~(LK48?c2M1 -V(PuHT~Y?^@EgbX+Q#&qo63qeT+a8GHVj?F`~ELX?7$j-x0=m}JvX(%^=$_Hout|YZGYRghCS~+zp_7 -jenpc^JyV|UVb&rOxK)SAH2zTr5fH)g{*UvSUcT<>coAIC3{5uc -4EL5wN7S?|2hx6Rfs6+P|t`Xv*+A3IBP@(a-2^sD}ChhIFZSNhASEa;2=Onz;HS -q5&`}VTZACs9n$L1an9U -%TKm`z|X#ZUtLM%1#6a6koAKZABNHnROfNBF5GCi`?c2(rsR1#lo3F%`1j28U;X|%nqwNeVnVV;dhXN -|!4oUhbim1RI_sSkosiWBG*X>c#e90&py)#)5&@wYcgw-VtHAVf0PFB{GZMF*D(AW^Zk%811SPS34){u7) -L1z2e+6^At;4II0}E+aX>zG7ThHP#k=W6v1`({=vI(#ALn>aQ|;{ioo(NagT)q2KaIkB5e)VqL>Y2bj9ev -%Kh(ELUGiAdXb1lpWFh)wO^|m2`OP_$b9iPfu1ktM$i|ygSH5P92bdxecG|QH(xV*L;=XLCAEoQYm0D -&mu^u)gDP0Tc8x(O6*_HgO>_q8N(Y8g>ueFW?UkarN4|y -Z&!-snw@;eY8G65<>Cc&cOnDfJZTD0@hu?;qp_T*o9n%trbnRT?bUMKbrJ<8E*B6@U+RmEWKoUn8R~5E#JwdDc-YAAc -JQ<2#E_rycfHPKNGhK?Wcs_C!NZ&uYk$uvx^P6wZcPt$ERl80$9isw`msBR>jdfQq<945j(HO4!58XO -}@7s0$vfGBbOs586`6yPZ{&cIyI1rN3?fQrXv{EyK(vK9fD{-wyapp0&@ip*!tg#63L@T4S`oTsCn=z -~xjq^uRtDR#xvnLWVS)kO7lR0Eh-aWSH9NU-EI9?9jV`PhuZa%`QVku|8EcqnSU)#f2JBOKIHHdPm52oP@E{rCruHG4qi9`R1!0zl}w+rChye$BDC(KAv` -Y1%Ad6BM(5}fZ2+-GH=&Y6M*iT3j2$gJ}1o$8^EL>J8{y<0OFaAUDJh+r;dC!9Drp@!5dubf-xRziUL^g2D~SHR*J0|yJG;3rC}9(Ij6b2@|d6{iw;`y`HV*5-w -W&wYH>kC_$HDa}i6OZeuhf7!M&6~9B&LS5nSb`DD}B -EM;@OF0C?Otf_e?dco#}-q4rh(Tbmk!T);3rh+G{nQ)f4U>^t>8#?vU$GJ=&0HK>?Ym$?j?_Xk=kY0y -+-@)!iFIitxc0R@F^G>zw2jNF7+eln>{?z1EaE$SXf-xNnbV6r^@LmRXI^|PY$ -H_!Q|;rwj;4>%2iHku6Yo`yKN+wDLYvODk)(5E7%?48>w@ZBVbM0d~>6K_v?=-tucU0byg-t67>n)*B -iv~l0v9|w8Y7QgLwHpYySy9je{9`<%H+y=zdo}t-r)NVYO?r)p@Dg~6}dlm^yb~v@oOC|fijZ?$vp6b -cbwWR*J=}LADXcZsmjQ1W8S})VD14&F{aP~$Ix$jmJ(~g3GZ=$6=wehELX$NR~#sbm*6fXU6?Bg=NYm5EwT?VjU$A5AeKd3;z -Tm}>AjQ)cP^y4?MJ-8$qlxapu(o^^d2Wgyu`I;YWg`Ec|+yDp0DH0f8gxfQOugl!v63F#t$Z^m-#^w;u3Mww9?z1{aJ -VN=UCvA!9P;v(oi-!AktT!9{nPN(7VsB$>mbJIAk;(P$g9=qrJ*y)>P#8mCz_7QA<{~IPIQT&f%FuxK -17F*p -Raxmec}&Y8W9NDI4MfvC~s*vc0NZEd$^&tufK2@dwb4P -$Q~;5FUDZxzB!k?J-yJk;X_Wn1?bsZAOEF&;>HhurQ6)zcMFO|zNfIalab2;S;BQHOynEk+f7@C}uy@_&@@-d8;Vl{*YeF=xApBWwMdyDGd5v -YjoeN*4Db(5mjBNYp*K_QeF3;*=yPht0;C66 -QlD)aX=_U%-b1sv5`EtS2X}!?H31@BY+Mh4I8pB3tYL*WYp&bW9?whzSUey6e@99y7+GKII8*gQ&RXS -dMvU5`H*+zwN&(0>*6hqRGNZqSP81(QC4)3#~gYYR?&cJ*UhCU{m^#wgudSk1PK=pzV6?DDSkmE)*g9 -{(sD9GwBw^FOZ=J+r+()5(H?o>f1;K440)+N^tiBDLh_bYyWEW%`Eq>}pDYe{hir3zt`hG%h)<3b)++lz1($j^4E+WMh8LZ`ItAL!^dPuBJ|-qx<&g}>=N4=x -=+m)jGJ{#tDO-O~;DoyXgEPd8R}pmrYf5@mwNTE7$^Bkl6m*iZVi))-&Cu0jKipJuUyvgXsP$tZ}hx!PxK;ec(z`*~E?x{W--^{%f$b+#!;2T7G(!1kW>ZV~XdZZ$xEWoejgeCoh3M>=eeL=97(?$L0B -qP950SEjH_2BlfCkxIG?gUi&@JX2vH*iJ_fFcpq?8seIkiIV0qGY7Yk#UqTo=-Ta+W!!D3Yr>XcG%Ap -W6>~GAu}bV}DSCx_{h_hE>@if4zDH|A7#B{gjn!`p>LyM%da~K7!kDYdd5fnX1&PNV#MO*EO;O|+5W(qb3Cf)aMl;>ES26Oo51~bSMXsxNO*M(I} -YY~-)ZSEMEbzckV6ld^c&-Oz7XK*Xymh`0Ol?46=FBBwD?cyRBSQ)#|1=Jd=j -)G=qpquIgE1H{fe0BMcmOA66k1AP*2l_R1CsOK9I@B0H|I*3gaAx1*I9m0gXpykn`uHn%bnJkA0Jpe4 -5$&@XYU;Ipw%k=lN0dGh8S3Kn({d#^S=rg|?Ds@e^p@QLyXz(Yl_&+n_2b}ud*?!2ylIVu9NCYNe0z( -K4-5xJ6Ou;aO!=FK|g4``p_YOeWTblQ!jQvZwUAp^U?JigEo{YPiFaI2B6}t--@~&9lQ=#Mz%g{H~!> -Apo#n`@kzfsPO^WoIa2sd=Lm#q9M0lS@#y;0r<&-T*9Y)4W^y3a+Ddro3wlp7vIv3(c{?gzj82!d}9e -dzrSzDo|buiI5X@qfd~+ktEM#rj*Q1@F*;{AQ7_Gev4%#=Am-A*4O}g -+TBt4uf22`dc!-Q(e2ye5lqg2?0%sfyYs8~wL@733vlExastowHo+}jUhhJ^jl#Ws7{8jlcK#P}osEP -+)a(s{zV-+D*D^Qvdj_h{fa&U>?ke9=P<{S5_Hmv6>N0`fxz2xenZWN{=fAp4;PdPJeZ&aRT9bLsMLT -i11BRM8*}EbfMthP}PfsDkvZZH}CZH>#4PmJu%L5zYctzp!3I^*97+%kbrD^3H91Yd!j=sBe(whw?z{z7J0q8uySk21NZJ2^r&}kxQlZSkn?h5Z3YH&XhPTC0^gROVoC7|nkj|!XW&IZWS -><0Eo&|Hgq8D&PBrc@nHwImtREuut?wTO0A2>QAlYswqamrb0M)8o>k&!Q^>Cbzor1SoxQFbBCz9uq2 -bgu$sTD8e($n?jg-;lbB&x))N#Sn8y;%@e0rXN_42CNJg2g&M#f*XMe^UES~?sXPj@D{&HxmOzroYI_ -KQnA;h?^zy=^iGl>-P*(G!(_c@UH`lOu0T&tc)7x?($aT4jB2I}d!O1Tz^HcuGqrJeQ6gR(ey^pTO6K -m0{iZUKk@vOG$GXw%@*A0|g-#cT&+$hUysYjt8JRj}*^lUxJgsQ=+z4ky=u@Y!JRkE{uYbg|&qpN4uFHnZpSJy&Q9AsqpGjs#S*EMua&lY2hEsd3^ ->A*V(oH{+7aVAv{n&S++f7u5~xs~EMHNtU(2dblu?3);gaS&;lCqct9&hDyigz*# -nehGKVI#(W`84Uvt*zQ-3V4t<~7UcyD5~9I@KS0>0rPF}yH9%m$t@=e$r1Ogb(;(#Wl>lr4HoAO$|+G -&ibYRvQZ>y6Oa;nR|Sbv95Bqx0{lL2L?^>))5=57(7P^ksRI1c)AyJ7=&`czyN`+dETmv(otX&P4#(3 -9FBrRNg1Q3b8-PaX4~UhW$VJ6aQ|{P+kyj&IBFvpHy$yqp%=+T>G{&Xr{qg7bUd4Br!ZdCbP}Yy|VpfYs#F-$L9)qG<9+UM7ML3%4<%rviSn|*Hnyif3ZTz9ZON^SQ1 -Zfw}V-+M=M{Yu)=MJEFwLK*h7($;o;R*lCBqam*M?|1TJ{RD#&`#LH@LoN0L_QYpG!B9VY347d8y7sW -D#(R?JWx6dG=7jN>$=*THe6zGt_drv}y`@qog*Z2bOSU=V0n{G_)pY^>lGwm -D35Pgu7(ADzM}%d^2xm8c2D|Tq{|AV1SIV7s`0_sw9sjebe~BUgZrUF+75fey2%->55qs$hLV^?l -K^vaLKJBTS?65C?Z>C|{jtIY$oaE&0&H!S&SJH+EV`5JkVCd(56X-4wA>keLp>I2hB!8y}l09#La?`` -q$yHaNRy08Cj>Kc>(o&0UTB{qDcif{GSI(NPoEJt<$huKX=Wq(Ok$fEtTvFeWqi_w1tB;B6@7VzQl@E -br9*SYeePx)qZCqu^d_dq4^fhsou3F$8LGpKwY`~I4LzGUE^Uh~hF4E)n;{`r!D-@oS1b -0EMENf3!35;Lq&L|<6_USf@zK5vd8Y7ANnDvh`m?OYJM*BAG$q@t;J1|Xa@u`2mKir^ku9b4-p2IEXe -PhE<2`}Tyxm9;X=uW}&3S24lYT_TU8U#Up{WOLMky!cR<5%eC9EcR7iY1bhZHD^jUT2qGd4w}~r+;IS -Bzv!1u%O!J&Jh;Tm^Fw{|(LtJ% -3cCR9Nhqr;M-fld5}Q5hl8|F5F*zdiN8j>`XH+8@dZ2@s_yf`V}jL`Vd}35Xy`62c+!(|#>^yvHj1-8 -Xo5(0nI7_7-yB&hj^^oxkmOU}DdfWS@6B&xt*|5hHI?>0m5YzBS}R90G-tE!d;q9H8`E;e^q#O`avFM=^XP4>7P>Aw%>=20u -KB#*_D>48$=(f+bzM_`j-vcX#}7nHLz{v(4F+d!>;jkL_kl`IsSq1U`;W1c}Z{fp`r%W!NAfx)_8J{g -y1oRNctF9Ko`@7eeinl#P1=dOjtx~$2Pn!bwOP#2`P^lKF#bwWQV*uhIi?1M+Ayq405zv)qeucKPjX! -XrH$~qUOJ>!ZyE-WjseVc*ptUq|vTm+WK2}{d1uIpPct2%K!G9-$!=}gbq~b7Phl=SC{XdQr~W>+dJ(Jq|ke-x7ZW#+pBQAZzSh$p* -Z*k;_16f>NY;k_B1+??5?I83D_PKyGz{PXV1HROBe0GxgODx^^$s7LyADqmwvWDdp;PEZCcgF)cq5yelJp^pgLq -Fd6ulTefcbGC2=5xc83j`t{hNuO4Q&O{|Oc&yD6sM|Z-FRDW$W&p;omV13_E+}d}&g|S>-@a&p$T!-O -+1W{gFXkK)V{@8`RyR~q(eYv#Zc?w%$_*~)LOv#mDfGn`Pcjq6e{w$@bQ0Y_^J(B}^$#v$5>G -UrwPJ$;=r7Q1Y|ht4yF+p55anduf;$`x^|=z7eoh0>)p6LCQj3cv03ov;ccT0p-VyY0k#8J-K2*3pqz -|P#sKt5}-0mgAx;{GeGl=yD;pRNSh>Ij$$%Y4{&T&P)2X6<@iNu?wbpDl25{16NhY}Krz<8jKEfBXw? -)aw^JRjO4er*|kwB0(2SD^ME+40V->S=h{k3ePVLP`ToiAEi4jRg}UI8N_}AY~tSPdWvH@k;epzF!e9hK1E*cR9F)6(T7=&~Ejg7{bt?iZ{l+=o3y|c~B+5>`pJ_^>F07^+ -9ExcW#54pA3chv+e3vR$@3m3uj!oTEHX#TdYaO4`z2n^4CX%JagirBdbz?w7(lkjGT)ox^x(Ue`PBVVc>My1= -$aoo{(m!NW>X#t*D43^CfiiF6$fucdxvX;kkk8UX*vT$7;ak0UD%$na&y$=0X?OS~;WE5KqWt$+bS%@ -=9P%4<^hjecs)YN6-s)f3OSbvyo=yKqC#Zl3wOj&IBI~+}qMz<<7917acZ8*0+kL6e2Sq@Ju;R`{n9t -{$vmYnNp9+)S8a3}l3vCJI>g1RUaSH!x?zqJZ(aKOc8I3x4s*eSyOudomyhr#2#r07u&Ou7KDt7rpdLa?TlQmYBr!T?vZmYOL{*iL3vBAo)h --Hpga-5As}Wh4I#E7e#O -}KN;*LqyKEwV(h@pStuM|8A=s`7s>;lJCYKfH_Bf`nde6dBUYW43T?3PJBQV*!kNll5lwyf`~VaTyvp -YF96A&2Ah*#J}!>oBQM}G*0?*5m}>VG)=_7LJsn|HQvMb9>!4W)g1)%5m0s`>B};`{e! -`M`I-F~g5#(h!Ei8(ZHH9fBdy?q5m3BtoDYe}_N}gSP*{&kOXZeKVCx-#d}Wo~X~>EjyBAcLhq*eY14 -uzwn+9Pd$cMeVdMChjhg^8z3v3!X+^S9) -&Nd2y5Am#k0o!OPMtKbCzECfyS3F*{(6eXCcP|!I`VJaQn4~DdQPU+WM<*moGqg>lf_{w>_;lj3;;tW -b0MA7}dtF_-@uXy8jSP%b>8=Av2EOl^>ID^mU_Cpzru~!*BR-n}Bg`FY~>ca=$tMy7CUVFnvu|9r|kg -`}G*`_24V#e(D~(JO8?0NaMSEEFYP!Z;f7U7`lbNIQpxT5l8zw%52h=4|OK|(1D+J9r!lM=(#egSK&+ -NY*lw&FSYzYtp>BV?EoyViRJHRmjy%cnndXsA^1-Qeu{A)I>}2v6-Iq`8a436IMuMm-x3l`$N{wPHew=$-6!{m*DTO97Rb9t3cEAhC7VSh7KQj4yA -^wMp38k7yzfJYIfq&4hQ@`&kA2AUQ6oZb0SS%B&A4^&fM`0StkHEa~$;^cQnZ_|Gb@lV0_w@>5X)BvE{$#2|(e6h@LbfkDXL2M{B%4Hlv>Mtr&@x^dWz%p%|(ZZ=Mdo)e^;Qu=)U -2&h27owxA@!`717tjXR9dnd8mDHQmt4uHkJ!@31=JbQOLiu -X>`Ta@2sA-}Z$EB4Jz;%y_bz1c2`hjx#OZRGaqUK$PVbU%;xSd1fYj|ytnO`#is{@a=w;k?IF@SEz%; -WL$ZW@#lV7#+O&Ov|`JIT!ZN#Eqjp8qoRhCr4dH(Czvqd~2Kc!Eomgps&Q --F}Uzk0t@_t!440}T5!#1u;ejXOPJle+OU)Ee$~`kI-fF-LX8Gt~z#MT_~u$#x>Sg4^~ye2>ZB3H5K#A*k+AA^dOlJk*Z$!wC$vI}~E(b;Eg0CE@9_r}i2AA}}jxzdkcwm7Lcoh3JDrWZ9 -@U6UX+R1jYGKU(WWKLr11W{R?Dru@zlFbgbyNE~O9B-Gc$!7srgA#3@BO8}8D9U=^-tF^2i`lU-&xnh -x(~T^%6(dB)cxZ;bq@WQLIfp`Ua}w5&2M*~D4l)6GhB_>5qwdQ!3GB>zpdytub+TcNhupqgZjfA$IVk -#BZDj?kdgoSNSp$%d-7QUT#as(u$0KlUC#l`dg?iTKbHMAccx1;)p$>;A^YG{(6bq3Bb4HvhSeVxVnB -gH>js`)pgLKcDA@^-KQNZ-xq9z{Hde0htN0EuniN)Y=kBIi6Mg1d-)(8*QB;5++)T*WUn##VzMXuA*Cha{IK!5Cp^0)QLKbI#1A9R<7mPd+;Qm-$s`*`-y -Dn2^}DYP4&_$7grW)vV!A**R)QrzXoM$OV`EVHhbSGTxLceW#JvCbbxBXl52-l?+o62+4g!<yq;vO3+5m(#2mD(pCSFg4}8i{yKy*>vxlJIgINgm`Iu|^7alL(Z4yaI5oBhEn+$NapMbB) -i`DRCoI&ZpCJU9sx{U%OCW?HL!+5)61dT~`4fyv4HE(GRALJwHG2Z -GUa_=M|zezV;Ere~`-+l+t*8h-$1TL1+6a#fg`{ePUCF>OEblekhq3B7G!WX$_UmnF#j`(z)Wp=snd^-W?REKlYt+&~!7y-u8VhaRD1?!{`zg$Nvlqs -v%$BoC8R~Q|p>M(_K5TH7oc$t{ufPv&GM!_|QL|oL}Ir4raI9@?RO1g3|ML;S~WlAoLV2N2-SRM@;AC -8YMAszL3;}Kx$p1}~5vrC+AW2w7q<7AJkAFUDz1+NORcyG?xJN|D&DE{ApP`^CsR}hN-970hbf{+xlp -%H|FFcL*z5=Jo!#}I;mP_Vgz}2H!JpDFUgx>lE3tfd5_ot)81j`O(pl1koj&5gMm9RLZDsWLlyg~P->^ -k8Szeq!27$V_`aZx3vcV+{!wq31-;$Gu${2}EhT058ofyT5kghQ6VldJ5i5ZUt*kXK++VWG8+ro%6Zo -{FC*W7`$#(x9KJ7mP{|S8B(G&0+@JZO=)35qF0$+@_oRoV=r}1#jy;}wur@pcKeAF-XGSc~7A$#xnS{ -?SnNpRP`n%Df|Uo}&WRS(#Q{xEd(V=Z1Cmpz9`bwuL;y;z!o@Vl<%3+CKdqmy3|M=0{-p>A>j)9o>SD -H<`$fFBamYOL^MrT%wp>; -sc7&iZSzN(|Iv20>)34{%<> -PhO{AaW**`q@TUh()ydPp3NI^JAQ7DYUAPG|tiDD>)Vleh;4Exf=9e?{BY@929Z>4PvZ7)84H)h$mA^ -6U0W_#lF(-^jwv=F-$6@=_DWe*(bUKqR`-{+3^jW&eX;|}!JLZWZ4Dq;&mzf#6*Q3s~>Gj~^n_cmXKz -BP*QzCz;NjEhSCn?U!R>=qTbm&62e}YoT+CV*n2K>@8%*UXGPxQ9q={aaeKg -Xd5X2X+4CCT_1V3ySblwpWAbiUHUAXHw(qwn`aKPFf6CeZ&Z>aVmi4!*`pzI4_)8DRpA4cOSM2<-hm< -(ReuXoLxJ+({9(dO~Upb6Q06QL|9OA)4zG&B5B_QYLG^K7)gI+#iJj&vzDLgYKJyyw+5>YSbv~nF+Q^ -b&KNku@oB*uMtnEvUBq60(PBvuyOVM&h~Ehe#AcPYe0bk{vCpAq%Lvp+ryfza>WXbn|`0M_}Wt`l{y; -L>c|q*=9AXVbfexT4hzl`b8jb5pq+3|)Io!v(E9WqsfJ;1Rqx_UQ#I%GHG(@N8qWUE(q>_j2ds+T%E| -x~j5mk*B>?J=sz;n{XyKEVV3AU$NT<8c7by0bm*_vO}xj2ekHDM_|D#?RNPrU*%W-d_Jcq&7wTl4ZKR -zkl2qB-rGjTg@t*I=U4%p*s1R9Q>6zRrNpcH%5B_u2$P6~Nd~qCq}O8F9Z%ljlnSU$waA=4d1+&clgNl9cIjV}c -(jhqOO^`N00qqVm8$Xw=SKS7c9XKlNqzx6kym<@-5wzsaPTUn2e59$NiMO6)mE9psLxp#E`#IMV~C!TanU} -@lkq~$-tSn+3GqFKm3JV0|+SyGh8J7G)wHb7Vz4qpqh$H)Yz$@ -a2kF27C=!f>Fh9XA?$yelp}bvbo?nr=?Ur*@7r1wNDc;Im~rrwqm3U*V;+%{IK)Q2`v}qe|PHsiY??W -S-?56&$!MvL36HZmt%n(+-*P7lz36$hZfczCy_rsHe0WY6VHJ#%*3nTS&AG%bX9}Z=YZv -hwZUn9MAme)WUgL(m5=p{g~u;h7Zs&Q%(f%dMpRea|Hq}8wKU6MddvHxkh(4%?xA#>+}&rns_dAstcs -ha$@Rmck}JDH#?JSGJq=|G2|RXf)1;SUxDaw=I&N&LE_5`37T{Z=I(Mbt;I>;qh>sG7%r8NA*0&KrBq%D{JOrf@5gcs@8{|o!k9At -5Azgu)~5b>8brA4x9*z)w9MTPeI=3QY8?kDf&TyNaBtvP-7?@O^AC+?i|Z6LEzU+|rK*y(e&OPljzuS -j`&V7({&`(1ltr<~Nc@)DWLPSqr?SWNJrLbWoqzO}R4T?Jj%OJRMLQDgNsMD^EHqO#J>2VjzF!L+Xol -YPNRH4vVyGTJ>nzf*jLa5E*Q -bFA>Ysk@JHduX_8Pel@WH-n~j*BjeWL+YQTWg?nD{*X@z{A!3f+lL|%?BL1>X=5CY0Q6PWU4E!I|Cez -0Bh?l921jjG$Yz1ssDxxDeWl^C>!4|*c>&oyL(UjA`=S*_HY}k;JjdFfHo(h^3wYP*cDd9mD7I&%#7E -v(AYr?oPa&%1+94=G?YYj@p@I6g#lM9vEJ;Oj*$uGqDq9b=XaERq`gwOG98YKYsvxR)Mm&YyQc^FSM& -%&nDO)_yiJb5BD>Wb2lr2*d4iGh8GRc}I+)xYHJ?qg{fJ51e~j>Q9j9plq#jm6>?q==% -7AXO~O{il`|IBRRQJcBN4&DGY1EcQ{`mxOF?QJLAe2%p*vxw>m+*8qHul0i4pxfUR -!+Ln#!lp^2+SK&Yr1Q;3g;&XaKj{zCZk~qj|t4IrKMc5=-!5pjXB&@?IO8ASN>r?yaRsN*=rc^=W -V%s{pU<~jiJ8W7*QoXdj6=vo=#WekP%WA$ke$*zvz1n`pddggY;#>h8ba0ou8L}#@UvO^-bRPN1%_a> -fW(aYksL9@K>FZd@d7}v{V{8Z%_y?JaPwSY0ouauH9J>y2*>co|B5!ou%!=2qnhHUvsY|{uJ4JnDCD8 -?ksJlHlPb1e816|yM22e7S<+&F`1PGN+Z8^ypbzT$g+|jgJXr{WXbA{hWCNhR}$j&%LS@KdfwoOyGm{ --8#h4~ytE8&yU&ano2M~OCaYkD~jl{=KVaZTcAcjcg3rY3P?#vM=|^F%MLy33NNz+D)7;VL>vV3fBD9 -U|4j;LkFAA&Ww89Pji1H}v9hJr_~>OBfs`2(AhnF?yhHOc4a8lVcI~>56v~SK<1~k^J3d3=_Wi=QJ>u -vf<#iGRW>Bc4B3Nk-wT?{(N``IxdZ>2h93bD;1ky_M-M`?dx3})n#YNQ0Sni#B@!Sn8v43-*hIrQrFW -%SrpKN2@g7>SRMh#wS!K*?>UVUIWU(H6Nd5ur6P=tgVCSlCD>y-#+BmV10TD1;A -^0)P(Es|F(7|+lpdKbk0}g-1VM^9)=(&p!A(GLpr6EUikV4WM;k0TY0NnHH3vg=yIPOCnEMH_{)RqQc -DG6pY#QHDcUy@!onk?yBz0xYbRF0l6r$!n)gq;VL_1HrdWh_+THW_qbWN#aXc!A2A_2qB`J1b-UcJvl -#|v}Q7N@?ni7&co|*+v^D?Ng1Sat}0?FxHVV@WE7+u4%dum8Wns2VmkC>pTeB8v?qjQZRE`ew_C8^aH -w(!CVZSY;KMO6iL9qGtU;wJe}_wO8LO4*;X#1}nPKU9SEIES=4$+xU3lGGpnI)PldTVH(P5C7R_&NhD -6woy=h;r!13Voq%r73~-Ezs!HXHNLHve=@!Bfkgh>_x?^KKi>DJeLn&q_`)0sgd!;t#%UC%F>+N@g)x -LiX&lf9Mo_=3V|=BJ=xTUJtdnDF1BMT;gbv%BJ@af2~4<@TyBMOQaBrY)xwv5ItRJw*I52(DA4y<{%I(`dHj7A%4ZdnZ%!pVbQX!`w5$U|foBS$Wpgw&>JZ -D-JyDaaygr^qTzS@nAuLviM~uzTiIghUK@MC=S_O2RD8Gfl!NsyvlSP9y>+;FUHwr%RFbTlr5J^tFU! -XjWUk|E!YpgwzIn}{xM^NV!`lwp>sRW4#PF|^JVcN{kIHKSN7GiTpLHv=veBuTCNd<-B91|({+Mt7%Xs`RKj8x^f4KY4wj>k@Pzu0 -Heve=&);Dh`srlK5`Q-&=vPb5-w6r&7}H+~ ->BoLS=uiAijLVH6yZK(ZHI>=dORoFIc;8|t(Fvqbv)Vq$=l9Xhi54DN91&ECv2hAVSu?*J#JokNX)h -u(ZegPE&}&E;dr_0?k(Sd#{#ta< -Xvt9a?US#<1ViNIH-_z#4;A$jQ|^_l4ibNw4kAt2IXt5$Y{ohC7Vq9_1KTq$ -doe|tZHKupFSjp-20O(wdZ0nIA4kcl^Xn -u+PC-vOd-W<_E4ej0p!I|QLW0hc&0Cq?q_FUcJzMRG>|qNRY#eT^4h`r8&k2WcC&9xfom-MyHqi+h5p -D3iAd^x*$Sbp?rs6@q?~?&Gg|lGH`i?T;2h9_Z)rLrCIb!$o#M>WsNPJ7<<=y)YLj!ZMN`UpIQsTFxS --&rew)>SKihldJH>Rw6W6-uC#y_91&Ri}Xvme;m=^R56@>fHv3%}tR6q83gB1>tPx^QSjmY+DUhc_$M -JI5gRU9c1)??wv0r+a(2CP##ysg}8vN}h|j=>pzruP+X}DOg<3S4=c1t5Ln=mMMp!f@ZlX{csY>Km;& -a8UT&3I?r$W7bte=^Q66y^8+!ixDgQ8P$+#Q&yjr6kxLqOm|{SRpJLTB00n>?aEQ^gmnI%*p0R -5c)P@uhGAx=xa;T}pZK~CW5)+((+cK|1r?|AvENKIWAJ2ES&rY!`jIXo}+}Ar3-?w*UlSp}4>rY1k<@ -NVdLSkmpRiD8!FNW|)W0_-7?V=zK>P{v6xQEGYZrl814POJj*sdz+j}B(BZNh<#HG1ltge??RXa!aL#vCiu48b};RdIbH3A{%pxmEqS!3wV9h$-2K#I?>^ylyg?nZn9puuEliQ -4nt@D;RuGk=Dd4Y+Df^q(b%Etn8U#esskqVB$NPEedQ7+O3#u69gV+riu#KaeiEV?qsuyGnq#S`8h6g -{Dj$uQ9difH0gpUWzZM!qlv7Rm_3(y>C<*nk1F2Y4ZT9e^OFv7@JuIJMGACScV0Z>Z=1QY-O00;p4c~ -(=H1gJfv1ONaS3jhEc0001RX>c!Jc4cm4Z*nhVWpZ?BW@#^DZ*pZWaCvoBO>g5i5WVYH43xtM`Z86Sk!A}DJ>+a|r(78=1wRgWE52P$|nXabtxt7u1cy>$V -sFN&hk!h5K;|PJpgAgMO-J5CurPS6U`uZBwatdD@-owLHj%;DkhJK**Z -^3@@1B1j*NYD$x`9~H^z&Nn4}rK2=$zVVTvOI5R|sz+4*9D~Re*w6`1|5b5Jeo1RSg~>aIo9x -<>Ut|T3=n@orGJ$!^Uecnp)Sh&Zs*(T<)v#sX1IHvTp!VACgodMVN)S!ew3Rn5^77FZ=VsdDQoPI=<$ -{xTU3Ck2dJ*RJv`*@?P)2v#$Hl{Ek#W4)e)0dWSwu$z}}dC-Y20!7ERJ3nXXcjggH$vPbcaBk&{)yqnV+_5N+4lRGJ(n -i;zKN;U`A}GJS%IBSo&Vn=gY8Gpro_SYY5!o^GE7pZ(;`y2f`E>YAiXF&D;eY+qf2&7l865SmCoa3EC -A2TO`e6edAU{LeY1R=`G(4$rX02&BDAFtf#7!22XCq%FejxC$DpH8v#loc4J#FkwaKQcuK8iqvPhvNh -pqma8|^`Z~~Sof_qARnkbP>LVIyU6m(RxrXvQFtb$8ak8-a;8qvV6;aOB!klM?4(j?xeG`E -t7EgO9Pj9-OmooVBqVTk43NXkHURajNlz-JZgMYJrIMR1+S0r)ZTD`E&J4;!g7=VlRU&wQv5$V5xJc6 -|LA*cT`wDs_*wGVc(&pVadgp!C23tFbXLNjy1y%EXa!H$;AZspw%mqvtQqpR6oWBO`_^CWVpKKLa%E! -pM3JwG)|_8w5rqI8uF|{lfJ9Il)^Vkl@}u?I%0b?t4V`PS6*BhUv9acqzGbh0P{x04vYi!1|HkOVaA -|NaUv_0~WN&gWV`yP=WMy?y-E^vA6R$Fi4MihSMR~&^0gGAi0)k<5UR3Zr^R=Gr68bwiL2 -7Ca9awlaYQ6QD5Q+9*Cx3<5X6MDUYTgkQ^mhg-XqhyWFs6@o#v)*!?5;bt&}9%@Z -zHz-iC2=0=3#1{na@dXNvz%;~mPp>e5`8LIMZ6M=35a>#xXNF604#?@;`hG^GX$L;J&B9yG*Z`@Z#*B -?$fs8G%PC;)WViaA0bSuY7u+KCAE>hZ+j47=Z5mKTX9z`%mNHm5?)F1}3g?n#uJDg6SJ$Qio_Gr`|Od -ih3)~qC*;xlGUd>u!eh$$<@gxONyM9}Yyu5Jm@zVv$DJuMxFiOz4FzG#?$JPDZi;^<#FTa&U)Y)pbTgRQ#`p#Wvzgt{vze8D_F#u(JDa_VG -Sd?cDi_)?V&A3HfE^~!>__bNfvsJNagy_d0_-RyRn6(V#8eg<3OWD7*2k8l2w&S+cU~K?l%@sFE55hO -`S#s$X&l$g)GW`w8}<4Qh>^zEVHH-SiJiYaG&S8R^r9@z+G`UxL=4`*6`Dkey~!ey8VPqO9jmhsHo#_9}&~Pp{BBo^7`!#d~PpCLeT997Z(S2rF-k$-1?Uflg>VDg*u6hwpZfQZqvTiVkP -JFV&VtN^%pCqG#WoU)31}K7_xnLNhS*^LLOLQ)#~u;o}=##y#BQBPi{w@@$Im8-6}Pr@FLin5-3OycT -12+ZczCj1KrfHRxc4$VM>C!=OU0By}zbYCaXzgfz^~Z9-e;r^7$8=?1pBCZS2?25jK1=iWPjPn#9RN` -fNz$STZNFasnn6uX+*}tRFcc2jy%Y-ctfW=N3@V$O`pG7EYc`|kY&fZ*L*-Jsw#|n* -jZr6aXxV?zjdLFR65G^>({yZLL@sy8!&hohHU1s}K`3q1>0|XQR000O8`*~JVIlqJu?>PVf7J2{x9{> -OVaA|NaUv_0~WN&gWV`yP=WMyvA=%pc`RysrD3F9E|RR1=Xm&sx!G&4z3%6Czx(a~wvRvk^OG;O&!2qq=@(yrx -_$ilm!JIi?LT?%?{0p#J^SHq`||PrZ}+dCzuG=|czL(ozuErs@W1X~y?P%1{_)43U*ErY{`TQynYk^eEjL*(SJn -4+gA^d`TC2;yXSB3UT)w0YK#BzQO_S~>!)AiQ9r!h-uvYDTRG1on)uDPf3SV@>ecp?_rBSl-o3ed{QK -R@9LXmSuirl2fA@0?^Sw8}|3kd8ZGUm+}{uVuayS;tbzWVj;4-c=e@yArXy8rI+`Q -xw5+V_ulciWqX@8A9s9sA?<>%-66i|4Pm$GeyJZ?gHAh_~DG*DpVOc#O~g`0(=n`(JtK=hrV|IQ07M- -Q$mM-p%2kzxZbR{O(Qkbo=w&>$}Go%~wBv_v-#deE9SG7k97U+-=X_Y(MeZn;-r$Q=ie1udj}5pFKqL -&h8(#yZiV%&;I+}2Y(-({&jm5tMT~mlOO!%6#QePFSq;GHv2= -&ef$y)#z21wv)aD9+kSp?_x;bWZli^G@Aj`xp8d<0-#pts`r@zKzkc-e=|^8Y`|BU$y)o1A*I2)7>Hf -!`!USJ#F{;PsuiyR}9ghb7^6Ar0{v|%~(Z^3dfAZ|F^!~Fa&%XHd>#w)ZzI?iUw0-r_(`QdU`R4PFo^ -D@#^Yp7Pzy9=tZTtG}?%fnKnZGs3|7VK7k0yV7h?#tO_xAbyt2a6HzsB;ui4MMcx&83`?{~4xFYfNcq -PFL;MSlAA|7#6jJ-q&#MPI`}pFO|-<@xLX8crrQ!t=-I;#ZFkKR(d^*oQyAesTXQ-uT~sc>DIJKYaM%FTecq!QI -QBKY07gACB_jkMWVm5C7TuFZI~A_uv2LZU3OxI!nA%$9(tFSjXU{QR=&wR?nBpUi8eJA3yo?`1aXfzk2e?N1uQD -rzc-Od-~+#Z=OB*@{4bueiZ-9A&gk4Pkwm*h+&0M{ri`XdLfr88~Ets&p-Y4>nH#7Q@Y;Pyc&-E_Vn9 -lUw-@9(@+1$w_iT})2C14@9*7&(=4~;*yG38=WTR(+?L+TZMfy*HvW3XkE8P2ydC=#~rypC72iIE0?bi6`IHMme9$3!FW443-^4mCWqs5(poblIIv)z8gJj@m^`?nHLZ -zXy(?d6JgYmK*-=vzFn-PX=u`{A3TCnF}U?PuYAoyoY`o&6@}$jNIAccj2`vq -`4Ml~VtX@lu^6Q`x>I@9H4n#r6T^s^*$2bjSr2~WS@Evubm=u(V@674_IGD`u_B%R#?jV-zRjn|<&j{yKN;p}0oz=+7gP#A}_#<1M{og)`>sjQNW7E!l33bt@}g-p?={XSLA%Jtnjq-SO=#`_3mBNQ^#u6;C*#wQ -a>e@p!;TKqh#q0E -I|C^gQHv*R?Z8F`%M#WSQxNa#?A1}P{+{(FY|yF2D}?=oIkQco@o0!8X5UZRJ;J(6J!6*I9-VG!XNxz -}{c7)q?bbckZPB-GkLbdTZpCad=2*s3Vt!*~79Zj*-3`Xf9nr=S>l>RE#}UgN7Cy7x+HYduv|D3VSB) -{(cz?mBW7+96txb1S?Bn=rp@U(2@oRLNg=!4*C~o?+v)cEY7)zKgJ2l?ln73G{5^EG4)YKa*&TBEX(N -(rE{he&TuyHk_IG}?`BtrOFo@nE)E?73LW!9E@iyz-dd@D%aeL01b~$2mO0&O5eey -DRovXG1Q$)5)5aiCxC>#IrEslktV2G$uLr4|X1}#XB_ngT;%1jA%F3{A%|&ZsH9@3HK2m=seth`s8RR -)*VABd=e+YZftC;IayFV%EaQ`HMQnEv^~~sMh9aAEG*;e#hK<^v+{(Kd0>(o1K(`})7|kD@l?Vzrod> -{)o$IxpH;R?OkO3vG*&AH6y4G>+vL%)k$6lPyK8yGE{kOe19V&m%Z+m{x!+owH_M_1pSOb -Lno}EW)gtmD8!AuiS^^;pX|2zpF)o_>lt&JM}ZM;kXj$ImVpgJu&AAP6Oh&voO_fC_`x5RIwyDy9|VL -bb_k7K;duPYu4aIBcJ5l;wHh)$RA8R2>dJ*QpFv=CoM%t$ySf>x|x{Il`HXI?Ov7 -2Z||;vDWnK9b7V_TQML34!#ykUd#9UBi8MV?w5lVtS&7U7lY-mcu|1@Yq`$c7d>!EG|4;=$pe_PQ!$U -lk}Z~OVudVd)8!E7BVfcpVuaE2f`1Y)5l!$YvE7FG2Q -C^8LXh5f_4L!ge?=iSkrmnMevDJH^7P$d%;Cm7k(Xk?Dz*L*SAyQ2K`H3|5P7#T -1-)EOswl$WVOf8Vo#m1$<&}SnXkxgFoax?tx1p*kU@wvURY*aN5o3X2}NN0AC+C0~2S%47|sp@iOtc( -TQ+K2K%(L0WPT^uz~L?toWKX9z&ZEhr-w531ji1HKKJaRMRrfShs+I7)2+v8N6HKvk|zY(S?r3h^W`t -2teiNbZ1A$V+gk52f*Trsp|AMS_5>2!4~E)I^oX0`rGl7?uKqJK}rD5&Lm?LZ2~MNE7;*)o$RD0^p1-42C9D;Nc6vJC+z1br8;{TZbq?>^1Du=~yzta~Hob;63B!0G9wAwo|u31dG -Q5&43ex?tKt64ki@H$D?B);P2&(I83_Ah7DWHb_17GTVp_BL=33u8W??aHJ&hWLk>JXNi2&I67FKE0# -;)WyRWfpE?hmqB{3|ZJ-r;RE|7rt3;Yj~l8TAP(lBuRLv`kGZ_6+aNUP3Q+HYWho6c^Jg(4r)1DL!R< -OnUD{#M*(3nPjS0*rT!4If94155=PW`rZ>VFoS&}?}o>I7i -nR5q6k6k9>*6|dioTsel~%n_8YxEbCt`~z07K>7*7n#OXXPK+%&*xjjmFNes@c7*|@E2X>zM@$88oR^XBiA5)2Vb-8CXHlB|8$MzfK1_ -a7!%FQ&gFOH@2VQ@sXiBZn|Y8PC>9*FmA`JHTxQJFs#04xBqF(h*4SWtY7u?hGy;$yMgjWOVFkGi@dP1 -*mRY6QAxW70d)gehG(rT8S(htJOnN&xpATkF%NntHe}KUJi$zPvRbEx-ErTfOozLH5YNPiui>|}vjbc -*3^HNF-SnWJ?g7SEbR{@C)6bnJ7(AKMpG>qAhXMTAC9J<=W!}jkxR7Jj{ -LYfQA`j24T}^@ufz%lFv@lVmk{J|xB&X9!^1TIFK6y8$FNv?m?5U+W -LjFsDB0VsyZspT%zbqGX(=qDh(<{@xNuO@ix_5)Nl -I!!h}otQLZ@4;&6cNp^m6HgYoW7yNDR%R*lri9?kWKU0*o=z)dsah78Z>#2BM{8m&rfzb(rtY^baR|$xMGjyuinFmN5h60M8 -xF{(%*Ox0ddh4zdxO^bL-q;zGhTC4M-;B^YcY>{cc(J`N=4Hp%iZ(#pi21J@upI*l;&vS~LkEQe7&?J -QDQ)@?y|$admVP30j02z=d4N(9)+GQ+(2E0C|d82SJ*E^KLDdCLi3(kE|i#HhwD(_^wZU_cAtAezLcP -BubkMq~#B(%L9#3DGPNhW?Bl7~u!cKwcw43_L9KYO1$|p#glGth9I86h8Zj}$bB<$rrkz8fb4Zn$s377sX=-~YB2$X%u+PO?q!$nyGF^ZCal85f -yhVJ13-K551q&9omjWdYJt=$uGiULaO0%>8{WAV!CaSh#&-(Bi?sYTzky4l4cOeGuVpMGTa6vnT=Au| -g!=*{!DB`zm{p)8Ua6Vo!Y9<)uK5jIQo1jjvhd&qErKI&? -rTZh6MB{v=YFf0M74{5T50&H+R5Qw>xyEptpF?+R&H&?D(3tUnR>{x99!w!>bTvYfj=`M`?SOOsYdWT~iFyaHMx@H}+kSzh#7jz}o)! -@a4&SfVL0L;LbRMSdIX~k+mac@JXk?%&rHfd7qA>q8T-AR1wi~x`}PGbpO0HLrlgISHPQoh4Q8YA^_B -qJ3{qms%G3EYcMrG!JfxauklBmKi_@ss7W)5DDbQj=(v_`z8hw~3Ap9F_U;k^7>7OJHJS9zbPaCdix8 -l9MHMtCF>Jf4K92-cw>tCa>^Rmc?>h@;k7;;U_^r!tL$U17MOQ)nKu@Tvcnzg9d@8)2fKgOpUFg -L9P08Va71vI(U9*V&%x;DP8-X~Ff#!dFXkL?XK4tWQAHQxS?Ckn4O~*ei6b%griRWw_fQ2Jx#bd -Z`VZ#FHJ;_s<%r5~agTUn#VNa0^-Iomzo8ps5fWY&cvB%M?>6Yy=>4?VVNgb -CL#-El7rT;}zyXFER<;E>s6FKtaQBlxYmny*Uu9+hlJ_cmUjRw1*YZo_02<;}ln1f&#Xl3Pphkt5#6A -Vhsi-p`C-2tWcA|RANYT6YYl43#UeE8hD4)6i?rFCzSXyMegK7OPY?05E#h)#?e@(&9y6{ZU<#z& -~1g;jvk-FhhV^=eJ*xYGM$4^3373h-&rlI-I}TkwVZR3YBSHJ{eq -ON$uYI2M_tPx>`MtkQQPjpej7wfav!x3lwzazrj>Nx9Rgy6xQwm}x_Dr>)9fkz)1ib~t&E-1 -;+;)Cub<;D(eBFNP6k3QdWlnK_UpfEjh0hnw@v`JL2px>j(QSV5K^VjEWc5HB-6%kcOjYn}&B09X_%h -lTz|%rMej?FKICNI?2-X;WeV_qN#gQwzmAQ;)4hf}Ds1wcqr~K*<;gwHgw}yOV3PO4QMKgZo52V;GZR0|z$Jy!X5?#xtSpuRL=GayA-7 -0cKr#uGjKBy|gFA>#z+2$qNk*DQ;VWJ&>o^#e^FZ3s=~1WM4R2nc@LC41#VomkK88J8t@6N5;H+D{;_ -(8PP@5~&B<{2%g27nw$qo)ax%k1-p8kPE&|NMmc3>3NQfK3VC -$uyGXlmeTUS}sdog6O^fbeS5RyR6)x8|bs*uesN-a&iac=rohuU3(u*PA4ZTR0qewrXZu>S$ --n-+W{^?@KDh|bjyOQ_^3JrSSSiYoepl!YM1_l|CN!)3gR^?I=P_N{Gu-*m9K^yo;j$Q;5g_JTUg$aG -LRw@>L)!(^W|G$f`QpE{r#$6gu@CvS(h(2R)JuI+Q~}!A$<%!Bd|9-#lQeNH1pOFL8v;LtRUs@_>uiR -spB-YbTEbMw7<4%8?*~tf(EU0dSio^nGzd95J9;E1@OP)oqsq3msH|QYz|}82_NqhKzm(=mmmWS#Kan -f&jw#J*d~=aZ#74=mN2jQcEXpCXSH4Ak)pN2({V32FwLW@IF!MshPO*$0=m>};bR9&*7plPobV-t^QP -z2Qxw4}oGjGUfXyxz%T8D`ma$5&uIwNfVQJVv!*}z5z68^WJ!2xcSRNJ%OX_l#V4!AE>o79fxbpx5u* --E|`@`Ls_Isi)S@7HHf{I9=&TP3__LpFb$OP}Pksvh7+RZW_5x=SrWTn#u{d7 -7p`{Iy;jw+IEg6&qnAcdGS%a7ld@rG5+L97MW-^iG+ibhwOP-+5CL`M7t+m8{Hl4SH^F@kE#D?-vB}` -z+*c07QoD0Q^Z|Eb;%$F75AY?B9PuIWJxP?7WDx<-(Ww^HA($a}6=OeENJXhDm0JJ>0YFP09BE0kD;Z -+K#8#tr-@0YNT_q20ep$J5|0wJ9fjG{$CRruKhj$7loeduSpsNKhhNDaUCU -BT^vQb@V77yw*zvy0TNxbax;CIv?(5~=(PxFUAt0EfqMb}W=+qhC3c(bOGx;C_m=sgd*1>|BL4l+bEe -+6gRRW8%tCP59GQL|t?i3G~*>y|zP!L2k`T2K5>7vOCX`C8<~`S{FvF^x}ZG#}^F}_gYjS -99O&OUV$~+VrZ%Z*r^>gPOsM8T|tFNl&K}n9sMCZkbNPpAasbZcxJnSOWM-g?Cwt+$A=qQGQq?XQ|JO -Db=Ma@x9-AhszM;ZVQP#S*)IALl;`n&mW*0O6@HxZ@z!A2e(Xj#;Wo_YaS{NWVa}mEr)JpCbwl^ss}) -*>Y=8(@vr4CnJC&THpadJFV$pN01!4;+jtocB!6>@F>v+(IejUE1 -e%rU0df^rOA4B8MX{N`!l3|b45-ARQC;cXn2>gc?zL63VzPx<^;k>`Ns%=Tz|58sVOc1)pL8(st?#<+ -X@6BLTrO()0>09Li|NrdS$&j;YKZW_R)H49#e1|WQO1Jh- -<4v8X9Z`Twe;i82u?j-)I4y2E(-Aa6GdMw6*D;fIh<;MYTZfn>H?4iqb?0V>uy#`Wp0sN3nGCmw$&DY3LhDPjT7`RTDfO -Z(h^>*0atXI2%ONP$4QO(1Zb;=GKE~=vtDsHeY6M&mFkL_|Z#f*R&G4*R*S{ghP^_pwH -&r&QQXq;Aq)M^S5mYykoRq__?DTfgQZa7mXw;NwO93|pxRF~KpFFuFku7v^hJb2^Cc-7TSd)lTphAf) -XEF6n49wywaQfiIqUP!&4$G5SfmO3|g7jUMXMtazwXqW}$Fulr>W=w7#G?bRXzF$$4KW7!+tu{AB(MS -(HEg;Z#=TI7VQ+8_T4D7bJ*w~j*`f@JA@vgB!%)Cz%#iDgjmolgQ~2s~_^(;s3mU8JOA&M&&x+B|}SA -+p%9C{VO!a$ps9!)Pfx^rVS}WfF3unusIyTb!6m&jE&G=g1~vV}+0;uw3Ul3Y|7rP8FzHv-=;dqD -SENQf&Cu|f)MVq+NpfQXpFE~SAG$unC`<)@9ippb)ZwPo*e9?-oOb6muv6{$vBcHsckOTn;EyEd|vSM -k2)e7Pk+Z%Ve^SUvXzy4Py{CY8CToQ7p*zk#*}I^^z&kRjxRP%pLt8nW0c_eD3`9^mP27~(OKb2;GRq&cWa1*V){0 -k=OpA+09ddd1I2DbMA^ty`+ngEbg!L_P$f`7--T_`NHS2T@kydzw{fUvHLfTHR26O%B`A@#-LWQrM%_ -L3F#;}?u!w$|4tme0gR9BQl*D5N8|t1~RCrMO<6LNmW{$)IRw;SUs_qpwd84+nEhU^DrzA~WC@*EuPw -RTafv^YJFj!pkI1})odZcb%_KWVdG^>B72QfjB)$=Wbd1F?qBju@{$Lx0BA3h8E1e-qG{;Bt5p&|~+a -i;$RKnnM#3Q0e12Yrp$i#A%D+Vu}vWW@``#)Q8Bs2W8P=o$gSfdEv7|TmlA2c1uoasP-q=TPA@ -}qRLZc^hgkf!YM4S4t)TtrRaZu*I(*5xbFeSxLLn7f$tvB!xuEU#ff8_E+|>e5SQ?YEOxNwq1L=SCWxLVySD*MtE+*%-1A^_gu1j2ys=^ldH-BEZCmUcAxb<9ZdO3PT2`xxEpXl_lC9n7hLgty_ -r$}};cB$?bYM{+Z>%TtPexS2H`cIdd;MPK4sRF&ienIbh*XRFg+DcwoZ!30gGTMiw%hwhyF)!)-~0|^Q>mU=ZA8Q3CnR>}?#nM -m*y73dSy!s=EOau8%)f~B3oCx$;;Yy>D;E1mK|3RRM_$2i%NL;4tQagb0HP+2=X1eb5dD6(GI)V(Uf; -FxBED=P$GfheM)PAd<1b$3=ZfZ{E -=}Nn&aGT23F5a`Ldp(pMXp$znl)B{(JRV7+U#GjF$WurZ`97W{JZKgivG~+;HFU3)EU4ojHc2cg1rng -b@!q!d=2V4LCPIF0XeA5&cQaQ!ti~J$s3Y42m#{Aeh;8I44M3d?uSG*#L-jDAwakpA`uRW>iAvkvV53m5+RhHZ1^v7=Vc#)Rbd>v2$Z- -LS#rh>rQ!}LPPgD$d5PAaHyEg%mg6NrOxyj1V!cV&^T4+g9g9v9>-H7t{fxpt7zz68?&{ckg3HK{gBY -{L?D5r%IdSXPol8GB@Y>r7K}v4?DtbPCH(`s*D9Nvf!);NG(Ca-Tbe1$E~_u+LqjDMc}%j5BIiFm*I` -+Sb~z`k!q$_CFkZE&3_R?clT7we{SY~l$GyAyELQdKL>IygSb>)H*oN-4K(GxpVaoF);>@CJZfj=R7y -`zFx2iylt|if66iF-OmOY(w{2ac)KFt*PnsR3Y>+Rz8qZ4py+MAtt-)XkTio&UW)Wu@ZarxaODhI -ItfL71!t<5%UN?QX>;$-oHVjb%1nbBQ(hS!HggzA@h|%!h6+~?Q$N+8uauuyxJlUop!Tbu$2NxNq~%- -l-^8X33t1yGnJ84a7)HbfcH%;mbq9rcUkuW8Lt&E1ZAciyEo6(VICY~87TT2Wsg47GMV3Z?FKHXlKs6 -XEn!DfN9nrA;%_!dF!IRl^7$m>U8{AMQI7?}?a5a+b+4WP*z>|2J7Z0ktcip%Q6cuzmHK<`3r(X38YP -3&?h?a#t5>_6$2s%ZEM5c%bEG5X3Dw3ODIus>H&tE>cMEBfhf|?hx+b3I$>5>kqX5}_25N>1HJGsy3RtCD~qP$^Q#S5pG_2bI2+kD%`xT0IYP)gSuBmmxrZL%p_O5`%myaF$B)CkO9}-P?1MU42vv*09J-M?^EDDftyx -^HC_bOHFeUS)60_x1_~)8RoTaCu08KhDqjHM&9lRxq0!qlzt(ZcRur-+(bfN=IT=~xlugDpjg-eYtKv -Sbz-h4o!v4JG_h7$yQIgp-JjmjJJ}Q(#EG>0z;Mg_!gxRU#l^4v=*PdXlQs{6euXa -8Z+?plZ9#b+a?@+q(^!Q-KDE?p=`YMzG9BZaL)Ts#zH800{Qz=$ZG!G<2_>w7Ge_6`jHmKBJ=DocJ@z -5af37VMr0TQw*(tQrqNFG9uEt@H)EJ(Ji!lETdRV@kl1{*<$+XeF^|Q6V4ExEfET(LcOThiA*DsV}NzoRk*>2UF}ZqFX4RFu5}S)R#HJ&a`jo9+`~x>Bd! -~a+OGw9q^XVEx_dHmB!5Qiy(wzYhj$D}Z<<*b*Ufog=OMm=wCKryfJ-({3ZzQf5fWkYJbF|23Io}|56 -B2%U2!0)O1uy(l?AF&2{$@xNNANYWF(!0s+%Reix=u%rDsuaSYj#* -FJS^X@j_^@;9$aLYH=tEgoGf;h&2-Cy3Ux`@{|5Hc?@y8n8zwHH{z$CME5$B1rsBSC!a_On@R{(D9!aCyQ3 -J{EmL@Hmr|8!H$O?(RasEoy&x(oeAMDQL92L$f#OW3&k{cFt(1!=NZqUFfA -O5j_>&)#2h=6T1cdHhG-fi}tmCVNJ$MYu4e1D9-S^aGHgvC*KsCH;jBK#NofhxWN>&Gc1vJu8P#4{|+ -;lgC%CWi_qJ?W7(3dD1=$^9AIAniW_Cu#FI;P#?EV&2$C;LqdhN13R07-j5_qwU#I&6r{x|_^OfiL4X -$$(ehWS)gr5GNDMyll_8Df+$C8#Q&WdE0;|n!NeM-GduC6uW4@2nt!U(LJ1cXo~m@W22TyW;EN?-@qj -uR1z3`ZomX%=T-@iytxQWEMNVuU2q9hTcvE(bf0MBs)zAFG`*N8pk(25soSUEgw5}8m-T#fFvQW9>5d{AEE2O1e(*MjL%%pu1g#G&q8TvjboB -1YvK{m>xG?d9q(y4Nj(%#g1(atIb#EwVN=%!3rKqZR5+$@p+wUetsVx%!raAGt5kz1HRV7B75&oKxJ; -^XWd2L>myHNOh>~BqDKhMsG6Alrx5Mwd+nagro@13fj4;T7~n*%SAG!tIlT&`(k(C(%UG29tDqs -`57nr&i|!Q+U8^UO6NhMmTYmcaQQu_ejp4yrofSUO+Y`0(DY-m-i$lU86>DKD+eKf(gB!<}i#l&b5F>9%;|R9JCvSj(^Evcl@ -C4klH8>U9Jkx|+s$V+1uI_cWj%~Ip!+{&}Knp0N7n3covf=0(Ia1}^*(OZ~89s)<(JJMB%`bJF;fZg! -Qv`jLqCk=2sxD8}hjXpK?lrhPI%`aJ6^8I|=Ez%eUvzb^dRT95^2iFEsZv+oCwAsB01ud|_vCc4Mhxp -~80SDu6);^o5WppIQ_B-qGX;hl88V2=EWMEtfINz)h?)Jy)OM6}L+sH^_EHbi(Y?0i{4x(}BNQHcG)k -Gyq2HmbLsT1V=BA)!s&>uk^Ex(g$z{LlUdiZs^DLIJw|Y8_@;-6iOIMJn6;xTh3FfSZ)6lf!g~dZm_m -%uvNB3HhbC02e?%EXl(+TxPS9T4YWhwa>->hZod{|Yl?XixZ@uH)99bB$3yyt^z+E74cAsUp^W#$ztN -LmQwOa5@7z+mS1B%&+prrrBqhiB3dNRX8DJZ8LnR-iI3^4Tm1;ZLpq4KG>*Q4K)>jc3pp*Zit4@hFVh -0=vh1XK~+kss9B>A*|_CRBF$QngBvzQnj$EIS@+Lp7nWd#Ed-4O9HBW^9EWfuBY86UKGcN!-s99zB;^ -tLA -d~_J(~L(lP40kUDucW}aSk3DEP_yu5=zoq0FsC&RC2-{8-hR!-nyzfo=4)S{k7X0%1hO<<4)G)ISx8> -F8@$rnWd+eKfZc1wAanigdl%i>7!pH3rQv`g!e0+36nt~v7(Vggyws?yHTz1E)ZA{%svX#Ptd&I~#a6 -rEuMYd1!EDHKU0i!nHjIyi{h*X4KAz2aFmqSfG(XP2#kvF#+2<#cNGKro6Rs^LlK;eA0~Y3vOQ#IQ5Z -Swg#$XUSY>Se!09C3M>(^96e1EfQI8fxO{fVZAQFRyl4}@J0QN#4ero;bnJ -t56}HdE|c_nN^hjG1LVs!57coyci^qyMbqIxs%KLa#6ozq3>rF1OG9?m)8gKI*wkWrG+~MLGj5`L9kM{kg(?YVttLBoEbnx(gxv+9c!0b;MqZE(;x -0H9%%h21smtu@UZFJlQgmsmX=QYI^R^d5`d1#qu?270qEFB_c=O`_<9C)?_8sdBYBL9hkNXh#-)33!2OH9ui_&`O$^w74k9E -c2c*}(Y;n=NZZU?S$qR=NcIK(ReF`9Ko$OO!Ea -fc`qO_hqB;M-whx`Gs|n7h9cGgrs^u!x^Ygp2<2oCyV@RgL|(tmw$4k%>E??{zhDxLKm)ER&?Zmp;c_tqqfrFC#yGP6RQVhhqk$M*gT^hy9=3PoM^N`_HQA)MSZD{k(7%;A!yEkWEI)2U&-wbl -9A%zHWIAHm@MFDJeC@gpMo)RHYR*%EM=l11cE(;rUr)0g5b9*=4?;BWI0>pm2$nx-lK?no_ga~&kOLp -t%EOGPRnM=}Hy}fp1y_>TR(O>c*vqz1{d#OhL=3+BG(>Wq2HeGkO3D8ig#Pc=eGU%Y?@-GvLq_Ghs>1PzauKy03h%r^3xt&4wShCa@T9ue(~a)2IG7WO -QQYhh;CP)L-lKcr&*^2Lc@Wj~%(#MVm$dJ?{|xH&rg<@=Cz?o4`tmM0a7nku?>(G=6PE|cc=J0v4o?c -4_KCWPx5Op?K$^t5*%|ML0gHJoaQf8@b`VG+k>ZUX=B(_ClH+n2)q(+ME`NsZwQy2tc;fO)9{7q#z+QTVl>DL5Q|3E`|WRmEvr~&4Y^4dVs);u%YFT -8{9wZdezrpAd=rc+1b=Ex`D^@kzb75C;Kt49tKq-NuIJ8z|--Glq^bVnpbd1XE197Uqu&28EIkSrTIs -&;z8H*x4xd<*%?1|NP40h|c!qI+!~h4h9qws-e_@NT&An;ME2$3kIxzMNxCQq|t|p7G5EqM&f;Y&yEv -0o-mquZHEpOS~LUS3q!@UQ1yvw+TM6dEYDbRdQQFm-2$^{+*8QwPc(Hu6TW=m+&@k=%Wq^)M?0Adhtd -@YQLWqoDR=Rn)4Y5UG$H|Ih1_y|_sdIQrt<4JGQO#MojH2;fCV -9EYpEpnN+=J;xn#`w@~`YmQlTk9PDX+yxB70~xK|A&!kD6rt_lz$!|?0_55JNCXy*5NP_y^jbEXbS?5 -YE2;@7(7f$zsb!!TyPAJtc09*V23x)L^c^OOpv=(%yfuVZ;oQu`%Gb2nb`0^Ms(!K7G|OpYRbJB^3b1 -C~VrZ#nczl;#8RKoqAfgnK^0D^_RThu6`)HfcQ%3wQa{Q~2zd{X!FM1c2jwLDZ{V@Jhmw+@|-vcn|&6 -E_}%@dhwE4kNdei764>yt>_p*QpXvfbox?N@)fS52f(qA< -Lfu6o}}erFDDe!7p?>XOUYs>CI(Y2-y%hoAL_{$Z$lU1{4VFJk~yNH`$ZygrIf -NDUTKb}bjG#+}5r^7`anCOi--hDt(y#Mv7B^MvlJNn^4!@?qg4OO?%|C*df&0Wh|D=u7^&UjFNHl ->MwO1Tv>q}>z+CB{I3!s#Te)p%k>pO`hTLT1*~ps1ZqiW9ap>h8@OahK8~C>h*1wHPiGLzcd^w_cmSedhZi-;RN@ApKE`tPH6uft2Q3muab;cfSK~zGv -%F^?@4sAZ&mG`iOp?8^+*{v0o_IPqPYrpV*wRVzsoX>sxQ}1}Yt^yKYWgR+Yhva7h|0`l?XaoF`OyJl -?%tjyvztof(;t|V36Qk3%c%UQ`&2w2A^S{!#+^8kZw{Z%ly$#wjU^` -AT`snJn{(JDpAels6o;yb&?{K$^K^aI1GorCG -hTng`C~IKM}JbrvL#4qE)X`%NGAoPm>PkywqT>s1FrOvuC7uxl&tUF2qgh?`8_q3lT*+W_9Ow1*d!qVBo$4;)y(>4&d?24Hvl{_tnh -GQQ)jhd;!XgPU)DV0{>g(<9ReqSDeWN2}`87fMSL4E2<9)?4VH{0oVV0i#WUcq}Jb?#mY?dd}Y?l;7A -zgWv@ffQ>_hBw#ygzRtgoG=wc&aZbbN -d=}Jhfyha0nHN-$bzUtu}zie=7*m^*>q$Fvc-{cU@O|4#msdA09QW=)YBN&<9bCz6j%Yue%=hyz)Rfe -QbzTz9Y*WcgV{BKZ80|XQR000O8`*~JV%jaiiJOcm#-39;vApigXaA|NaUv_0~WN&gWV`yP=WMyAWpXZXd6iUMZ`(K!eD|+d#4irux^asD*9!_%R_xTMzhK!Y@?_A`$|g#Y21zAQfBg;RuUu*ac -Y2bRJHy%8)i#@#AL{j=h7%eMjnqO>Y%(V4Xl#BX~;=T*W6u3d$ -zww_w?Ep@+q`3n>m(>oL?Me~sBXwHu93upUEVxzs4>k?(Q-0k0p5RHZXMKh^3Ru=SupwN>yG^_m9=tK -RmO3AeqwhFH0mYJN%{VRk$P-RL=g(l0HbbERj;YsN1qp`pjCX;y;LZ!}7PEhUH7VhZb(_~2_c2G)Btl -6TPS-Dm+1$ZP=){aRy+J%_go}C&5A<01q4GidOcOQr)&cod=Y#k!>snb2)c3^B1dfgH}=tnnq0eB116 -)AMX9+91k7Mv^1Na~t)3-9p)LKOOnv7$9o={PS{8w|*$pTouX>2g8QqnV_vi6)v)b#>#HuQmHPTKf3y;_Oc!SwynU9g<{+s4 -qRoi^QfktFmlg%%`$4`dGNfilLnsb`!IspAOPyHDNj-G}byyDrf(LFC#){mJ8hTq?~*Be$lYPO(n6!a -DXlYu2bA{R=6#b;h~!=Eed{LrDb1QO*i4 -Hn`;D0=uIg$SHW?p(D6;4Stva{2><}YBme*>0001RX>c!Jc4cm4Z*nhVXkl -_>WppoNXkl_>X>)XPX<~JBX>V>WaCzlfZFAa468`RA(HGa%!7aw%IJa?LwhG(ekl5hkrBc4FluAegOd -=#!Gla0eexDu*kdVNJTx$2~s8nobdY+!?e)?s`$H(~}x~$(cTXfsJs<*mzy1Z>)eV{F}$4AH18w+ZOa -wL7*qpQFbBo*BSze~@v@qIFx`O>j<5R&6b;cIdrQ$AWQZTeD6th^Rqg%?akNWqYF4kqMVLMz9fiUh0- -e1)&!GziTX0MmUCM&nK>Y%N?GEDT~+l^rtHbOBXkO@*r>RWB}H0wPzuOf}D=4$CU)2qnU=!i`RH75F- -ogBgxlP{mgmA-c1}FLW=xQ79*LLfD}u9nk$kj{`qGKTPGxL1>2yw%RZhf>bcnb8PH2ErJ<2wojpOrHb -HT2u-%{o(3V-PXbpC7d$lcT^xsZtwlIdOB>#_`gAK4c1kzG7a>k_KO1<`!Qxx#2ww@Z6-{|ejn|fH@J -71vMyL@0-dOiF35j{u{Z)htBXm}-F15voC4#RDw&wlAn^Rs`#HJVI!5iUnWjWUb>yx@9eFHm&?ePl$a -nLzvMFT-IC5AJv1O&*^$7b(cyWgW)>w!MjI-Odp_wX9Bury}jzX(ZArl6opI8|u-dV!4t;I7`edWRLY -%jRvf_rSYvG<&Ujw@Zz7hiY_R>-3se{o7iH?)#nlcDG(8>I$(mF&i__$SV^M0XYd^NmpoFdMY~l1Kg{ -yLC>YJ{Z+utj*ws!*$9#8VlN+hfj=_m#ger_Uy~e?ALtUzLkaGHeHrfpW$oV*Nbi(O^r8wO+yCOh1zj -v}$RVH`UJ!&Ox(t+N(YqSfR?e!`%1QNkrApNAVr1Kg>aFW1CY9r(qhT0Ks4QlVo+BuJW6yE;6zct}-b -V}W>R7$D)#%j!ZM8es^-hVR894E_Zgrdgsh5su%O{q6xNlz7ZmsKPx7TT2_Iu5C%j(qdqqx@oCEV2hp -|edpW}c{>B_Gv38k{|^o~6{^T=Gz^{&?ys8(7Icx`lxpy-01~vU&3&SG}2dg7Qf2QN^F7O6h&y?B=TF -@yE3qsU13xjvUWcs3K!Kyk4ds1VatQXQ8Aj1cT4Q(a7q?f30@^=ACt2>(#8@yH36Pu6=t=Z>d^2J2wK -|ey46d)Vmm~lrJvUW39Fss#ML;U9;7^>)&O8o*$WcR}9IrBx;al6_oIXv3~v_XW7%KM96D<;F-7{3C- -o>x0&!Aq>hlwfib3oQ^ns4kx(#60Nn`P=E*{_xEf_`Ws4VDIJu+up{$5Hzp$?cr;nQwsLJMH_DYn1IVBb3nBLSvFk)>u!x1RCe{;Vk5e7^n7f -+BkYKMU&!LZh}^pX;kuU>9YCqtsH3JuuoLJfN|6I`4H`jE)>_?iUu}__3YU*GmdJ_+!x69jwe7MuHJB -z7LZ67pgD}{&Cs()>`-0`NA39NxYm7dt{7MnC|Pwh5J#ZZff4C! -qL&4wb-0Obw83OB_-LH=wqh}zAB_CDD1>WFTrw)6|1D4WX_K=U$H -Hyd3cm?gCB$wuoyKmL4*RkIA9OYxgsKu;+|8jRQ^tC7K*j%j47cu!N5RU!V -hMnvBf;fwhm3rL4;cYSl}S3@x=?eR;7gl*sd|sN)M!uzYIZV&QTKvgiF#GIIEu2~(gk -&{56XkY#(>aZM=^Is(BxiN{KKpGn)9_6KvpT+w9aX*tE-mT>0oZm0SlHRe>#WpGuO5@;sZl{XEG#JlW -k&3z}gvEA13##Y?bf~wgR#@`BEGF~i}SIeK&U2ePG?dmu;?|pkTV2@n{_oN$lgSa!E^ggbfS>m+K&Xc -ACQ99Lx5DjIhG#R)kSHGglH|LyAPVjRmQnE^be(r{>UCoj8gQ!MEHNZ!A0TwbN%ec&=heMa;j3wxV1} -tjK_;qCkU^ZnO1RE_+`fKG=GF>NRxSW5xI?qk_7w1T-g@o{lkuB0sYvAf9Gg@2M`)oUrOX?4uA4ckY= -z~d}`)Ke8sZ-Ah`;mI*`@(^w{?Pe8QV%Nsd!)XsJe|~+mHz^%qnDW7D+Br%hWFJviSG`I9Z63figs>OYyA&GH7G+fHjiyspU+mH{7ge|Y4yW(K -uy>yQ#c;p53fEeIc-tWup_8tw(c^oc33o893Lt--`Y8#!6xvOSkW9zlA`r_xD_^6%d#|H-g^*jZY4U_ -}H&ntTcU-x~p`(uJ{8}ebnm;a}MU9p$`qro1bxBKr12SlFwoVZ3F6nQV>@VOY8XcBugS?fS7(F!T}3k -71PHodIIwJ9;dZ&Tkc8XKi)Nsj8vmpSnPoJ1((X{uMYXsYk0sa`=KKCkY2C|yV1CS>$;XCQCpB6erv( -TCVrrI|_Ir{3THu=`X#^#5PP^G)jg{dZQ|KlvuL`wo>Tio0%4iJF&pZ?d1(DgFUaO9KQH000080Q-4X -Q^o1<1$P7h0RIjE04V?f0B~t=FJE?LZe(wAFJow7a%5$6FJow7a&u*LXL4_KaBy;OVr6nJaCy~LZENF -35dQ98F%XV(sH56<6q@D&bsRfq>e$9F9HA7kmPYn+>s_G1&XC+zki`2&bQYr$GXJ?*$=9w2=T~$ -Bu(0>|^VKnUZ$MZfsj3&L`pvL#AaRrMF!bI~mmrOuUg$Ufhv-*<@!RYsP8%rKAt26|HqqGo^kJs3T4k -fpoA|LS;h1#J86@jh5B>yp56R;f~z;Y!G{nR_9(;oy3Q>8O|ppjV&VU?ta=T|Z9uyzSuwXx9bT|?*g_ -Gf~qboZbu1k9YTff*XquNY>T^pv!kuVPBGX|8`E3&1ne-(kwdTJtU305P;+6-*PmQ8Le;q!N6knNFN) -kwyfgBKtqJ-rYhCHh8f1uKNuU=)iMA&@V88!o<1mJ8^PU*KDJHtUWTFL&fiO4BLTFGJ$n9kpNxl$ -B=ECPbV7p8K=jSBmI93`fJoi(@|+F2?=&90rpabYa?^Erz}2s5^t{ayFgJ`yRj?aWAnGvr6Pb;)o!(i -j)k|GA>M(`U{oU5J3^bhP@({IY3Sw1&l^ymt0gsq2xNvmqJQG?|?@jm2JVB=F~gXeP`7rl(N1pYWl!y -!A~7>V)DD763i)lz$1bl2&Hrj9+a`p^|?#2?N6Of`=;}`-2qt6$x`<~fBg8)rE=A1L|SdZLm5*qQ`Ow -)Oto5Zxt?2UqWnDAFm2H8=!DeC{d#1aqn)P4Mxzl3X3XCDq*c7jj+#e~G_y3aRG~e$$|cTGX -_1?MGZt>)wx*#N5+8Cf=1e2hc5Wq1Rr4bck{AeqADwzvr{SQUmyuHQYn{_%KV-(82G2oP@2=SbrT;kN -CM1_u!Z|ij*`hE1TwX0sAcIog7_@@9Q9s7?&5ihZ-`oADi8i+0U*$InM53trcP@D3Y3g^h%&&FG%rDOk4777nUO!S)nIss$ZW(okuZw7r!&qxRpmun#ZO&6znTCbph|^mE+$Ztypg`S&kq5;o=; -pevGo0nt**8mABNDI}J@Ek33M+@*YnX{&T-}y%hUnz=V0n0=YR{AcvxzI)0&bcz)|Yq@R(kjH(yl{G{ -mblZiXs_&J3N%nREfr_lCpZwvc{H7Ow4rM_uTIsWe3;+=c}>lgRl?Z2Pj^iHAHYw`(k6|b8gKbSNyi} -=`ribZ_>v8$wz3zGx5_dr3izuE$|P-M@doOD_3bl&y6-GX4mtk7J2LJ%}6951t0001RX>c!Jc4cm4Z*nhVXkl_>WppoNXkl`5Wpr?IZ(?O~ -E^v93S8H?HN)r9~8G!_bR6i^l|ukJf+EaFr3bZ^l@@M_=8T!o}Zo5VlC)e%av4KM1wdGBvZPI|CCYW(XEn?LZ`U#*Pc -#AMuEtLpT(Mdh&pFm4PzxU39(P&QABYfG{qrhM^r<8Dzen~5`m-5CRft;G8wBBUv&~|^973OCf4@c*T -Tz0K+D2td}+$ltSc#CYMoJI(4;q=P8TYUiaXSgBAT&mN;8oP{U`z($skslmdhORHPU7eCq!4CexMr}t -+d=wo8VzaSxh%YwvLsNn6C@`^P-sV<6XQ4p%NfK8p);hbiwF`S_n$xFnfaMZ>flL@;yab1TwYufmBAG -3jP{vyx+uu3=2NWRe*RotW4-lx&`_3^p++fMJV)HD4}8gCSOL<$K#3gg-Qi^DJ6WCt}7@hRqR%&b^R%&<|`;T-d{ICh93yP2~y?yB%g`kQwAgPSnHIjorpb&vRq5NnmC{I -P0M=fZ8EPWX-F0XGF)SrH(Fa6o9KV5u%3t6NBw@9$nBsWPUp%_tVPh|xhhk~o&Y;cJe{g30xc=46JAB -`8&;e1Y`>lyXwraxPZ2e*^{jBajcH`nu_gX9Xag_yxS2J#N#5Gpq@kVKPsGD>aeUyvV~3=Qug_q2ct?Pm<61*%>m${S?J3G|g)J72ckO8tvo=m7~hb;~O0>AvMo -XUT0@#e&nSf4e-)jWPB%dBVPU&?1aueBW}#g>)B#(yKonmvmw4rKGA#XIRE(FUI!v1*OlN0*KMRC3R| -dLwQ@rlp?=>_VddLVXl1ko?kqqtgdV_4->*_F+*c#B-`nVgqLQ7az -2HET?P21T;*||!oUCJT!F^L(vK<)n#ntfJl{@Ggad>kjPe9W7_h#SHQjZHIF8cmglCh9g9Au?Efb{iX -)XOV1u@e&4?P-|nXWSC9IYX%77o2PRl^gnXf!L^<>=L7C7wwi}$+@XC*J?0by8g|QN?XUMK-LIqWD)(_L9XpI0Mp;5P0{+C+SzOD{ml;=M$T04bDKBmDd2r;& -e=Tcegy}h?vgdOpm!P5qdeCxHlcQq9@ma(1W5u;XxTdp_OLW-)ffV8%?WY-y}O;^QHedn;5UR!x()Y>g%`=`SA~sKMrH8Uz*Gbo -TBzXqSUV>I_2dZtreS!tN-Qpd(X5V~wUUUnzJ88s=19^j_q!ZiTt}hl$z=*Ai8-`BwBND4kQ|mb~jmB -IB<40jJN5fwZ>V-QPp{{@%8h=FJe-rscRS_86yZ`WM7+0OG-Bdc4l!Mk(iz(@_PH!FXJ|ZJA# -QFl^n*fUyCZ8v=3d^xfZ2kZ{$tTjTxxFH!5Ro|T(|^xK{mDBiV59)4rXJ!Aegb?*-B4c5Yc;Ldy!7qIEa8= -m=QCWQP0Nif^<5v*P=t|o+t51YEO$36MsNY|b~ia(hDhZ^4fNW((s;t1`Q`L45WYFc7jDs5XTTs?fet -$OJ@j&m}g9TDx`ZLccsdg<#qK_@@3nDR5T`y4`7S~IEnV9RD}#)kV+wN?Lz5J`PS^Fj{TOdS=g_I}96 -=is@KtWcCdjqy+!~1`YrKDF6TfaA|NaUv_ -0~WN&gWV`yP=WMyZfA3JVRU6}VPj}%Ze=cTd6if1Z`(Ey{qDcw(0quqx?0>~z?vYyoYZNJB~ -D;FD1ss|Xz65gr9_XUoO*x#9x2JclXXUc*2lZYyLWew?$OcV4_$UY_xg0xyXy9bUAnxvyZVz}@I7iA( -P+kWXXI4oLeZ5@n53ml|09(Wkv3C`VT&5IYcH2h!t)a^Sm`+%(kZzE81V12v$2>nOj%asG8Rti+~TX5 -YZJZznC`VH?Xh9uIHAchAnso!jK`WUan;OG^Xi1!A3y#Mh=cACb(Erk_q8-&%VxnzS;>{oospBmY16b -PXRr|63iF-rrJ5R<(K|whj-00ZrJL!zsvp!SIWfX4Jxi%!CaY8TjKt1qsSAolPKFa{OL!3BNaV?{+8{ -pDiAm&elv_`$UFSB6O_*AJ{!7mN -@r!FW6XE=zY|#Z>tbnqzyHN^ZBEjb02tuq?e@74O2%(2Ps%rnvl!`>j(x$NEaMvp%G>)xo}9S-UG -Za^Kn?+ix0tB0G;fbJg#_jki?L^NcsDfVO@B6&q|3dl@@$%LJd%9f=+qP-Rgs)S}@!Dw^L5)(xND>7g -HuO2F$PeZ*3Srd9rGNoL*7Cjq@*}?Sfbr&e>{RGKwm4ZO^YtIb>8*1gV@ -ve?e^)r_J`9p(Zdjymp;_(fBH!gIchY}DZegCPoV+RMk6&kqlohYvt6Ctig+e-9y$zFqtS?!uwDrg_y -=2c-qe%7ICj`ctR%g8^CtY=A?tl8E1f(u7)!rQ0y}^M%YPS~$7SyX%>^VSE|f3?Yq%r=Zzg~-K*}i3E -_#gn5LNZlwpMeSGP)Zkx%Ae4G%@!gc5n<>+i8gf#zjn&-zj_zQHU1gWX4g@&+UMCCh{qRBWE|@Cc@%k -EGx`3H@uL#u+0r;@SvVJSYE_=Qt?}(9=Id+_FkUD6WfF!=FDLHEdpy3D$RYIJ -V0)RhmRSN>~597i-2Qd+(c#65->P&#z`ZQ$(QUoo8l1X@GAiBusBs3A!7!Zv$nnCg=3{8*;=T4^5t9X -FB?bc~kdF6?JWZ0G^x;?(v2)l!`n_ekkV13Rp0^)iZeOi`-wt4wuE2lZo!r0~IwCqFq#=tw*r2Q9^X| -2SVtcH`GXA(o{Uym41lL%6Z2%q9ET%B=L)*L1k$te~|)kK+Pc!GT0xJde0h1EXe;|^D)XQ#H3v^C_Xdba~C|A=AstnXv*#;*qY;UE;1gEPqrf<1?-w9ja6}_!Jq9_wRQQXyhlMX))+beaHV5KQM`e56dJ -eID6U^tt0ssK81ONaX0001RX>c!Jc4cm -4Z*nhVXkl_>WppoNZ*6d4bS`jtl~YY`8Zi*P^DCajg-BZpiFzsRVIh#LN&vBoB2JOnoy}S`HnI)r{`- -y>2vw6vtxuje^WJ>f?e_Zz!|`1*!!#L3sA9AH=p>ZH$ceR&Ms**p9pU$_Q{PG=@s(yb`u(kc -5$uv4wECX$4wVNe3l2R@fSREiDn8DgBGEG(c_k$eClQknX5YkRC!8pN(by))ca=1GLu#S@??J$!;A?* -%)`T6{h4I`|e1S6$*>}M#-GXQ<;-?1mUm?n5(G3rfztXP)K?z1QWyZ!b_tEAj$ra`#{z)g6nSoob99- -!B}*J5A|D^T_9d@(KVFd>dwsWyCb-CT0rVXwlX_zt71WJ^hGL#4Po!7+^dKcI2Mmdc0XTbh-Yg&#>-_ -q%hm&q^_5{S9q6bew>u?Sn7gt<`KuUBTaTw-%Lw+4`$&y>clPt&!sob&kHLurkfvP|y4#aEDNglX7f!yrs|JtE4`nlwliLPbA#>k-1zN=Fe`dd=JEniF=I`vA+LMa{{5Cg(ALiwpzX4E70|XQR000O -8`*~JVy+WEXXafKMKL-E+A^-pYaA|NaUv_0~WN&gWV`yP=WMyf&c5WV -|X4C0FeING?o1vU)|tfJVd5r09lQRHOM5@i#mNQ0yjsK0(6CD~3|8`$MZnwj@F^JX~R?)gIx!>93#Cg -W&0ONR6?nMcobA-3D;(sIXXZp2n7CMnVxCt<13KZTSm&}K_1Y(eJ`I#$97L_YFYF7=)p(mA;^9EhKBX -H&N4Fcn3qM9Q4d%Hr4TwW)tVObcxqduljyJflrjGyZ-RGoEW-;i{dZUUW@^ySu+ZaW*`lP)dq@tfiP+ -ZeDrTR1#_BjM$V;o1VS?0t?ZUnIE(ea%)6EH-rowIZcbo?X+s^hcr@b3^SEiDL0&x)wz2^V)s<(l2WF -~@J!f-9zr-`D*Hnl;0v9Jyz-_}WhlpI?YJrILBprniYDA5Q+ncx8&tC>H&UbkPejU<<-{!Qz0K}UN{x -IXNt+0bH0Wz}?}-ce{oSwJSXk&&FgV=SGWJOu>M`f@M>qE#c#WhhlIVsxAcM0KoGQ&osG4>M?ePIZ6# -9lSq7A3g^1PINFd&w`kC)%(t0jfA7y2H?aX4GP+#?oB!;AhkOrqFJ0b*2Tm~s^o2f=h0N8f-5AI6jM^ -2KLA8ZT$VB%#qfrjQn4yc|cXNf^^&6))z=Fd#}eKS=DwJSXx|;vAHPhLRa~Oi0_5{(|;eBq%f5u~&{Z -r(9s7h~bIstN+zQsnxcw7m$;n_at&s+V$zp2+R(q@3-!P<<#FkhCU7Y^m%|FrhZ7YAOU2^A~(FFzRj+x^?_+di>{VN)-qUZSWog>7as4R9wi^I -E6Tk9}x1xt5A@dGldJ&E^B8&3-9BlY6$ue)_JT~Fta!% -3&pNhPzkF*mT@c{Z#IYzxdrPV2S6(pj&2mdm`g`(sP2pWZvFKGhMy5paEqg`-tqjqJXjsk!$t*Ao^ic -D@{bX_EHOku_IC7e9{B{pLBw-OU_*XiYoG8~%>0IT%2M<~cW9x!_}-?Kx -^&d13*}Cc|Xxr4aVg>(IRKs0q6kXNDUR`34Ol?cHy@-CsyJqUD^Dj_K0|XQR000O8`*~JVLXo%E+I -iAtkhF5$pG0Jn~j}zk^19z;y@^?_OPqw0)FioKl9#ru7PJIiS!>`>%KQZj%>@u3xl#qBgtSD+W$VoAbUyPEH|jG6 --{IOyUes@twpAY3=ppGn#U3f02H59vT~gq~To^3=8(JLPz{>j3@QN0e?{;DjMgg~;7K`~YsKFto#o|1 -c{sRlV$V(zNnfh$Vyg?Py@9Xy2Zai}bEM{Tg`t^3lZa=zHx7x3@TdR%3ndb(z^35ti7$p72v6b3&x?I -2Z(;mLRhNKU0-aewJ&8*X-@!JXK?Lg6_eOjnxF4tewDLe9ppLWNF~P!*Cu95*9bT5rlCvId -6;!PdB;5FCw&mU|akA3^a<}S$yi?*({tjge;E!CXDb`i84h$e@2mSl*;~*Q#6Lvz~FWk@RZsYR!RuSaN$A(c`~NQfyW5-buvHlfP5CV -j^B|%m3+(L;Ru^kDQ`^gn1<)WpRip~Qs8*daSMJTGDvm&H$Ge1n7{s&C2ZBnK)Okh-8I@F^A?x#^QSW -<0dZN`}>D=8N$`p7>kDln*iuL4(o+iD}x57>RlGw;Tn$BHa&Eu_=(sup;P)h>@6aWAK2mt$eR#OCs1w -Azd003?e001BW003}la4%nWWo~3|axY_OVRB?;bT4IdV{meBVr6nJaCxm(ZExa65dO}u7$s5#66ZpyO -3kH_N{G4h9dELUPH9zHi#>)_%`V+tl5)Skvwp!in7iDy{sPR-JUjEuGlMTLn;*Dy-+5CQdqZ~`xNtX~ -4L`ye-^oWm+QSa?udjb0h(>n@25Abu_0`~`M)iboUdd -1jqxf=Xq-yKVTe1L5nEDImJY6Zifj2-I8ZZ*(d$X3JsGq1w`BE{pw=5+J5wuLqGbMHdd%1&%2~Zewox ->9m2AGI{l#1dvGUmzNnsy?la%|QH)kc>bzMDS47&T=I?@*wkYUaCT$|DLM^9Y4TU>dg8rq>lyBb!K2B -myHz@EN-|fSk_l-A|}AS>Vd)m$wy&U62ae%-H;?n_C;$p``baQa4l?=`>PMRT={q8SI+4r!NFZg+7yv -V2QBPnC#LnA&@V8@)qxB4+|JfAK}x$_XpF(;|=)|({xJx%aQnIn{ooCsuUAuPl*%2#Px^oDPnNvjlIR -A&3@!9rfv|xXy(HJ<^zA>4Ijn>ALbAKd=|J4K!8|MY{_gQ`9N_(kQ+%%#%LH%bzb@-wO3PuX$))jgw* -B;IVlPljmUQYO+8GdSY`E6NJZ!k@C1Z3U3gZUv_tAU=`NvE{q2(IBc}^L_sNNgzfx*JK;a{v5cDXOQg -z`@YGYj|1B&fk_4~ay{hxY$fQNuA=N@pU_pe+cm#tQ!|58l~HU{5CbNfCl#M{`nmjhR^O*a}BHC53juf|p@bXdamTDx-CzH)VAD{qJEg2`96AcT5Zh -v#Z|K)o^-h`!KI;EogWOzmNXGuhVL^qLgV3u8wM@w`;#>=U9M?i;L_t8>^awofu8hy0GgPbhO3|UI5z -?xl~>zQBo&HhUx|s1*3VI&ouH#s{xtj>D~%et_1(gmMH?}8=1oOnp6TB|K3r!y-Qh5X9Guj!pIDY$0& -?lS|mgg?%^Wmkbd?h#stEU(*_R1w#GE+(((|iFv%KpJCy>9itVF!w4K{83F?9k>^KLUV}zscoeGEi3o -V7z-dp(N&%+r_Qg(b42tT|Kjx&7k(cW;wz?;m+-pF(B-`4tEzrZ2(QN-<2jUFqi)A>G#Qym^5y{eiIg -ZaRg^PM%EivC*@_&D?3!Sugfe>_VsI?YJzPe>?XL>tghzpbw7K4Cr-7>C6&`ifUtRM6~}ZK%d0Mblu~ -ol5<=Aye>cg$!IR->zh -SztQR=I?evTc>xv*w!xRwD>OSL&Rl;Eu)OzmV5N5@f)yo0v?q3JMkgrc44tG??_F_O&$+%n*fG_3#;e -z>?oSZoV*M`nhI(Yw(fygetggzlI^s9Uuc)UcLHnmtGZa!ejm%ZnAkX|D4=-k~LKFCp$<6Jk)09m;^0 -xLO;JCg&^UtMnG87|5j5j(r&cK^n=r?+oJ%1VS>n-$}lR4aRVBq-e#+l9*;lw=~d92hd{=}31;lIL^w -+QHp=Ip+Z*=Y7Ky$`*`7vdV$K*fDNJXKy{@oLyU?R5j<(aNYG4-@)2sv74#Kl&F?O9KQH000080Q-4X -Q^5*gq7()I01hbt02}}S0B~t=FJE?LZe(wAFJow7a%5$6FJ*IMb8RkgdF`2PQ{qSv$KUfQy4DwJ6>t! -q*SZ(TqO$T-3D%bT#u~yzQX3L#65Qgh?z>+%Bny#_g2?J!30mlM|9fV79{N||=!k#8dGoT>g?6ja>>5 -otZ}%G4kl=fiI)cGiLQmQEwksTHcq0k64-@Y%+i^tJQ}clqE|^3BG3q -KXG7ZF!yF)3Kx_d+5R#-CL#dgj{fiz>L=dFw&v6{b4NHG8g(Gm#E)`#*}Z|b^l_wcDP5^>HvQSqu}u` -WZBx3w1mVM!+Whe-7}Cj+NtjcseEh!Et`*e3nQk%Q*a^z8b7Y)l!T`^=s7sJ8Y) -s|*%Yj1Is)5WgdJn<@ed4$6a9(X2!;}7zO-ge8Y7@Dd}|G^dN%{8cW>caUDxZ~!R;CPVy4lG>$i+#Po -|EIfpJz!xWOC;jsZGNy>W$#`gyC}8r;(8FIt0c(=gzor$Zh3b$!rk+_dXD^l$Wj&uC@=3@M`&Q!=?E| -4OliAtxSYawG#`JN|O&x467M4$v9FSUgBvW>ea@qd?#hna+1wL&tOP7xP8QDxqg!Ti|*@8h$Dp_YUs9 -jn%~{tMyIerZ=-`DCN;fxIY|R0t4k2wuPZD9M4l}IQRU3Tsw8t=Cpi1m&q4$d4L-OTasST?0(tu5;;n -xEagogE8Q9HWn48nIvrt-ZC4rrZlYA`>ib=zZNQ&!lFFxZu{Qt?D~Bg8S4rhKTa|1UYs-@^AL;UO8|m -`AP9vSh_otCg<8=kn6?k2NbOl~lBwdl$6-ig*btTf3cwLEfC04v;+NV*}fGf8LiI+JuJuN#qW#Op?+8}T|xI -?3xK=_IeSNN4dni*%NiI$fMbW3V`k>!&np4DjYm$C{eD-qTTzuR{7c=k1!7A(ooUjqw6Sxm(h!2Y&XipEG|}|$Y+i&^-5~tQA%9Fa-yXa`$+1en!)Qd*(0>XKa%~}KD1m$tFV1=34QS -%+@*;sLPm?u4WfsB*KT(2kU!c@IzxCn#(u){)w%2RzBJqD@%ORm2;aX8#L$t)pJ7=G)5}?|UZIyIOr% -nDD{p%EG_3VF#()Vk7V6HCZmhr9s5dpPn2_hEtM8f1^Nd!=Yo9iq9O2&2#63A4Hz-TKJpLz@Y8gE(r) -iDuuL`fds|sH@P=zo44=Vg4TUg<>bo{+u;aOW);W=AW;aOW);W=AW;aOW);W=AW;aOW);W=AW;aOW); -W=AW;aOW);W=AW;aOW);W=AW;aS_N!mkcxa-rv{`Y!coj~kUK^t+J4ft%PO|Jx8_dlXp~{a1^-ONyT7 -gu@}*-mAL?8Ko!4=-|6-$rbN7QH5Nla6le^`81+?q)^Qi9^H4(1KK*Nt=*H!z!oE%4K7N+w_2iOoZ{A -R>g3punJ#I*i4h{gLt%!LnMQXL@wOwqul=1#FRDx-fw*5^vaxf5Jr3g`$Et<0Bb%$cl-5Leo-cp-CmdV4zU|w7xoTAqYK!3ALz;6b;Vo;xg -d5Y??9t5Af@a4Ss-14*tS5L&+SH}i;A){k#5X+dm{bX-;PMXim7;nq}TeJKYT2#*;Wr9iyJXt=iy^1X -8U;f_<^MF4@vP*z8#W&e9L$Mv|>PX&&=)1hKGOJxc|3@3m%Ofi>c!Jc4cm4Z*nh -VXkl_>WppoPbz^F9aB^>AWpXZXd97A|Z`w!@{hv=UYE%_;gbS@sx+d!B2#|!9U&2OJR7KWe7TBxVMZ4 ->?^wZy4+Zb%1*Xn9T3f_J5X6DW8tS>H3KXlu@?+vNnYj=lUmu~x`_6ItHy{KG}zv48~VksnxsIBIlgr -T+iCZdQnlwMMZwMJzJ?MYz;kRATaBemh0)Pn2@4&aB}nviWcme%W7ijY~Mq`|A+D4o23n8r${>!Ie%@ -;S}63FEOVrXAO23s);k)pm`VZ{Pk2ij(jLwW5e74r^$_4cE_no@UZk(rPh_t$8}bxX0FBOMp -D<$7&)8r~15)aM3~(mj`4Bb#wNi$nq>Mc^P9bSLcxShQ|?{ht5mqQC3;!>;Gi-DpBB8n-5XuRZOzC -N!Q-#v`xWAo4ihN$kWtC2}tD0+ee7CFk5S5t%ggN0iqfz-Ue@_RLY{kPCDaF&vT2_NQtXDY;DYg~?vf -8xjkW%GT+N7U&&x-%)kJvafF)O&>Pu^hXmQCO=0gfd}(Du>kd5q?E4Htuod@2QAoMy>2$IE}L(znnY6 -%E9V~S4m&?ML@p|og;r~7a2cbQQ8jmT##+sUTzVEw1m`SV3CWYU485L(E2a&%8z7-pW;ciqjIex*_gBW6sxoTgGeogtmBg*vRAx5 -Z${I0#%E#mTV6-3z$p!8;bPqyl%(N6SaTG9*nzI;D^agHKlav8JbS@ZX7!z7}3u}^ft`8SR8cFMqE~N)B(&rxKbb -Ae7Me8+@NO38@Wlkg;pxx{3M=|eH;vl(Sh-kVGESIPIB#oLKeHLA)UC%UiE;sR$#zrE0Vk`424E_G8s -*ZdF1tr)N+Zc&G{@|R$sj|guiTRtZ>P2tsxFBoFv7U=2iWO=J-idI4E>I^OY5@PNH@?np}m$5!V=w{9 -w>MvY6qU=5t|{NB<>7&-XxeMvK$l??%&MC+PXV#J}g`20eD~#0t=N$Ms)A!Z6mFOZ*4QHRrK-a4Vdzt -7GbYc9@na$EW5K9OdnF^>p6o=y>NR>42}E$A27I&UA9<{WKkq4F67!r^DSJ9DIB>uLb=^&8Ht}BeA+|NED1XA&_cU+x_-Cq_lEb^fJ+rGR{=hqSk-;t&dYhFwn;N -9xe#+MLk{F$X(ZmmXyUi0e&9b$x_Ie#%5!(0;`h&J`7+oiy1xucQIu|xU49O+KaS;`ONr#xSpyWTPdC -L`iY)Rq`&vDWrC#8!RsID~O9KQH000080Q-4XQ}jUtd0`j;0O~XV03ZMW0B~t=FJE?LZe(wAFJow7a% -5$6FJ*OOYjS3CWpOTWd6k-Lj}=FfhQIf(NQo~-YM9EXtV^!7D**;iYcQ}4XypBZ8aSAvY5K6I8++Mbz -wtZ;_DW`?T_l(LRAoj+#^sGm=B-=b{#Wil{PFBbd3^TZ;gjbN%l*esAN;NSld-o>Zj~4Bcjf7E|I7aB -;<7wAyxo=kb@}P=-}}qUi~RlJ!>6nLn~R&nvAnt5@2+mjn;diZa9Jlvxj=PJS-P`i|vlRcl)Ws26A3x_(?{CU?4{n#*`*K}oGg5nZ-0jNs;oZ$|$=LVh^Wjr@b8%IUySMx6jeH8?rd(XT{mbE)^FJKk?%#dp) -u*esxgEH^*&RPzf0f~nPhOVCyX)k%{J6W?9dkF&KE1x&zsbpu_iuJr*Sm6YT|V;e`u!i0dITfSzc5lB -9g@7#{l4t>`JJ_Y-5syuO&po|e@0cNaJCpW6J$#M{aA=W?0aIDVaZ_nRX4W2bM+{z|g%Q}+2 -s63mVMmS$C6@5-m^-Mdehr%55lmcO69`0LY`FUq|qKbODXJ3qho0#HRPS{%c=@)xzxZ{RYJRiZr$v>Ebdisr|6e-1JY4-&>-lgc0ZJx!y3bN94NH3wZ>{o^uK@I0qK+P_QEk1h{~<7v5nxV~ZNPxo@} -5Va0>>NwO=UOvw^|Lk9R_~eKGBdFXuNy5)BP*(2h-SP0DT)ak+G5nPF{a-ib1BR5od7U=$=JHeG!)5L --5#Un_Io%@#_~GLxjO}Lk=Ka;-^6<-Nh}Kw3xpU{=%5Zn-`8%fh>+c@e>{J5R(m+@{u?mXGe4gPuVRStrmoEYOY+{wRhOD~rG#d -kh`ObERE#@v&Whc6%e?flh?pP!vQxcB(g4`w9}K)TP_9f`FqMSEl2kOz82 -rX!Va$X+CKj1kWJ>z;_Reuon4mDj^ -$gc@&RuidNf~8sX7ub|P=Ld-c0`rSwVFa(2Us5__6Wfl(kd$Tk(!v1&*YX_3I){4=cOWh7F`h4!b*!e -@?8>~)aU|zEo6c8;@r-bS4>wu=H}BxGzTo1N4W15Q(9Dil0p5svE*y*%vhkhV%T*fn9?1$mkmo`%W1? -(u(L=*^ErA_Xqd=6LKU-*0fD2ur4F#ZNH7`cnBZFFhIxHpGy{?5gc~eyv-IGDKaoXeMu&wC=?=lBhjq -tvTyJFG9lm5PN7vL|{)NzBZq1$4AthB;e0fAt>)<2pFrOoTHXg9KeVKVIJXW;T`Kv?P?WJhi;&KeBA_ -Rue@st&(Uu%*9Z3?U8yp?yD7JJl%6i6G;7W}W(X&_0!LX|Y&u(~{6zWN0m8n<_;t*L`RxX_xoQy`Q37 -K2gJPSwrF_>cmRT;N)_htwH5GLM-~|wGD07p5&INKW$rLLE|;5C%8G(J(w#N|u_zeRv;-ibRSZwz)~5pKFpzkO{3D}-G~cy_n3>6HUSPZD`9sNFbYhcBo7!tU@SPqif0y -VZUtq>R7z;cx@~(EyCb##-tqw+ytHbkIwI5rQS!~)!FX@K22$tIwpELQjc`X)^R2@fk1ZOQhw3S7k{#{Xo*9w*gzsBCIVGuxVjwl-Pj6Z*HD>kWkurp5LUDzzJdpg -U<}|>=H>?s56R6F?ueaZoB087CgVbu)`Va>)=`}lJSxJ*D5aib7c{=46M#j)uB7aLQawU#%0lwYHb1OjvxsTlWT77j#+N|PAykg?x? -ln0RwT_RD_UOiAq)7via(AWz?6IA^yF}TyD8nwA^u$2BrbMsWU<{#@<1V4z|W3vOpC#)}{&cDI~HXe% -94=0oOBc>Zra$s{(+-Y?vd)qt;(hr1sSL6V8o0+bRe^vA9ezN2Om42*e4AZRjVHI!{(-3WHNJb2BOD`I$>u3;f5J4NB9 -|av9n`(j~pczTfA~a_+cVx{@IA)4FOUK8=osFDR1t98(7Cq@2I$f119Vb -+X&4Z3?h6<}eT=D=1Z&68gJS$PK1h)L^;UvPgzy4&4$?`XZjgVI_$OE4vbyym7;wSnova|GdKsHqc=AduC&=E3lj$lU@(G=fu|HATmeFJ%V0kFNQ5ZMyHy -+ohO`b=3Nbz-M{Gbz2v|+NgKBgFHbycz3XD2nKy@|rsVKS8@JQ)Zb&?4^3jZzy&%jCYkx?(7jX#Z>Bu -ggtrsX+0Kaf&vf8o5ya1oF~GKhxEr(tbWpJ|66_MEcG&zPwLD?C(f4qAwz7)u-DGS1`!>?V>&z)=;F$ -!#i;aSI6s)KLo5@#xWJL`+s?Y^wTIsw^BVoaho{>OtC?TXum&$`}lN5)2>AlVMvNxT6`PUhk8yF1%FfvPlVqS|GO@zto#mFvbvC=3R7FK1=eQpGq7f@|%fG+{!RU9?C@9-XN2P2-1jym~ -b&~UCGh}qblsZ^{WWl>#NZQ2uJw?YLg8ZKJxnOIxcC%`P{g*}=2!(p+K8q8j>&}ync7_HrER}>d*oQJ -Fxn(A+&zkl%+Psu0ZoKO;QTv)!_ZK}Q_D;)g`MCDPpdO(3V*P@)VWyNfoK7?U3jcoEk3N|CY?P0-$#f -$8eG68&5SDJlLZ&bjcHOyxAVIxQ&V`pHK^=x1wB+LmSrVlKK4NR-ckpgRX`8Bt^DU($eK5vf1=SAHju -LV6~f`u(lcBwY08!xmc32b51DYk!zKvp%2pAq(g1k|J45Io79f~qX2bb>-8C>hGSDP@J=%48i{?ARcO -5B5D~kr9G2B0+4RQZ#R|Ed-Vsf&i>#Ow$O!6Ct{8IVlMoekRXH@HDkhmf -_prjJn^8+HC+$d_b4#FZUSW)e@IL)kl3fp~n3TmYxnGZ5k*Mlz;1gtMVfI!{979K7KX|S%QCq~tmagB -UHt1>!~E2=r8^noh7B4L7t>(M>QMJ$FiMrHtHl)*_5B%@9Vs*_^Vb+INPrWos*cxWN*BG??MLJ&NoDj -LNw5~K?Hq1X;Re;gofoLQMCPNSYF%nJPq)@hcDQ3%&#E~Qw?aFz#Q!_n@?_4E{C@aS?r;AZ5jY16i4@ -X-30q&;Zh2I)#ZAc*G|z;0YDj@TnXx1yea^+H11W~YhqG?i>iAj2GE#~F#(6vd{^_?yLwDcGz%QR+`F -44chH8Nq)v6=XTu&OydpolNgDCnGuQE$KuoG~Njh3cym^#m+1j+13AIHHjhzbm=3KIC -LZf|4QcMt@hs;kc|Eac=ClOQ1nhzsICRMCbscHZ8xM_F2+us~r^hW>?+?2#F6$-eR>AsG%AZOP;nM-> -ey3Zt5lkN$-8YM%=}v@BAvQQKVQaLZPqwc4d3#L(C>85mUnIUTr}a)l~!nU+nm$!KnWkW%6cWft2&7r0Q3oyNBV??jg%9CA-9V+FWmE9Ql`Q}Wj{ta83|%8e0UF*cbGf@9B -O}i{$tRwIW@!8ta@61iAY#kNz!;-8nLGXw^oD~TR#*N!~XBuQ^76nbQpyL&^ -gn~v*Q2E!keT6irL_wJZ0UqkJpqvWw;xI!a+J_Mbkk83pp1q=ff?NfK2oR+G@7S@<0NiJVGDdMr~w0PEml5WRO~91WVg`4&<#(^^oPbQSf6VDg~$kln2>Fgbd@c97 -5y+*9(F}AtfgMmCYthKrAQ;0itD0)=$=rHVD}ccxXR?^Q;_V7}eGF@vE=@hZ0FAYd96QBbgl(p0sLYmIgz!hipE2P+^l{^QM#V -(WIc?0isCUS4?>ldu)x54DVoR+GK1A=$ZgSRLaiMs?`&ba@M@=h6-PU}#YFJ15ER(NINgFz4!U;JswfY -})9SS23|kCg}T#lDk8c;EvFHYl0(yKFXm4!BLJxf^JFxT7l1pX#mHrT%5q-!9! -0XX~*RQM=~d}F$c03sZ8R=?Nke+HRYy+BiBs6fNcWTI8p1Fy^LmK=F`OGQQ44(d1_`RN@qIFix0D9q^ -Kl@nWF$A3x&_{YwKKZ5;0+P26+O|Ajs+~lC`hZu?a9Z~-% -Uc#9oN(Y5qm`x^Eb+F`>`dciXCULKdy?Rci@}XlM04)8Ig=&YOLfpEfk#igM-Lb9l-N8)X(9|yz&8`r -Ayz5Cl6EfZ$gnF5Y-bWlR(+Yi#|R#d9&fu9H3I@+2{9^*tq!u!>-I1~<#nZk0t8cs+vXQL_~s#~6UU?*m$RrTAL@f?g2lxA -9`MO3}QXA5$VC6yO~#TW^{esQ|&+?4Fe;NlU?>>`Z|uYXYNPXuGk?U?a# -~5VWW*!`z1AG^j0$&4@227?u^3VWwcStJ^;!JJlaJ{wQW4)L7Nl9?3>f+SExo&;BGOHg4K*X-?K?;e+ -5jtJOs13G%0CwBL -lmgsB3p1aYSnACJbCk&*Z}rf|8$n*6(&FK;bUF5*du6m!q%iYXc=*AB3}p21+uDoMJ8Nx@)MN5|*_MZ -3CrSxW_+R0ISl~E01=7_Ai~x@|87AwcMXD@pd5dXJ?d+aq3*TI%}vMP&P32p$_G@zqDfiX*!L3D;=RbhYShC@>l<8;?9RqRF~-U`(dR_kRO_VZxN(a%htpoQ&t_)Q2X- -b$H0xuMhNQP4z=RqX}i{Qz;I4X{c}JGe3x+=R-9-8m2KB+W{In@5?&GhvUdJf5{v=@zXWY+ -_JUh=F>T#z-$4)$=E_NUJA>uOc3PAI4Hitq=Z{s;T62*1= -h*3l?E%;5Z={N7IyKxG3+!@oZf%Ru>09P0;b-wA*B$=Ba> -)O;NCVX9C~ya=^^tBqx-aeR)Z@OVH130N9&t=nxiWPdm4xGrMz4o+Qnl0L6Lx3k$sUhk2`I@FSu*)ZDE{A8%45!_`e -5gNo8ck_DiU!kUK>hJ7dZsI}?N*x>8>X3+YpI2z$mC4YQE7fir9@!tnljje)-iOXrI(0&Lzk% -2T)uy6MXtJD3w}*!io`lZ-Fc$fx$z@SDrb_21r(x~cmhPzG({xE=c3?_Rtl6TPd9_k9L-JT=tR0hZv~ -6r|b5?5D5J?V1XlMYxQ}=;b!r$Ee%GSX`pMu`;wgY09jHQn74S#fV+a%tCbWIA~u*b~cq6FtkbCP-oy -Mgxj03Zr|!x+(usd8LYstW|f(zns#>ukB8aQZ^u!Z;46izde#Hw@LySRav2Z4dMWp4C3f3 -GQ$37JX*Xo?1!VpbdV%8oW&H(51h62on0cLJJb44`l -)9p)$sHsc)QbLMI_|??3f=_jkQN?|Vec1eR-u2Ghs?xJ25mR$)y^LAlB{!aDX_YgVWq{c9f4q*7k?Yt -Iq93H$lZd&t3dk$UYsMy(XtMruB$MLSQ@cqV(#F$FNExoc+nkT6(A&++3sM~HP)JFKD;J?F|_qSIuOP -G*mTajVtJlyGiu-ZSi6CXRK1ZGqV+C`~1nPMUTctKwj9j;pYKI`5#mw2KH%3Ag81I1VbLVE5%h-JM^# -e#m_$*X8(lxy5SqFz&4XqIV$g>r;`3o~Bxq{IO8WzOtHbaM#^)d~*##3B9hw`fBBaV~ -Iqm7U#=CdIX;&+95uLZh*z0%!bXMB3vmH5j~G8w1lcvOFiK=`KvkO&7bmF=DQM*7HOp9?9O)3F^N;>x`!BA2@8-Sj`FD5Q=a2V)y?^uK_4d)jt -Gn&~?e?dK|9$`Z^^5rZ`|p2zbN}+iyNAc^7Ma{bzUY-oAYC!`=Ti?ES-=xAE)a4-b$25(95vJUq_7zkIxV@$T-`_RY_Z_|K1e@yJ*|{2Y(^?%np -QkAA(S^Bgh6zr6f|?aAxc+cU0xyFI&md-wSFyI1o~K6-fb?(zPcALBKD_4e1ljlXQ$pI$uvZTsT>^geUu~b>y^Wb}f4qBh_ZV;U#gE^-zJD1ve -|rD&?#bENz4e+UtLwZ&UKzIgNQ=a_g5@TZTTee~~f$CD4Ae){zJUzq -(TPoIDG@t0q2pM3sod$N7;tL?iNf4_@uetCBv61BYu75U-k|F1oK{qW|mzHi*}j|KhRcK_}6=HcD#_S55i$ -WBP#KkaUe@{j$!4WWPe!R^*kT=e42-(JTSd>OZYa{p}%{mJWxhsWFP!-u!;xb#m?;@&-{w7;Lqp0@3) -FXJ!&)qmT^pZ($g(Wtz469a$z)kn|&j7I$Dk9TighFHG%=Jnmwv=AV~cQ -Re{uiQi#Pv03{WV^i^rJ$7je}C?-qCd_~zyP>$vcLeE06%55Ilyr=NcM;O^CrAH4hNw#7?H|;fj}(8(?YREb`e^*ArF{LV*ZHTk=eqth&N3=D?scp`oxP3X)<1o9_hS3QCF)G>8S -B6O{u=8yze?|~fBt&=ef~hB^eko=ul&#Vy@?xRHoy5*t9yDs{`KqM?;oVSO%40`hcJ|{|JCDOe1m%A_ -TP{-=Ja}dcoX{j_8nUG{f{rd+rA5Hr)3E{^z0AMZf@L{ub=Dtw`Sj~QJpJAi^y3;m)bZKZ&p-eAlV>0Q&#ym!_J@z -3#Rb2*37eX3)7bah)b`^xCf;sSE$KF__PC8t&-gJi|2l4aNqcopWV9&NcA4-^4_C?aeoi%SMmYuAN8c^Ok4FyT@wf(K2(2TSt$J!ve5) -@wb$nk27m?;ISV!@h1H??=41ZJDU>sZ^!Vmcw*w#lNU%ar&s`2o4qW~INio8GN~gzJzO3J&iSVE?zx^ -Zu`@O$estbFcKc*ykJ|VV3wZ1<4lrD;`nj2TIS9ic!W*EOGpn%iz%!nL81Um;|q#_A|5FF_R -urm1^DjTV?ckpIR9%Ha<4+WRutCjTW1idi;GqV-l`xign6+yk|(3ugD}?2+7$I;rJlt6^~HTdi)v}H= -e_b{xajf-)v`$8Sm)=#3VweVx!K&LXUXGjHJbT#%9H{JF`f6tPZmaV{+$R(R3Qy63#a?L3Z~c%3k87`(UvwLmvg(;TtTqpgP|}nS!M1-IZ?!rb# -7y!V)4>A`x7jk-3o6|hDNGBdv|u&jVFS@h=#a}eLf&iHmmCv+$E9&YkIfJJg -Mu8$XN(jNV`2y+1~j4$Y|6=A#NXoLjQ9_BP^;0{_~F2DZM`Yls;oxE{ju$d`#ZPBBSHiam&C0H_7s6Y -7>|Qp#kw3wzUq_nHE@Z|J7S9Q5;0k2vBs^jY}mPw?RdmsDSS5XzB>rk1!uO0b<1i -ls@mDcm_)o&{Ce;zjyWol63+>FV9P?+Qx8cAIrPoka~_uR8u-XJF)>9LjX;@dykczcKq?Mig=;mX1I@ -v_?=Gcgb89RfS`uC<9&xOJ`-mAA|BlWSOhW8YMP|biRbIeXWTcS8csBp)E*_SssvrfQ6C$)Pl`dgeS! -4ITV+muU8jr>6%cBaHd!&4GB__usRz{9y3w5;$_{L&c5}*! -!VX1(kzP3y<)p!W_>KF2+I+Mp5GFFqlRw9qQnhHn7A;9UW@l+w|J>oarFQ@iFt1-Y-n@AkH$vrYhVJF -BfIR3j^^S#VzZFB;%>zUC&E0iAKCpy@WOcVDc;NYA$(S-Ze0UMz!GDk0yua~LcAeD63+?IQp0&fC`iU -`$GAt|}M?3nNA-xt;Fs&` -iV*64|8#?^*HY`*2J@NNeB-SlGglAdCxJ7RwfIhk|94X=?-x8Ml5EM4h;^p?N(PASGNS8=nZ&O1)G%6 -R<=O5Q2h&5mH0|I~YAS^uW$V=z&zR_{IQ;P%kRTNsQ=HsQ)J+PFVv-z!I0aJwz>GFhV0@J%}-@L3WSb -iu>6&9Bd!hkA!~;s}n&T62-KSZaA_uFSIFCTD6Fvu@qo*^@Z$(9joRCQO -rV>y;oij4Efg`H_QPMq8r*m(WR@JGRZ5HaB(_kq*S1~wS)<(guPB48n;*iNlIAxX`oB*- -i#KVlUEiNUSJgFHK4B?dOmVdJaXT(~4eat#~-OIE>8W>U~=G(XJ5j;(1tf{>LhW1+)zp2Xp}z*usU7! -sqw-wdVJ<`n{#d=LZ;=Gia~n0$VutWi#8J?Kl=&Ukl(KD&6@lmiPv0GE}XYv2f2D!LWBrEx$$3@LO9L -?g8th!C-bEX5QFoOfmJ#||1qMJgA=p#oT^j55?>jMp$x -%{vsqYv4hu(3~f>iadZ&FocY7h$*&;QEuuh7MI0mDH8^#5F|%%wG%Vrq{=e%-tL|&y*yykoL}#63pksguV=I9Oi7TPC0WtOqlTa&*Pb!%Rz>w -1Tlod!m;>hEhZPii|lF{q1f`9P$70ZrWijuJI -j*KSU91GI9QDf%VQc%#I+eioV%~rN%7c_^BS0drO$+36(Gc#@d8Re_Yy05vRg!@MHIGh?c#Utce)l;9*0Li2KJ!WJ3Lr6 -c%4a!i*DuCGnGXEIB?RU@6n~sq+Vae=KQp+3pXf=?)Df46^dXQ%tBp3;X%NHD+rMR%Bt9klbERZFLo8_%PjtB0A{orKk}&`ElSUc2y -3_3QiRc9^N6X)?u2Acy)+@~-q2=3PQ$}uV5S>FNKAt#NJrMw4s7`Xo5Fg;=G7_qC@4A10mL=ui3jSZW -3rSdL9R*R{1q>KvQQ^`NwRlbMks>Jh&HQsl6a7{ZzK&!N~GbsHd9&95ToZ}DCp~v@p+xu2te5})*6Qk -y~#eh#)`#{#1CHp(1U1V+x(!gTI`#t6M!#n75o76>cGVNzbr)rEL~p|;d=~sDj%50go1zsnUje*Zl_` -FfhkoZG9L(KdkOLWmE43$iaZZXNOG0LO9s#1Sy{rW{nZ$YQTEASSgwFa(&*p3Go%VcJs>5?m$nfgc4Gn*&G081XUIg!nvx-)u?f2aihHSp`km{OvH&u>@-nI29AIwnctri( -IyW23vV25@rd3u!?pT_#%X)xat#f(7$Tp11JhFl74;qjLqK$p(OORM3N%F_N;oMyM}tgaA -E~7Nl?J&GB@*5G3Ku6<3@FEJzFPwt^l+4B^W%FoquM7?~J}az?&T8dZf7NIIOmuqLjl)FsC!0ZS5W(n -*5hl8a|%w^`pfhPuuwm=ZO$8wlG*x@O#{D$K}Kz=wT9j1H8$fy;uB#8N1I2eb2EtmnbHi -DBTWtc{FSRS3IQZ!#V*9wlIn7P+3fGhAO!JJSQe8mo2+ga;Zh*sGI}4!KEkh -<@II(kldp1G9rXX|FuWFfhI9XPv^fhnb0ipmc*p$y5*sj7WpK{+dcuPQ -NTgXY)eKsvC_t&G_6hR}4HpW2nJ|k~N2ZSvDh$ZpI{v)ivXR&#OL_FiAXZ_$y29dp7-`nFA)Fs -T(3eq?~Bz+)xXGY1)dIXC&!WOgbB)A%;kA9+vkUI5c8LJ)%3Ha)1FjsV~T89~M}-V|nRtZxOJR`9Spg -G(pf0Q)v*0wT^PI0HO!Yy)ejDn-h=4GB%UXwZ;*QhuLf_r642r9QY%#&@mHlAUMHGlc&`(opC3ucRoH -F9Sm`a6}SebB$O?T5$sipBxyGsLsg0Gt8o@uAMS=NJ6xl^}V%i^I$>K|aqLanVn3)KYN)id_8r -Xp45Zkmub?^f%Gn516WbMLJ5{9BQ19Y1xu9mrt7MwcQVWQ6IOv4>ivL+K09h!L}=@>5f1oW)ni-0Q|( -z|KNs%87G=^&+G#zlqgYx8mhEakLLXN(GJ)EKj=+$6uuvrykCq_9gb@{xo|7;$wcv{vW@P~?!$4j{bb$Y6lD%v>o6R!~P9~a5jxz`gf}e;+Dpi(Vr

A(A8wg4i+N54F+|^%q|YJy0-J&rCe$w!8Pa)K -w?e>zTaPJ9YQVJ-4|Zufr$M%RbTWwMT}#FwdelRF5MM%q4^3qbOi8FuIY#iwVW!VDJ*`#?$!7&PtioA -+z$;~073}JvWj+a)FY6X*+Ra9lsDRpJ;eB%X4OQ7rmlwchw|vabFi5s)bckXT+JZA>^W**U(lu)~c|| -ne+yW{?6^AS)4Wu-!p?O5#4OpP+X%4Z4!n%`BnugZ36qSIbA8rcaw&sv^0+~xxc?w>3!<=+#VwqxWCO -C*(g;^9#%n8P>Xyj|g1T5J+k67oxzfsSt$w0&xFc&YbqB{sSnS~G#h@q4NOR^NlW!*w8LqQ~I00ZvCP -ubkyU@JNs1;$169tK!sjU=d=3hsR1B~){8Vfnb00)*vktb(w)x=D0bte#fU*vkDrhhGn)9{*PO!D{1p -&Lrt$1uTt#MVvJc7xy3kQL9LRXWPQ6f -cX&7VFmKj1}1^2rh76cFFU#6qJN4i0VN|UkdqTH?l;QI>x`6@inPk^8fJCg!OT;218LtOU`RxI4mQZY -M>U`F%^8~j>*s2b#HtC@~MF9T8NV@M}ZynS-4oFf(ZB`+SJF0_Mgd3K|V>oS?Da_i*+lahfTza_!h}8 -GVE~kHO<9aa(qw{Qt;E{1caAn8#_wh4c7@cFt4nll^4?38CU@}O`r@{dwR!G>5$Z$oF`F=-Bx%k8Z$* -I$EjN>C3Heu0o3@xJL*eNB=|&K!*pD0e1Nc^pz&ykcv+U5Q{?S@cZ5{CpDhERh;VHxQc+DVKtnnPy_} -1z7;$Ep&0Qb_@0p42k{nNBb^_Xtc~92fPD&_P>u?@KkkZO@d_^gTV|m$%0}czJ({{{yF*dR2Oqu5_G+ -G_AK#b4Co@(te#Tm9iHl|6E1@oG5hr`lMg?Wi&v2NmS2oIU19K3@tl%!*W#!!k2VBOF%lN8osrQjMk0 -+vG5m9dT~8LlG}xw)VrRo!xSCmMwjVr3~|6qpHxszj_TduhB*qzhtQC_<1VW -&EG6)V*r?*FMr2<=)TbKDA{te)4-+t+JKb}{`dqFF(rt$-7k2>%Sqv5}qj=c_pr|Y3RR`lKfeCC)qs~SP1KV#ik-rrPWh0PtfTQVdOC58tOH01T1M1D&%4j1)9`0p@ -yOjaLeUws%oqUWDvlOlBU@ti{s*SWu}mBGi0;^H?8pQ9W#^h2QdkfQcVIX-39q}L`eVARda%+-E;_6= -#`E^x=l!WEfaYqGhkwwbag<7qA5MZ29~%%x~-ID5= -Fg4P*`B@X5s8UIV?tzpjfaSk{+4Fd!|X*UvVge`t`il7}71=OOggIBkTh|24P9WI;F{`6{zu1^v!f<LCW-t%T(egF$q2_7WunSU*u-H;F=lF*#QLL-05?ujU@oQ -p@_Al)X&PUSS{pz3rQ#-f|VB}J+~;B2gx&*6QBn+Ii_>_2P!5UZ;$-5}j|86ibG3$M?L9bC|Az%nM90 -syv4&s2J?8=BBsNOmcRFjkV@B;6*Vc-@OJz#wgG!bud80NT0$CyLlbf)2TMSV7uYfX55TXQ$LM2hRAs -Q|rjGbD0d%OztV}Czo9SO1DTjl)K&wgOgAD0?

Xcbr*q}v1sCuLnQ|7wa1hCl)nuv4XiS*mR?YBO+ -OGqJKTo~6YPo_MT*DG3oWqum%wpNfwd28FDh+!eU_VP=_8m`w|fpw8I}0tih($dXaAdd-|DBOK!CfGhMXB}U)6lwLItFj7k1nCvmZ)g={@Zgs{$ -2wljZj*SGulhEdh=k9>Ju;Zw7kjy6sQAk`KF0PW7mJynyTheF&taH||fdNSxSecEwV-acqyDo>YrWMG -pb@F}CCR^6pwZ;*!RB1<4eiGidLmcVAw_VoEX?O?(39yr}DTP8~rCBSq6ur*aA7IJ0u+0YPHXnAt$o? -RTOL~3*246Od>fsTEHSwOONS{bDk~XW(RT9loX-Kzm(}5lUuN`JX#46nYlE0xeI#X-25*n@LPAi?`Vz-4;@+eoS()0X2e`ggSuT4Oplf7@H@n7sdPu^K3a=MbJrH -3Igdi0foiR;3)z1CwmP%Yxc58k)GKIj?0V6Tn=J1q5>7Ff5$nvfu5N&^Y^T_~%R?koV>K>G{)a(Xn`OE?pk97DQI#ZC@ZL5dKPO -0xlLhi++meIN0;`8W30V5ym%Szu*8ixJWw-4>F8LLDU=6y`ZGQUI!j7>8m9xEZghmtypFdzzzG8veQ=P@Xf>({V?M@Sy`&VGIq{YYif{gvki{$YC7DO4%MWK!Omo -l4qjBXQ%>*J4IxDqNw4HjT>`dEA?1>aX23$2vo^(+75R3=0MslX0w-8uIh*4WGE%cBr=2!rk+!!A+3y -SHZIEs&T`1koT5Fk2&U`gZLVbsVhc@H$q~qvTq1gZwlZe=ZD6REc3c7@jX>9dHSh8YIg)e{+Al0<29hQWGGEFt_@Iw#zzp5$rhdRjCS9PpGx-I6zyEe`0Jcy{dR+Ypo@t&Df%fC1T -xfH}n8ExF`4<7z&;cgAmZ8dI|>vJluHuETRGCcscC8LocRgq75pQpwSstEd$hV8bt3AI#y6v_wH-$_C-o+t_Hj@-G=s*PX)vT*)o-x1(uGP9-yKSg{xqnEviO@Qe2^Mo#G_Tvf*fl~n99 -S2!a3O5e{eZmJK4Ro}wr;MHP=j<^tEEIe%OP`xeYavpdTS!2*(HzP@&rQ2{p6vYGTosFHJ5BDh@@L0? -aoX}mmGLH9sUMLD6&6YZP!Pzp%WqkW}yyx>Je?wS?&2v(yh#Y${1xfEfK!}EhcMKS!mBg2Bzk5D^&pr -Kw}z9Q#2y-1)4TUw}qbLei&a1ShHn^@#tir0})9BuOw!y2St-?V6>CG+8&#YPy4mTkZvhW>GSj0i7nyP#-N43X%S%6PASF{WF$ONfrOByog7c6NDf%#u|;f8{Ok7Q2a6llV?~j&K -G>St+7-m*z>jwXf;W2o1@3Fq?+hCgeKn6BuBNq=&INaauAR6CShCwK~AkJV>`y%%EF!2NSYi8W)^6kf -4yhRQF9*7FlG5c#)-24!34FGcQ?E0qGVpJ?MdyDeVd|6{2OqoR0TewWmNGcG1&roDUW0)Cf@8+-=-lD -h=tjPy@<1^GsPL%`M;YCC$5#3IIw9Np)^07L#AoY)BC=HHYZez@0qrKm`oKfaiCk5k=61iP-)y$xpJA -d8{BEAHW+glvN7gZ2iYlK)S6GAY~RD#QgHh)HfZx(nmecUde7~EamcWiiFT8BE+w9US<>0ZP%Y1a(&b -YLOF11D2MSxq)JZ-k2S&9&2~~Sfda2L4VmgP|5w*$gLGS@#o=GGysK%1P$3(f?Lw6<3U|6ff7>+0Ng^ -(B0cN7u0&`w?1*F?dbe8F%tkB}RfQvJpC@C2ZGcHwzFzar=BCes%Y{DX^`SGg#x6`&}BmnrNU@LMGvq -T%=(A8=dN@gi}${v8-1i?tgsKsu5oqJz~9@1?hAdflu1E_XB0tuKJZipnBLBqD;B{^==5}^8ahZ9Eje -i?d5w;Av*?P42ZhGJj93ba*sI?=mL*HH3$R}p1a@cTdPGIJ!&`XQ;yAy3A^~0P&$}UvKnnmByOnex@8HO?&>o1$8Je -TTSFQRv$9i8OJMJLCY4enP>}B7Hgi2#PaBDZ%q(?uFX?OG2v{<4UKegWtgT>xjaqQ=P#Ieg^nLWPNfo -aiTWvbgl>I8Qh5)IZGS1UO;34fr7T@hsCVI|bnb8|qt%}@-o=jzCk!?za5!5v<3hiOshFD?S4Q~Eq9# -R9(KPhutnd9`gEkZu#Hd7%&$dxQrgp9MatlDeg5Qu!^Dxi138ffE|w!yZJUo7GUVmIBgk$&&0*5E6tu -nRCO9X36b?uC~|OHRTiuIl(qLM5lN2R(qZHYhX%3ru(x41_gt#(>YZob0$VAHvX~Y@Mkvb{C2efu2hM -dvq7;~U>R%R1L~g#ji{;w&tt~%=&*HM47F#obUCyt7zk^f=9x_{YBLe}YG6Ge-4-gC8)hnbB#wMLIHw -6nq!5E76FayJ8I&?h8#3$?xzy~XxiZ&`DG6ouh_thtG-!l~Yq}tgx`lOX_V;O1EYuY(0eVeVYwPz|@$ -muaHd`7sn?c;mkWYyVJZ;DF#147mNvU#;9G!iuOv*e@+qEFbBO9;~r`h!-jiSw@K>`!X1V!dxt;=FT9*}PHjByPc4W5+>^EVZwYQ!)JGz}qw+i56g>LO>uc3t!1#f?26-4+itIW2Ra -z8$1WAsI?a-C9KPUw~4~j`B^i&DyRO*^c_b^Rg9?ZmVQig2E(A#io+OCNAB#ZQ7@+fSnYD@>mu4DyMa -FDiy`!O4e~H2c+BHB@3K3g;5ixJxlCDjN5VgjZtSI1Y8)1w~-ZUp#CUA4fa?`B+1d$ -Ax7NViFFMmnLbN}i}d(YL|@E@HihOs~_|_GBQ;u0*!eVyZic&sp`n1JZ4UK3r_x>!!yZ;L+VJ>`$?Hz -*_)D4hh(B#=vDWtk4SU7~UiAxHhjCupmalzZ6M(H>9vEOR}iUOh1Us9!fb``6TSkP@7;ee*qmom2sBn$WQ)F92(HjnHK)Al-He8EkKdX_1=!O6JX|8t{IS7~MT{MJUTNb>u@ -DfEl^l*Le9nNVl6O>_~Jt?Zm2PV%V?h@*oQub$dFg;=w2AR;>>kD8$qCB`bkEAl-H_*ea$iv&n-3Tw{ -z&8B%=k{3Or48<*&_3`k9Gx;YWq|0>2Fbp|Xfkvx76>=HCZKC|J&@VFEI#g+j(&2u(zNYqw)pN+rhz` -0^KNViFyvBg7t0ODB{zs@p&K;b8hw|l&q^ECCMcE&r-24h>0e_3PCVG%B{t4}nTU(=ikiL#*!PFpi!1 -3Ur81{pq`auM_(c1^CsM-E801=u|c$$De}5lZ$fg2`RN*h<8!^M`I#FaeH~k!tr|z0cY_NVk-|F(2Y- -06wG@Gzj#r1W<+qxGWQ(JY6VCegexb6g4{FzO*lx_keV}wRz$pLZ+%{?dMzQ`N2yizT0}P)3^5K(QY` -oYE4{L)ne!!kZw18Uh-Hwk;-|F%jqMG928Qd;*=KaYm>VPX>_}*$XfIT{aRxWjzf;MX+$_R8|R${7c` -5V8Vo-mZ?;q-Kh$O%$-VYvXXR*lX-K!6(8nHms2Smhq-+`J;IhN&Eid3P6Q<1Bo;&Ml^ME_`-)M8$3Q -4y*Nje%ga+6|BjNC7Gr2-L@-Qy7(Sj??Y1B#~;JgYvj@?)(rq}x91Kb7ff`NsnTSv!3M34IxspNY+Pi -w77cP9`cg9nP~chodi_2kAD+kr2Frc^0Qm@bkVqVH}%3iy_GL4)1X(wq;UUgCDhVZr|Bd?c;N -@33D$M!wJuaaJb)tN6D%lM|0I@iaW$4O%kRDN_fe+djnO4*9kw4TKLZ!RjhS+n0T -g=@46UMf8$x32N!dVvj|lkpK^(A=T)%?8-_n;@rf%+1FN^G5Lq@<84daAl>%4Gu5L!WGiT0X+R?p5+m -!L=FsFQA=|^=d15zwj%P5iKYd*8AJT0_plY6HQ+eJ4?v}vnw3^LA+i84S-Iw4Y1`1p!V56i^3d>_bHw -UEKL~@g0A?0Ro5b;8{)C)wF#B_AVt`_Ka_O*GoF%hi;8BDrdC&wI+ZnJR-*hHUAOT2(gci6;z$eh)50 -kc@Ar~SH+(O1JRs=>59;3}7`fOMPfjFV3_<{@vMDoF@F&w?Y=&#so8G9CkoO`FqpU?K!MB-?%s42Q*X -E#>rtlS`gYK%|{P>oXWxl@;O96HTEX*7NC}A&JsA5<=&C#mA6tH+hm0Iz(>mbpUEj2Cwk0I>p%9=G?J -PGQDQiia0(?;bPs^X=w+f+YK%46bd@{b@w0~qKRydJo|+VUhUnGT?ndgdn#Z+F7kv9$rMW}h9x0Fn(U -FJyV19%1C9gMAW3Q(J{$0^_121^F-#u#2R>M*?c~rrj|d~fq0C%lqqKFD -$Kgr0&3BS@Ol45=XVE`t2_=M80|5^o;mtfH#vVj5@@(kg2Wu%#IV_4Mg)wMpMGj1{54w2hg9n=u3UZx -ALC?VyZY0qy9&5T`gr!OCn2W`n?DP|n@C+H=oe;W+NQzAkPn#x^VSh3Npd{cr)){&Sq}%P(1^c -qIT43#MXE@eVZv*|rwr0CvZ4iR8U9xo{d)L8ATDu0OOUR1wuB6XaT8qvG0=&iH2abe5cq}*N0uq_-N! -K2cIy|Fn5ke10w^gNUHa>gykR=c(!5*yyQdqdF+Al<_PnqgR9xo|iD2LroS6l({=gnDXWAv* -iV!zl0VyU?c?Z?2^DF|z0RKecltyTN^MxAGL%Pj}XXTiLLBaP_6aiyT&pg%U;sG&qE#Sg9sie%4vL -%GW+1llCt-!lOy6u*LZO|Ti3oZkSuk%old9(q{xp|Z!>o!9VRK;dV>ch}g2zgUE4h9+}jA4tOc`{A4->^@b+U?h%%|!n{t7>5y)VM|k7BiU%5im-f@H8XA8{i7 -MszekuXUf1&UcqOFLWb7fBcbq!3wVlz)Oro+bL!n^*_vj_+|Gql#q8s|PAhOr{OM17LS_SoQjF3MDgb -Xz^(*CSZ0FhL#6Ha!BRb#S6emr*4|%Gt(aBt?1@uv2YwK7=lHX&$88y4&06vAkv!p7$&IASv6UAw3j)nGAM-z#5}L;ZdPJ+|^Yhv>tw1G9(s -!o=?y`iXJY6r-9B7$S09wh`bwMQNlr25`i@JZLW+LUjx(De9rsX^5Ub}a(KbipZe*c&`O$pYh8quq@% -cHnjX7nRHUVOkZ!AJn}>AE3d^Q*!#qH!!E&dkFM+EnxSmNmrQD;wH5f!(i@3oV#xY~!UXN?B5z;aQ6@ -*NyVqHIY6%uz^4f~{vcJJ^5HH`4^mw^vA&)s4@nlttUH#U!59kNU$@yuiCtk`RRh(&Sa9zmH^_!Y)>N -w=P>?g200tsTZvw@nHVr`rB#n*n)AZPTWxKI{tKuY0KKZi~c{<6=ojvZDuSkpq#hB)?mLguZ$%#qQY* -A|C`X9&*w>M6`MSmO@W1yt;%q{4Ep&N8zCjBu821u9!0LSO*@bX5pvrlDUZIR!1S -{Lq)1!L!M0cS@34K!+61w44gd?0fFFG<2i}e!}!Pr*xiFyF2{iEx?6$$Q4z%^{WciFnPw-23>(`DEwxGlrpE6nb>|{(&=(%I!q#!W{DWGg -)GWI4Dx8h(!BIBLdyt;zG<$Lbr0N9mJ##8Rr|8-*eg$p3VJke2!-!G!H&kHXCC;0&0^(%9pn!vps^9; -h3l;8@jq3p%WidkNjSTlc&p1MxC0cp7s3#E$-W`e?u4x{LM_D8(5dO`T!MSVqS0@$?a5rW5J6tegc88 -s!!Jduj_n1(0mLH}YYPOD}xgJhqdx;tbB{%KqS_(+Fl&ZTGU6SuyRWQGlrzx&LM7YJFzjdR)nVVq(hR -ya^kiL=^$D5Xyn=}K2Q!&!o>x{9zI!PmKzx>04E9YY!liHbG^afkTtSf3I4s?=@;orz^H>XyK!=+TQ^A-l0i0*8TXnM24{pa0ixVy1 -k89uvSO(c{kzqqNQ|+C#eBDF@Y+EHeoad*9%nXvSQs&pRcpYiq>DMeQTLI}do5Xosg6AG(sfTgdm?FLE1+=fATTcW8nlIq3E{ -g0^7K*|L7}*d*r8GJAe9gG9dBUBbDRAD$j1h@3;W<~Rki(-k)4YUmD7I`6t0WWMA;39LM!g2MB(!_}A -kkTK5SQFX;PX7)t4fdsqZi2mNR`MB9yL7N3d>yc0`#cu#*97Z1LsjZ{nF8kKUnv}U4Z0SC9ri}&kq!mmlg}$-dP$x*J#j>6AGMkWYi-)?N>c&8LL=}e -;M4@rs>NhHx8jH5O-B=e%NoGPK+pE?@-GZe+0?h9{n-g;11R1rH2M?=eD9E;~cENWIt>=g8Zmcw{qep -?MshQe0Bd^$o2~Uo}U{uExHTJQ-G21v0%jzXa)I0^up3H0D%H~z&6Hauvm$BP8m@HQS2#VhfKd_rpn1 -#LcTR|$T?LkyRFTV`zIV@VJ=4(Ur^cY(n2I5`eDvw<|N4|lzAL3?|@~RZFAr;6bh{fCNo^%WJJUpmca -`EiT1aGCtQ6=pU4`vtTJXrvxS1oBSv_EV%y$0ssH~_&xqCCuuhazk~tEX~3r!a#aXoY9hNpRQ7nqSW1 -7Y{940qM48c?O2{r)^eje$xK(*Ib!Uz0)ZNe9L5 -UCLa>2>#Hk;J>}kQ_u-It9toy;Q)L=HXOArNJCQ1qXDZLD|0&%O>DPj+UjCz*3y%PreyeXO0wOQl;gD -6#kXgzJLX}Ylh!mRQVf`(84Q_)SGQc(=i6X1EqPohF^*2mqsz!IJ7?6>R$Ajg79C&0Mp2&7V7Fnk;~8saoXGp@ugqsQ3(_r5(A`y2UEOrMefdNE!P96;<54u2#slh) -XVJfOrMBH_Q?g~6S-Fu~D2hzRBpnszhg3=g#%?G@ENC4|M_PJ`%13@>WlFYWa`G2AP(NkwQc*HOMNw& -)3NMXz_-RXHcRxT(b7S}M)UxoL(YoF-{`!=#4Ody?sja6@bW6R@pZ^BM#qxkcrD)OAI$E$}`^wqAlgM -aq<+}35cHHehh!9PL{Z4bKHdYpNM^Na=8=BXCYxJRiSfk|H3R^cW<(8S8$^9FIJy2~bN`)rNnRI)47~ -x6i>8nc5^dScBM-OQ`Ck}1GrnX-0F!cp18-1h9$un`g;gwW7{e -&_B3u|&Rk`<1Ojggynm<7)@TE*VQ5;T!#!X4wF5A-X1oY1kt&xM;z-(~P9v(Qe^AzJL&ozG01M%~Uzg -E7b)9YUz2De48y33a2mh94;2&G7YqEr&Z=*kN1YX}FyWUz?70TK2`<5BX`s2`4!=^&1 -2I9pIi^Kg-jqUAVT(0sX=&*DKq6f++!Zp^%7^2y>Hl!bwl8G9j>+qeFL@j3`p8EwTYW6UW_Y!nInMau -fOW~hwb?D_@dZ0G~YoV4*>db1L{gYElex?nlaw~M7ugD(BmLy^$m6-E#PK9o7Ahsqe+rM_{#DV>DC-h -b-#?(cek-uH->F(TJKG?+gA&m+=qwQ_3;3W{~Q71psYty!ag%&++qO)BMOzxEs<8?&G9vIh*Vi_{zEv -1)}#w^H*F7VR=|lbP%@M+j(6d)Msr0Werb$@Am8M2K@#JDj4EJm-p^s#o!3HJLpQ#;sPWgme4oUSQ|4 -Zd?l-3#>*?tChsfS*<#bRep#!A6Mb}bly>QX;%@P0JrB_Bn~F5AnwbBwmUyd{eXS%#7Zr<15k*>5T_+z7%ePuP>;H|sl`Q{ph5_(gK%~g0Jw$C|hua`N;9COUM&Icd-{ -(t4ur+<3(Rr&JSXHUO+@w9yU<=3D6bNO%H`@zKr<>i~}^7Z}AUvA!B-ImYpUSF4+hw|sU|G2rmy~^)D -{q)P*n^#wlclYJh?alSu$MP!g`Q+l)k3M^Q^LUd_et!Me>)X4ZukXv+-`pNBsLoUEMR*&+qc6H;?7R&ps;kJdX_VyKn!bJh{Cs&w1 -}dd4BzHegD_%*K;MG-MxLhzxm;pEc3&MkN$7|Qp%sN?*FZPb94Kb`Btf$G>G_e=P6rekrf6-j@68*EbJy_-w>uxqAEh4|n(Z{7-kUZ+?8opMH7!I -?G|!kJtA2%+sllQ_m+Qs_VS;;{`O^g^3`|cU!OdG{^YBd-~BP~%}(dloZlSj=BJ --yg0D+fb$|8t@m(gK0sj2-`Dg!>Pdxeb*_Y2=e#h*;c=qzEr!QWVFTQ?Wo|JE%Jb(G@vv0qA^1OWW?e -lNGe)05^QeIqN|GLF&=I?Fte{S)Q8S^GA4^+w19p`5Oz&;{N^X``5p|Rxb1DyPwLN|Je4eGw_;G%f}!8V_81gPk!=;yik`m-oJ -3R_b=3*7uqt8Utc)hzp!k1Aw$j=>L<(c>kG~4JbQf&bo^?~c*=9Oxcq~Ut9|{$hxOz4KYo~ij?IVY)>pp$y-Oqu<-+k)U4>&?w{|{C*=XUotp8D{JKmGK}t2gCMa7_$}q -doup`NhRNf$v{__sz4}ShA$Fk?wzVK_ktoxF`jV*84&iuMo|5;< -Fd%Ns4e`{@7FE27e25cFjt?O}F#-5?~yt|zFaIag--^Ow|mTkGLXFk%-ZOcDnzwGO=WPE-)GfF>3%YZ -rh%W`bzvG`N*lb9SN6P8k3EZ8^3 -b~PgO6loN9~s-)48m94`XGnOGd40<~0tU*+$0SGMVgQ-?Flrl^sV`+m0>MTr`GPCGNX&gSl#O{U!&|BQUN^1RF@Tij~451WV$3@j$IS&lqzEZK`ahnJ -IK)L6*MIlPJz?wd3L58K8H`TRdr?O7M{Kz_SNw5}Y1K2Q5KF5Bsr+qJQA509o3mBfq>NfQ^o@-?jak@X+=DrsSQ?XK-HjmFd2-i{D{<^ -#ubFVwjmL^b;*NN1oUn6@8L)%-F&n|*#BW*30&?wmB2OMmc3|zAb4KNO28I_;1m;-D$>NU=IAl&S8;y -Vk=%d>q0LH!+4rO%QYyrU8nvLky*_wm%*mHto!OOM-U#^_bwq!?o4l^j#iJRC2_FH*j?_hFVr~~Rh6XoP@Rp4h#S`&l^Wfs{=-jk -l%Lw7NGQvOO7n&l)c`7xz3aFN9<%H?(;q(W`&CM9SGf44uCTwy2Oi#`ydiK9KT}89477`NCQdO*9EF` -LSBHYpa#AS?I_@5+2c9Y^M^n+!bxv9#Nl}ZtCfe~e~mSbGgexOF4YjQx#VX2z)1%40#*ay#O~Am${Sr --1F;E$i4rTA55f^S&KP!;^gv@W!xi>b3AKqfmGjOiFalHJ2(wT^2gX>+g6m0Hh_4j@4>ZMy0I8VVMXb -?iAV?A%@Zb)5ZXC~f$l5ps76ppO^A0dSPvYd`A3zScMlile=o1o%!FcOQT-)?&O#BcO57-eKi9=-P06 -ZWwW{S_i05*U)uU-U+i5eI>FE?;{K$}9-Fw2vu2SUVYvd~yV)v^*}*p>W^*btZymzz=%YYq7nfG8?O}xrdPQz~|Qo*n|ozxcd5+SNb2JMT<#l$$b -%>SaW+KPt-Q)DUzGJuTP)YCo0PQ*3waFM@3#{Ae2H8uic28wzAg;d?&BQ4zZ;*mXPDvNjO -*l3nlb*ogVCKU3A7ulYe06d@O&?cc#Ut_;Le68wo+-jp0g! -?W%hMwJwTN(7HDQz2W58fF?rNNv5_CJ}{HSWeg7r6Gz%H>>TjXF=s-#NPo)Bc9TcuN|0Jt)(z7&elI3 -Ni)cf3+SypycI5!hz*piQfpP&MaugwZq=$`+Vl@@O>+*@jWfV9q -%iGT?k-Y^6}>{5v0#V$?$AE2lK8Ogzle#02|2FfGXju6^KZu=ECDj+rA}uq)V3vd@NbFd(^QIRS?)XB)d -9FCx1_9mqnlWFUGcLPLit`_srED*<)o!HpBl5-W$2z3Jj;H3^q5t4G*uCp;$g6153$cVTQUmPIl*h}z-UnNlGKO>!+CCBc^qn&c4D78vFZ^MUll81cl0E2wUek8h+mmy({K33&L71p_fVLPsu@JQMXNkNYZ5;yW9mQ1u -iWK|WI(Biw+KuF%$+|xva`QrtUs*7?O(wcinG_KNS%oJ9ol`cjEBtUe)?lzO3jY>>J<|>DoCaxDSQXy -oTe1vspQ1v~4tecOMT4D`d)(Nhy#EbJZb0#Y$$K4OYC~UQ(A{BX9173EK%bg?FgPx89C_g)Hj4Z{bC0 -+nNdWQ7DKo2A6xN&bX_eJ+d+ysuh7D@bARdB$ox|mH9p$AH$Ff?gKJY}2;XIdZpaIo5qSR|1@5xW*_d -IzVs;B*XVgJ`3xY-&s}6CiD@gn+Z^o{}IuKw%5CX=XJuN8ndSU~6#lqQx9tRt_!z0SI^y&V`f?Yu9Ss -w0Yj(A)=@$FYeNmd?)0v^*j68rpAi_5mVYO-C`*n4zUY`C~=)Q0GrIoD`J2)Jn2|vr`gvNcb5|3Z-vY -whE1B+Ne^+2g59?%%%T1*hhmgFumf5I1H5AG_!f)T7^~zPaA=6sh4|5TIiyk*6a%bC>OG)=MjSSzn*f --3^gIq?x0$IS*zN;WWXIJJo(j4|K1a@eN`gp%hv_+VP8Dph(5bDN>vaqPdmXwS89lZ%NU<9kSV4`iP~ -AoXzV>Efjk2rXo)UrsJmg8p9#0 -N}t_O;<$vuJE@y)ic>Jk|{b@$c1Stzs_I)AA#i{M#S;){wi}&gV^NxX6mF#_?l8H=~S{s`BV%6%2L}T -7ZNYxHxlVgZ+CPXm9mO>BX+1+qqoN9c3cXmQZxo0PbK(Op^PAlZ;{7kBV|#WW<%r!e^>JqOqcO%lU+2 -WH%0|uIg}1n%TZ&=Vi;4{^Xe+$Y-sQWC~2_JrkFZq7N`Q{i3CcsMG6k5PsfUY0K#Dog@=GUBo7xkYtJ -Jti=N-G*L@Q?5k9(LHa30C -26aQ?*Y`BZ%EVypFD&r$E5~J0;-L5O8wdyLt~>m92?I0F)s6iO3Gk&nWorK)NsNGi0`*0XB9#(LLTeO ->luqL${W0wyS|cw^X4iTODd$hPr`aTjEhg0(Y`_!!$@w@Y#4HrrJ*8c85qT4W& -*4)mmIJ!`6SDox~KSst#3=8lqBFq^Uo(WD0}!DcrNo(>15RC908Mu82fs-Pbx4&YLPFv&{S(m)m>XsN -0?n*uSv5+F{LF@)Fxmn~Kz_*>FjF#nY05=JT21ni7JiU7bPp!*(oI;o5r5V2=5A%oN5&=eHo%hpCoXp -ClukOM$*P;JvN#0upZ;>D>$!);EuEJd5pCJ0TJ!K$q?L(H@uhF|7-;@<~2Js@KQ$_}R+5ay1~(Iu}9! -|s{&S_KEP(ME)5iZZ>)!vRh_e`UZO;e=$qFZ!eklrU$hhH@GX&_IZbhVe95NhSG8T%%aBDN?gqOld_s -C%X<>lE*b5grjH^7`8V9f+7FSW)>*(L+%{zi3nAG!!b0+8h4CG -SgPyY4wW&{T=CcT~Q|*Pk;t-@QtESXvf)6fW_Cr-ewxy6~uhx2%`x;Jf)p -}OfcJRX@nqfix5RO$T?$o(tXQ-4fI-~wV&NkKFiPQK8p>Kn6R9V7eLS{WyJr5W*HotW_jdiv1vFe^cNRRRG@tUEpiq@4l5lD -xL~ky;B!hs@O*VBla;196C>rHO8hWgYr=>;9k=QzF|qxb1*}Yj;Cve6HB(b@YlK8X0yfo(A>%+t=@zk -@!G^-YYJsVfr?7JgjCmBk5HknBZNi|IMfIqHf}PzAG(j#m3MQP#AV5HRD!Olne_k}nz~XwUESEYAphf -YxH6c{_ILW;OMJsjCfDcrJWPVY=RfXPkZG4 -6SExjkeawwLO%>rQjo-%n?=S1V*>cH9DT?I!qlWHMiSH})30^|^4DD{%3ER+F|jodlAbI?Lm8_j1on8 -`F}jvpYdmh|KmZPI*n2r7@2$<}J~5z5KXWoJFbDm)HtN6@Y|g%dWj1KC7K09Mr* ->NaUk-L)f;E7ei0%a`ej^PmWrv$brTv8@kCfW?!8R#9be?jHSCIidv2(e^R1+}yxTsOE=F`=aVm^_X| -*CO2k9_~n2mQE|6SPrd7RRm9IHdCPL!Aa>IC`dN#RKbB!I!)52L3p|t-GXc(o(rLH)M}duGQ}W!$Ly< -BGlZrl$tAlQ(1(zTg(S*|)6@?kdgQp9k~{}Tv<$_w;{jC%W4CyL{We_>k*}+5Tack6q;B%V)m{Z0ntV -B)$>)?a4#@y#WW}m#jCO30my%`d{0-$bfNJjp?2C4T$`%iX>3~s37s~QvzVNqZZe&JJi0O8@*==VIVo -)Mcw@r&Kt4faB_sQfTiHp*hsDVU|?nEO%rTcEBp{hZS+hRS-pmf?ewhCW%OX1szrK{CdN+gByX0Mh?V -Qs643HpdYNhTP2;_6~H$a|e=AAYEbOsnNE4N_!ySa*ZSS4y$ff*;_}*nvvvtRW|^^t%n~*HkbPq)xd9 --5T%_mG)y%;|Cm+m@NWh81{FN5ADuu?O4l~^T>JMRV^itl00w -OP05EKdpjuHl<$AhZ6c?JEkKM$9HInr=sxPaaXtPnZYK>$A$*N%M=z>(U@`xWT7OL4W6!hfEo2_AR4;p`})jJCs>Q1LUQ3tMfSo^c0zuPAu;~*)Z+BVG -IQ33{rx7vnc`BYYjqNW%~FIzQ?R>*4%-Eys>5^4&8xSMJoR{8DMq%S0I9@Zu8{}rETaj5PJF@*LQ*oz -Hx#!^F!Jvp1kNTdSF?SWG?F6gEdA?iLWKJ2yrr*4Ds44S`2Q}(i{xl_-kEJK)RG!Hkjt3r*UASzT3Wl ->&Y5jJt1R&Nz*bc5MUI@^Mv7_b~0K?tp5w+GG+$zl@}sm!9vts19#f?-dFX+?u*Gs~h=4Kj4_0!b&rK -rwr#cG5I)2w{~-ShQKuV4JB8KGca^!(t^rb@UhR+CZuiBgn_QO1L5nV!2`mB2qtiRd)T!|`c8-+4ZJN%J;UUe;sW`2qyMFEmNMCu)f%MLdJ8n?giuP2dKmSB;^Q$`0( -ha%sGHlb)(ru?2G`&Od$*YvTR~^MD0WbpI9Lm(ZM;&9X^XW6hgLOYC9}PNB#k)(rj5v;kffA6&@`p(cL9nRUiAvB)d?tB{Hqm?|WX8P*FeE+m@&OcytgomhQ|~R!HZu;r -Dk>rJ$PcTEYPT7vP_fRcceOY>s!mfKZruv2AYPNvIHQq88*9}x~>swf;lgX5|=tT4q~&$60uL)LzRRq)v?*OS&(HE>M)HXtA`zhEYzl4ldZ*8H#1hKbb((rs@((isoCW=i~q&?3qwdK!O^(&7QLYM@ZY%TCBL5lnYI=JK_`GkD^jw%@@rBwwiZZ#YXp2kn%swy!o{iqIT_S8^)d0U>N;1&azB=75RsHjp2R<;=#`h}BZR{V* -CDd#&)G;&@;-LuF&NwupGzOdRaywO`D(KgB>~5c+L4r^M80AuO}J!s@u&lmV?!uin2?_+2?&81Y*-Ldz~=Vy34x>5CpSkiE47}Ar#KrGFYg+BD-Z*fO8R@p-5h`(PDUJoBdnn -i9|m$4rSmv%fe;IzSu*iR%^#NC98VcrKt`DQW7SbojzW^<8#dxbe2<^vWGhMY-Ob@1r~+YOdl) -;WkcOeA?$WH$swkI6}I?Mo9Rg1P5*WeO$z8^TII!L88Mq#q(y@TCaB|fVq|UN)uGu-{s^NsGuh%tXhL -ACCNT8bHmVvIwQ9dccG8hzvnqTCuH0zUZno}u>!r9mkanPHJu(zDd3R)JjBJQPc~CTU3IK1 -4R@;dyIdGuMOCTg4*b29*l3W0q_a5s0mXh8-5nxmuc=E$jWW&OG1C=fE8gU?LXD3qw{I%>9E$aFx{}7~wFiY|HAQdkEnY-Kv$UJq -Ez3YkAW&@|fi=b3N>7_6get2Alu}hj$ByUH7xA$|eTYYXiNDQb@ -6_jqs}mYgODz6#MEeB+R#?id1;mMI#q4_5ynQ<5ZW2@2=j2g3x*mn&{YIK4qPq>|QG*?%TsjhRqIVhC --aOvps$0471#YC8)G}ab|%^m2=(KeUf#Rnhx-hmB-jliY1f0U;!3QdWeNRi4rH -BipRyJIJFr{vnVF`A|W4Uf9 -k?pW4wy(VXyGMNaheh#DghYGCo^_bh`OniLSQb1u&;c9L)a2tn=e(%vNP-Vh*2aa*c@3$5B+Yw21w# -gx%z8b}GFc1)t}^XjsO4K#W8&kg~y(Q2U^j@LNqf|B2=T?(tps5fJHrVdwC)ND*-hF&*@h0Gn$Q~b(- -!yDG~YYqrQR?att8mR4RI}z!i(uhj<>8&8Im=Jxfo^QL26BQv74FZ6&0su`O{Mrz*s<=btt{h&gS73v -TmCYL`st8!A9pf~uR4%L+TN^2cc5_LuXJfj>TbZW>s^A3#Jimy+z5~nUyUl)TUy$Cen7Y})vDx=0{!) -UWlL2kGZu9;x=j~PdMU+LNo4S^F9e^(zi?|h05rXP0w)zbRQ|}=XdiU1ip2JiH3%@HsahXo()mum2Lg -JyYi#D}k$k-$xyS=L{uX>RTC_Dr}T06RV5H%2Zq~aFM=kaAB`t-J!rFdlod-|x+n;Gt2E>m~3QPf5jx -4}MfSh*6p3g>Aq!+yc!i)`E5$CfR<9eg;s%?lh(6HVV-Rc(y`g#R_#bP@tA5=l*Fr-4-6uel)pS-zu) -W<_+&W<2vJyWB8UDrxr9p((|e>-T}|pCA0=?JNIBT?E0Y`x>yUfd_(fsXKpOMdwYp -8uh&&XmicuRYE!T%FUFW;F2*rC5~%P{YTCRSwtAZ3$HF>o*|w -*0vpvRkPFjtT_X~x|YDwDa%kZvemr6-!;Xq=fH^k>Gud@>nc^4Vyd);8SWttKGa2;$Hi87+zoSTRTpji}KACGi~Vy!y%fxz<#~`j7 -Zy^%x|;%l~mS99ToVX7H14Lr?vn=V+d!LQga8?TDG<1nShAK;c~487W==QmCeolCRHU`BSEh}c8QyjK -2q`=1m~tD==RhrBS()wj_ZS9%y9n@bc06>65UZQ9fwW?N$%8dE7NG1^?N;-6Xl(YqVhrJKdj<{o;#o7-0Qv{ze)6v1cpmetajGrYCst<1x3 -@;Kxp-T*UI!oDBaEuGp`2Ci>PXalp{} -7D)w28quL;?z|9S7I|Z@xHovpUFY(}%{g#&D{RgUMhn=)GLISlzNq%1Kr_N(9yio#%IeV7!zK!mvjaa -u)hS|X7%_2wn;P&d_kqZ39M;90WA5cpJ1QY-O00;p4c~(c!Jc4cm4Z* -nhVXkl_>WppoRVlp!^GH`NlVr6nJaCwzfU2hsY5Pj!YjKT|%=-!a3QroD~T{e)S@+B5ji{q;TOGfhc#FHbNt=lIN-VVcd$AL{lV24flylHNG&QFk~=zR?@C%|?^v8>WesYpI2z#NA0VcwvHN&tS$NKARqq) -8d`j5BwXN~g##1Y~rp~8NA3<@kJfc`BnuS_NGj?pBINx`Y811cGRo>W^yZ(#_(MZ@IG?i*?Wl8S{3O# -v4)7o#0KJ*W3lw4V1>)=vhnaP>LKSS6Z)uy6MXtJD3x2J~@o`lZ-F&6mJR{;aPBlGsduy+c#kh2M5E7GBU&+4j*CjQ!GK!&KA3-%&*p<#bL6Ux8oRu@s@}pYZ?7Uo3h3yv2R=2=1c)Vt~-^>r@6aWAK2mt$eR#Ocr)3TWo000g)001KZ003}la4%nWWo~ -3|axY_OVRB?;bT4RSVsd47aB^>AWpXZXdA(cjbKAJl{_ej5wLe&%N~)b}+H0=qRdMV@bGGcUon|t5Ga -iV9B-RwEBSNmyigIA4);8Tah8+`mc2EPCe2r-Q29)i -9i*|o{<&Rj@IRgAOFX^Ki!#YGtD?f_(!^@IVFJ69bzwklwE^Wtsd}eJ>c#Q#OOWVXK2)PLRX4QOsvBc -X@n~XU{4~$XBAHeYr)Q5|F>9B)cu?0#Ixmu?8mJYxQf02LH{~MFoVZX>noJ8_Y)DtL!Wd=qS-A#dKdD -V#sYqw4FmYmqJmfBw&f-7i1@dNDllr#Xs!1)KJCN&LfjE82JjrZ*~uH449T*r|d>( -pV)KlU+df_yq(Z=o-$XrbboP%&N2x3RtT?j_=-IeYjJj$tU%3baOMB+kl{ASGQ+Fc$_g_#iYzGCD$s -Rf#foWCgJirNv(+f3Fft6FpbqhKN^@hGKi5_X+*?o^P6m#=GmO(#g;~)KdEGtI?5me2SfsOu6ehUE_VZ*0(&uTO7k%|6w1>YS9)&91GG<5u$3B@m-ZwwH=(ic=?hRT;%g4YOQDp4949 -nz7Cc;8Z7v4TJiJGTZ{07S$>nlSBT(#!!zlK+!_IhtPj0I4_V2ZNpk3eiU4wCl9d+jiW~zy?Xr&4x6K -Jb2|C7FSlEcMO6hERLnWWmBJqtRuQV5HgpyWo#&5@N1&N~5c@C2-mP3}H-fE4p%`xyka> -qiiEmd01tf`r?B^V&dZegjkCY@27W;JgaxhN-9FpTgeYEvCOfUOwz8^O&;VZ>o8L8-aqMr=fZd7$d`K -ro0C+vijx*{yK~?VBY9{z6s4`xU~95Q{(Ii~77?hW2P3t-$6Me-;@W -Z{^i+I8>&L4%I%L9kWyIB#6F3>7p{h+?{1}0%D?LsQpp&tmy8`x3rU{>sZ+UA;3Bs;Yy>YboXl7kNI2Mo$SweQjaC?h -egzl1`U7m0I8m3yD%jP{<3gb>>=xl9C&^JukTRSX1Ca@DgjyBV -{)5IYuHQlt(-Jq&CFe(t1d1y&PNS0X0bMZ09)5QPh^IT5$lB33*hUkz_ly1p>SIoSa5N!vlZc$ir5|fnuNO%x8c7`ISI`&reI2-GRTWUSam^hG)h1A7 -g7PQ<_r@7&QiOCbAi32n}r3z1d7q5?OhhYdQ1-2kGH0-PTaRx2nVK&rC58&wX#um!K#^r2uw^|q=z9r -BJq(pG?%9QG9EP$0S`BVE#y2PI^y+LyS~LX6L5)N+tD0XApkBNXwAL_C6!Kh;cWR@8w<_4KSJ3vE)Sh -#M3EG<*A|)#o^D==@WjQSaN&#vEsaKvkOxa3N})@c*8>?rBb-=*HH$ONzRc&1ZlHUdW@41q$S+Xbr}Y)I74R}kgflQSJJeWR&JE> -2FV25Zo4R!VZE^<1f)bYxtnWkZ=)pLXGB#`3c~!_M>U2mBmlVt3Hdd5uhS|?9c*^caGN}#raWcM -F7<)k8dgY#CGxE@mA32R(y7@ -gx)p*{-O+{kRK3~aQ--GqV^SzBCZQ-yC}V^HTf-(>&F54r9A7@i$OLkmGlZLn1#<6wAxP}Msd+RG8#l -&x{?+%0wMxsap^O_Z9q?$)NNSY^G&L5gZi%kbwcb$t0~!EP9}ka@$*o}UnaBb&dRy@;Z5=Y+k&*q|PpWQ(%%t|j&jJ9&Y)u0t)eix+SP_My<_&i -qfwV6UwXn`s3A7+QyEuTXe2?1%2isLhsm%$u!<~|)F7orDhPQ{Gx{b#;Z+!@v%&9yLi;{4wxBDl5d!+ -*-d=>B)wTrgwUW}&t35(F2klhwC)H4CO^?76yC-6m_nnU?ql^1rM(66aI{s=jKb&0MT!v4l!P3jKOL- -WFOY5g5SUUMIk%zNzsed{<4whb@$+I8AmHy$MgN1juBlMa4_+_{@c>b3?r;$q_j3rty*D^iU4O(klB- -RZEL_@dTxqL7=g3+f2^_^=+AIXs#f`TTbHc#n4uG8El??}>xiLQ^h98c~=s?+zQoBOxp+q==^l!SiyZ -4tyn{};Ri7BVu;f{=Zl>C3CPXBQ#ANTMgg^1sHnA0W{Cvo|+qAMYolOL`r;G_Sh2dN)4ZUDk>Pp%yP9+_DtxUbC>>9)mU{Kv;#FZDyWdO=QiFM -BfoH`f%FZ>8foGN=rcDWnYrV;XZKZun569oFgl1&}5_!TFHj~sNxdcfHNb<@$klGKnFRw{Clz^9SSX= -Tp4V>j-7f_B%CWBofDANIY;^0z1P0IiQj+`tLib2=;ObtLicO5QGQ>)c&#zS@ZtQEueEWI^N5vR%SI% -{rdL%2O`>;voEbGSLHTDDc+qb2XsqV-m`f5vrMo4?^hA?1dx#EQ)wH&Jk=lspxY0X{R$x_(IL4eKJ&` -WrdFv3BbxSaf~X+QS+BON*^6&z@vWTu>SXb!T;)c#x~rJkMhan8A6Nm}40|ta(62W9)#|VQQj9UYhby -_2_N`z4MWuy$EDh8fdF5@)co%S?MqI>0rV3p+7cD<(9<_U8IZi9(|7X!xSIo9^AE<7w)p7p%v){{Wl` --!N=WJbSS)!c^h)GW&bj0Ty%xL_+v5E8zF4YWNi&~@fVDp9R8=FS49z)e%P`HuBQ{7@f -t?nCr*SO`9vph{@c1d0=3XT>gdN6S)(aDh-Nl4I_7#z6>mJ=sqpS&gUUuftSnDbqG7?AkURN`XaZS4% -&Qk}%)9j%+1+8x@YTF|jDGW6h>w!Vsiy`lQ!NoWGWYoR(`{3aQ918cLDLP3;R5NF!GquPY9TA -R{$nJ^gOI1{)@Mq9=jKFgyYzF`dj?4o?q=a}>M0d*7GD6`4@J5Wao!2h4*fg^53s0&{~kd(n&4lMV0Q -B(<{LxEfrRHXPOtQ9H#4xD$ewh$*S)Rms2N&|-S^iwVe-~{hX=^AI6tH2Qga+6#(^FSJswLT~C)o_`_ -amro2ct2eR(71^sxm8%{o`%b)10b#Rz}Zj|-Oh-8yU+$yKSDEAnJ;ze?wV!{7uYn^zWsGDOOU_Q+SL>t~uo|%ZiQr){}U}^9Sob(MdN$_4#si?Mesx%}mOQrLVWt>({NXxlnBGP@r -D5>XZB6XluNpX_R(YW02*L3_{-&kwfN;yOOL5+=_l&mFzgOVbB*eU|nWZbaqek?R$QgXX&O_m%U~q87 -ZpRZ~WV~AP}A2zvfP${i?PXwat9{>_OhZL$QBXmb0H5qHGb~?Kl4L#4&PI2B#U6`JnE-XrS$&uN(|B2 -i=W#%O}YBw8X4-uJJs!It3wq)Xk(&=Ah`I{C*z1cttEQcV9jaPF{r(_vJHG`%2zDN$neb>>ppNlg_>9 -2^;R)4aIj$@S;nC?Hc;!^D}hR?~$<8e(wlqQfs;#aY9^@Cvq{Uow3XDdcBTTmtF6OKR9}d)83Qb!248 ->LPdP|=n3_D)&3x=eae28hm?pcXoO=p1czd|0xN)+|-!g4W#dBOuaoII#8E>`tA|^D|e~8Q?GswcD -a#b=V(<1h*B}#2YDu=%HEd|Bu(vuy2mtZHLXqYWS9@0H+NX6YNG#eRG%2XzUrznyrE-i*?GX)FhpgE=lC7`~utG|X>B!@aIS(r^y%Xs*P9IcoSb=$b3-Y&{Bi_T@*E3UrmypRJbNeZ=;?CU_#p_#5Tz{Nl(RN-KHz)ON;{ySeGc@HfPCW&X -(j9ESffh5syI)^(-IJq0aqX}#O+pF^@z%l$20{})h80|XQR000O8`*~JV!W -ubY6)*q*v19-M9smFUaA|NaUv_0~WN&gWV`yP=WMy%g}QzRfPV8Ja-M -MVBxV;7)g%0sU#X-NtL?~BkPTUKLQ-JI^Gy_`=!@jF>uFJKL@tA|wI%bQtMnI|JooQSMXKKb?k$`{Z7 -@Zy{D^@}f`fAi{j`Qqzuzx*HNKl$w^7oU{ZKVO$`A8!6~b9Z%HzPx{XU2Y!BpYH$t=Jxg~KmYQ}`@5S -rS5Nm3<<0HQ_1#l>lfU`=;$znrcQ;Qr+4J|;f4#oFe|P;*?tdyzzcOb2{+sKk$2V8+uK%xLZ|?6N^W( -$2`v-lu*Kf;@A2i}WUh3+BvEF^iOa1&*e)Hw0#nzfR#IOGR^YZNWw!GxGkLBg{EY(b_xYH=dHnRh<~ODM>FVKM%Xc@oe|fn1rF>TM4xh@?efjRg)6e&J=kw=O-QN88a -P{zkS^MeX`no*c|Mc{?ys_Vv5BKlOo2$F>aQ*h?aSxvp@l>wv-u~15L-zmW{_V|AANcD1-P?Q|?)vHa -;g`pcbNGjEzAs;2Kjxj5KV08kKjhPV_x{J*n>X3{>zg;%caPWQ>ao1zyT_mZK2u+DBd^XIDPP^^@S5G -xakurugUf@oMLYmnGNwH&;*Gf3D^`I)0ir{h{3EYCL@G`T4I-!QY?sZMnJAus`SA=N~ -y>KIq>PR^`X*^8WGqr}wv)IYR!n{L72i|NQOuugkM<{#^d$*~^#DzIpxU-{o&}rt@EO{q{&Vzr0HbzA -gDw4_9|jAM(a?fImHd`Q<-nk7r-J`1-}`KXdnAy?Fi2^H;CRSKq!Y&&qetUcP?u<@aAdds)8w{^fVyz -Iy(7DX*@tKTa`|`D>H>f2R1S9P*d@oXNM>Pggg$kNeU8oXh)|H+cKD{CxG->s;nH*Efl%a+O=;-G~3L -HN3sQ`-`48yZn7Ze^+jPDtGr!m*sB{H;J7@-#@Hwj`H{Qy-d)*`TVjp%O73c{p)Qm!K>{4)y+>i^jEj -{_Yarli~GkXe)^|p+4pd><1>rHO8Nd(e)FIGSDt_K`~OFy^2tRG{OXFu%BTA2;r^F$^&<=E$Db14|NE -)@0+4cVJ|;%q+`doyaGTGU7T|qOa&C{Dz+c|I2W(H*Z+^bJzrFv<2hI5(uijnVU9%()*Z=nZ`tD8Q{p -!ct#O&3(jg6e6AMf*FUw!t+7q2wd^Y6d>?2q4VbM-+c<1x4L#^?P*?z_*qU-MbYXP^D|a(q6`s(es-G4jdx3hniUw*2AZSAYS{_rl%?XO+`GN1Fm0M-}3e|d3{llH^wKY#b -)%V%Hz@cS39UcY?t#rLmYeEZE0FQ4WAa>)4nV7k8i`RakWPAvZ0w+}L2tV6lnBRu=!>*qhbdhx%WXO9 -|v?tycgzWm|!w?BOK^7;S#;oFzLfBrK6`8OA()Z;P@uF>?%V$-u}G&BFfSG#ock$=s0Sz_8{@Li3gUd -GYqW$j154D0POnq?2HxAlwsV3#?VU;4X+sfG6b!p?^_9K7iK6CI@X(kU+Y4T2vdX8J?MSk#0JMv1|WT*U& -X3Um-7Id0y5&{Ud6qZt`A^DbwG^GF=ONdTQ=2nwl?gSCk!$HKyZiL~hdAVdb4{LWc8zQ1+^_CPzhZS4kCxZUh6P>pT=ZP@T+GO#$D -&6rXoBmYF&f|F`n<>w!8etr(R7+YGim6_G@8*fF;X?I8QqL-MmM9IjXiFyIj+&&=TQvk6!nn|U(^&T?1{#I@KBHnr!vbyl;KMKS_c91eHeLL`5kx3&NEH-(m*xm3o~01&cS4)IdDu47SOj{_c<>D`;g`^tpXdeltyE++_~FJ&R`CK?Ud8V -b;uvfr8K^){DnPbT;_{$)0bReh4X6-Zu$!&ocJxVG;UzDxtT%}To9Oc?yWf8a{r`v>TSjHT*HVfmzL4p&Q!-+i9|KCm!Gc`5}8cHq^19o;Ea_ps -6&ChMCqR%LNX`%?*~jgc;#3F;ayqXxy3W=D2BDqQY|G=L@Es&ClpU=mD@38hP1xzB>pC8b@0Wmg0 -cH*XUU*r?zUor!>@yI-AmbmHGHy+Xf=RAlmVTr=@R05Hnp6|H*qN!rn;3{%l=8JuSgUZ{{zhckoIS&BjAbUXma98qer>M#o<`PLbQn20WO|h5n -*D9?|iLjz@GnqT>4^4@r*FAZ0HF>9nT0g%Z3Z$5n0P@Iu=kL=@#exCNIekbjLknw6 -mc*?$OgdI)o+BY}_xbC7&zgO+4WdP|5s@!7)!#nsyCVA -KFn_tsIgY!~1&MIBW0{Z~PPo>^NYEVFMJ|@Mt(-*UI34S^Fkt>zID;^uX~tj@SEnzwi=_Yc}-60lOXz -2kbat#{pxfvY{t#)N!MZ;Q*uA&|}hL?saSM5@>OQKxRWvoR>q_u(}O;nGHQPt2hlPbt4#P(9p&bG}rv -D%}W}*BrrLF$q6P8650qJ8Z5cNF&nEU?jjufRO+r0Y(ChVAQgq2N;RI*R3ZnX_&}t=m}N=k``bjz(|0R03!iL0*nM0f%7-me -8bsiLyxi7tvA*UtPt-NU9ML}a=FW9{xttmlN^Y32sX2`7(sZH4hB<}_mt)o5)s>;b$H2QtF6%2+{_eN -&n#p;{yjN~Qxfu2CJ0?7&_E0C-}l8Cku*fk -M7W;Gjn0L2OvD^RRJv7Y{7BrnMi^aPR>NLC<8sFw{r2_7m?tU$2>#R?Q*)7j7iC|00Y+xGJ#c}aetCy -=Z_vI5BpBrA}tn5_yFD^RRJu>!>k6f01yK#}0I)swFecu5746-Yv%vY{uCtU$72wklApn5~N0s&MTJM -k*MoV5EYPI?r{RtQ%OVU-4L!j~1tS%VR4`J(NChKM`qscm10xNLG%(UmzCM$eo(yf4XiY<(!ff?Y&9^_z(@ll4U9A}(!fXqBgh=t&;yJ#Fw)NWV!}%rSZQE|AS -xSrf{_MBVDs6~6O1%4(!fXqBMpp*I>_1g<6Jj*Ne3$(taPx_u_qmjpo=x=a6{*6NI*@34r)qMd`@`Q6 -px8zL0uSkp_jl*8uX*dI?6D1Fw((D2O}Mf5cy@pwZKRRBOQ!%Fw((DkG*aSUeduz2P++{bnHn7BOQ!% -Fw((D2P0VhZ0G?-IvD9-q=ON(ER72<>0qUUl@3-q_N0T64o0B4+0YY=bTHDvNCzVbeA&0qRTk$%P(3trN}N(Uh1pjnKRiJvS -oXCQ{kKNCzVwjPx@eTJVw%RytVeU4ZWps6&Crqc|XNkb1XGQh~#abZRbxo+rf+0YZL4D87OBL -j>KFfzc%03!p83@|dl$N(b)j0`X`wvLE+p5ua*0agZB8Q7BnMg|xeU}S)i0Y(NGK|RdPNDnYFz{mh2q -t-nxykvlt0agZB8Q2r_lH9@c1S12C3@|dl$N(b)j0`X`z{mh2qaS-*c*y`O1FQ_NGO#CvnhlvF8+w9~ -0Y(NG8DM09kpV^q7#Uz>jK0TBUXmZ^304MJ8Q7BnMg|xeU}S(1)a-_k(qKakBGe#4X@%$kM#ehV4PJr -(l?^??%D|osFfzc%03#EOOfWLR$OI#3MAlLbZy8M2`#7+GLsfsqA978q -GzWPym4!V)Qq -G2+U}S-j1x6MaSzu&=kp)H;7+GLs&2!!0B@3)9u(H6)!k#QJvcSj!BMXcyFtWhN0wd&|8Y!oS>e~>&l -9rt7mb@fC7~eQe&{Ud6(`g3Hq@ibGq!YDeLl2{e(ZlGmd5L8>p5q!ljh;qNr>E1?>FM-zdOAIwYdJlf -9!`&-N6=&Q63cKr#|?T0J%gS>&!A_}Gw50AS?O81R;5R!N2N!lN2SN+C6?iMj$7&3=-KGm=-KGm=-KG -m=-KGmxK^V_r$?tpr$?v9<|S#)jvTksv(vNFv(vNFbI^0pbI^0pb8xLek3o+?k3o+?kIhRg!|@z<(sR -;t(sR;t(sR;t(sR;t(sOaGMUO>~MUO>~MUQo^+hW}oJr_N}3NmCz=jv!$9XYEbXLYh@9gVY-MeAhII+ -}Dxr0itTx}Cfva8F>n4SJDz+ -xN*YIqP(dxB#O47YeuM|ag&!6c&Zaj8l=b2%8W=Zv@1!e8397U)0q?}A~R!_3L$JEk~E&dQ4ut5@)E= -3I4Wwv@fgx;L0~iXQNbz|xVFQnP@3{CbKF32b7ja2sd=n0I8vCM`EG0Ead2KnGO|FvjF_gAz$(Pd)^r~v2r)EH7+m0DPAu$LQAb#F979F))4 -0h?0t4dg0YhOn9E77(cJg1rR;%cPjznx|p@uyTrSynP3u(zhBv&xG8aGs@02FczCjvLLoWM>M0>;997 -!J*_n1=N*T$>RpblXTKcPFWQ>)?4g&w0Q(H|6{o7e&#L{T+xIxmP24c8p-a;|ei{V+LK0U0$s#m%8=e -#M`_G1fb54jt*|@af6OJxHRH^}Rr4o#4fwE`<=l)`_#^T;%oi1l)Ax-`?ZIOGC>m5v?l;7kZiwZce -+Cn7VK>#EvyJb<4;`n`YIOa66QP(e72Aj=joGBIxwG^JIBPdO -Wz-pnR77Yq6Q4VF&oR;FvTU*UTc^GRg|+BbiQNm-%#j0}SuIL+Adom2%n(W1{W^l6d@Sy&RJ(CWC}zqhcyIT%#o^V-!eXi7TZ1RISPsM1u8Opfr#o<2`ScZ98tFLG&SPBaOQ^i@rkhbs81{i!*f9f -9$^(9RO1PgPY`^}M{ZL=-RH;%fHxG_?zPU6$i!7E9ZLQ2H&96> -%Hj$Nma-ko)0R&gzbTw+l@46eePHWOA1!J{BO6eR63Wsu1HAUHd9C`Q(=AWCUmcnP7jAuJhnLDt2JcD -=@AezEmU8y9+4<<3=bA#0aBL;h-`aBP2(adO&qCL0l=hU|$-_DBTdp1F1+l(naZ!nJNFm4@VMawKT12 -HW+v>-%y&Ds_X{2O>~3B_|(iNF~0x%At+Rf*w$TT=ocgaEfi@lnI_3`MRK_Y24%`Mu6#L|DtcMsaaj@ -25pJ_2e^&(&D7XGLu4bbHMKcaBo*3I=eX3DsD=SO%0N;w)n5}{g61jZ0yL3%aE7YF9T(5h-Xn>zHz4D*KphJNuZdP+$uR>7q`NouP0PG=|ccA%o~s@4q#hG#^GGg2*;?6MQ@mXMuhMdq$kNPa>bK$WNBA*$3Ww%HH)C -Xp{S2bPV;6#%RF?0|eKHWI94;6jg+Z&>#^M_E3_<9pMy3RzWi94=27UNJa(k=Urjn -&UaHOec{@(@(rqXQqi0ouvjNrvyqw){HCe03$Pn0(D2P^xalgR8hc#oUWr8mqwNR-#x;$ckrR13N{?0 -5?pB-z1%0IJoxEOGMHK|LAjx_3bAFST`1yMbjUdJ|6G9p>b3x}fWT)T?4j~J)&>|{JIi({}6#SL@an5 -h@l9&oqh$s=mO~DQX!>QLW(NV=zNR3Xy#Nc?w4lOe7$bXIi=U|U24KPzSvRM`qae9bkN^k6H{wlH6~?&9OFUF -w#p5czp!&mI5s#*k;4HQhus|)|2RPx~jjBQ?6T*Qb5^)saQ=^nsJktP;3L7m{3ig$Tj8U6jBu1PXi}D6bw){mS_pn2E$@f{6*@KIp6rD?lm_5R2 -+#F=?Yn>d*ZJm|5mrv#fQlFXyQQuO0+>!ihL=RM;bSINkv35JgHGB;qPI}FK^GOx}e#Vkj`3vqG -gQv>CwW)(Zj1-38FgMFGm^h+I69Xj_!|IRI?ojvYI8jr%F@hk)7VKGe6kCFnv=Lo71go-Pociw0T87H -prgSSz1KC843oj7~gti6AA7dK5D-2e#?!u%q84#yv3iMS-r-C=hA&0oDIMBu!b*}|D`I7u4NiL+C>12 -b>UnmTg#e#?sMo_U(kmV~Dz9<+GJlp9vcik&bPckx4a9jq#MLMu-hoW3IDf)UD3Rxf#n0`MLfFbWvpK -89(i{Lk?)5Q`IUBRK9TxL)iQ>a)dT?8E#30dq?`Q%cldNU@}p*m%5uD|y&b+698a2e9rmAFVuY*$RG4 -efhwMk?4)cu#F7d?&VYLv$q{0-PC_x>uJ|L3P(MYgSMFs~}AWk`H);95Jvi)&j;lO+*d0iaVwR-pbGH+e}Y{3DbOmlJ1yk`^=)H%P= -hjnp6gM6!$uq>1yEs`ur=C0?EPo4mxRMJlkeIP|1Tq_Wa9*d7)A6wK>n0a?EwtBG-8_Ms_?{i@`#z2B -&Nb;^(mKQ*YYlS22QeXlrcX!a2SWIgb7@M=c~awgspYmJO=(JeBr^b$oCOU_6x3o3~}rbxGYQKer0D@3(Ee1d4cw% -2mc7oYK}r#6oztEtX34fR1jBQ_yw612TnYK6V`7)V-Rkew_)W&c;nFStpbTz|d)3rQjGJN=H~SwEQZ) -WHFj9BTBUS4t1|it$LwO*HmUeR-Q@@o{uS;Qq4re6c9`5V}cBI))nJx(%7}@9Jdn(XiHScEe5aZV3PggdKAKA=0hjDIA&o>ht-0UAToKdV+GSoO3%*oI(4rl6s*Jhs6-&Sj`QTFxC -O!SLKI}!tb&&ch1HCkyrfX5;&UfmbP__ug=mFGtNXwOwqOZjsKbZEIl#VT&9x;2;*B%vUVRB --A(cW0`ry!0Sr+Iew?##%)7VfRNp^Z^$p&mhXDK0RC$FaNwYcQiJ*zy6=#q&sa!1&QsiQ4K)=fGs244 -p#NQ5}$?1i}7{IrwQy*eU5Af5-Q{GzB>r=pf(*3g6!K{TDuqL@F#JMr^Nr?<4K?Anu8Q}-G=btbFObS -Q^aaLFw?8L0&&$7Nezhmz#f2D`?FI4-da*3PI1(YVP=yeVK(Ac)bPim@eXBI}s|0Hdl(#B}6M!C67kA -%z39aD`Lv^C4b>4kHmlj*=j`P$^n)v4({&h?-va(h{6ju@T$>en>Lx*eC4HbxU5-IUZP`o@M6`Ae`t5 -K`CNTifBqNQJfR1zVQ}JJ;Tc2A(-cpKz6mFvDGk&A)HM&BaO}+>L&{C!jW2@f2O7Mj#VUYGhD -asVr=2j`LGqg85PTh#Q*HGO*hSXe9=91hZ4usv&89L^i6j_y9Kmj%FhN+SRg!HZXoN+95uOuU}Q(75* -x@9MhYk@dXK4Iig%%MYKL^?w$MLBQiI7F=YK9BCyy_&K}%4KqX$1tw0kOiuYW`*NHj%7$$ShCS&DikA -#Q}$+)jkfJCb+0Ct0j)4sPU_Gtz$BPj=|*@}$FsTkvIR% -0cNToI@fi`3jr*6Bd9M^R_%2zf;jpELG-Kcvl%F4(@Y3=b*rhNC_CR%o!9HqQP5ilmi&O|>v8TAqctF -)aTa_U|Sb!8@h%wJB0S(li&^I0Rrd#O`(u@3_m~$no5LFe -uRSWRI_nnj*XxIlQ4!%iqxO6k0l;^T=Ww1rlteRp|hyqhw3mVTCu*zg+LAn){eIE)Xo*05PBe{R%I-Q5>iqk@Div -$T?zTAd>Z6aaTX7(XB~Eq3ojvs66iL_wm3>wM(KS{_jFa)Xu5TbF;u~#tR|S2@DAC~(V;faanVbbvar -mh?-Co}3^AmRzi1Dli&fj{MrtCIkjJtvCBL+zF-?kXKOvw~9DsjQ1|DjpjJotmI7p$Y#Z^y5K?{|8Xm -xsbgX%!o!t6X{>M0P>xSSJqF-!tB%$Ds#rBhLn}ipu1*pHU3iqPjBTDro@xZo_KjoKQVNpsw&Yg@y` -%q788%{fk%nfA@Ac^e9Gx3mdqN{dcfaoJiiQH=0bHdWk4LE)~Ek@IHa_jd$7I}RYY&$xRk$1hT;-2>E -J?wkX}Qk*2-Hzgibga&((3j$|MhA9+N#SP<9U&6@^52g1T(Umf}@KoN>;Sm2S-%DLb4T->*29$nZ0-) --hwBL6BHm)*2 -1R0^ASGg$BDxz0Xn}%K;zIjwo_iFSs7+5IcQ5CCRYM?qe=7Dpb`J`+s;>RLA400W98y`~sk_6k_<5Ks -kH1QA#t`dOw3;UqRuPMQet`XAhu@qCs>Gc98B4tSg68*->OQ?H|xfuljSLC@QP(ylHBSI%@3a4DcA@T -+O_0ak%AAo6~wA;tIkEwet%D-{$)|N<*B6hF}-b1e-qLiUfb!JsZ@|BmtU+GRbm0+f{!)e#4dvzrbDN -Jw`%PLF9>ARvTVa*%wLNJvUa#A3ai#im&VihDbD9>AWoE55j-74z9NT{m1Ym@5j9@6_n^i-t8wx-dZ) -vG|33cW(|weEM%19h)8q*xA>Q7@p2)s^l^{upK65uBYYL`S8rJY0q5cf?o6u7^D0-0x!oCKqmc8`aQ? -LtGfQizEqC8^3@PlTK -fDV3WqsZotA;mH$kEn1Gzhd)1^##dVeF3U((RRQD?3Thv}Mq;wmq*+Q@Ekm}3HCvSQ&PuG0$NW1(=!M -~JMlCtN1qwdu>7UEL@rm{E65_TC}Z2c~%G0vowI3@+ZfBBurPvY^Qte$e+iNd1xRR`vIB -&TsOPn49`)wb-+AZSpUZcN4ip3oPMKMLlXVU>|zpRF$39E2*RjZ=9*_)g|GH#eeJpD0succ`pL!3m=DHiayE28DtC3wg*u~$N)q4&8YOiaIWqnf@mzEIukl8l -4dLOpZ^Q4|xAEdUD2yGx*^B$GoJ!ZbPP3eHVsyGrP;oqr3}y;i*gu*nvH3<>H}Q3bmJ4y0mE+bMhR!uKaTz5Id -Y>0TvZK*E`i`T+l%>c>Ww7FcO=gIXlC-5vac;(TJxoydYNV@U8zu$t$dxKofuL8_fmE4N&7ee{ohsBD -ezZ;GJjsLZyk1cETBg=iQYMufdSe;6)YytCz@DNXqPZ&L=#;!LC)xAm^Q3Z)uoX*J -TB{-DO7rpLCaU`e6;!uH}=4U;AL^i)=4p^OO=a@O#tl?pD$%4cMr8|HhrN%d^ -pdGkS`j^Nbt$baun~JQd+Nn_Gbf*epF+NR;A|I_Y -24cbhK>3!XxJ7S(-1~Tmmx$V{hD|l_dfN-J55a&RTDLB?snAC1CBzN)fw&Q>z}5S%+Ri(1L;@X#NL-; -miNC6nL6y_Ea(zT?Is~CSdgxVgN|PYQP>Y}!HK{N|VjJU%mu&ZhB*(8*HaoTmtl~1%ADQp2C4*Q1*rx -OZyO&BH8&Vridq&+W(JYUq`W=yAm074TSJAePNJ#fo6(R;{%b*0wRGG5+edX -%=OkVQuk#R5Wp$fVz?qE!r(L<|*J?5-|B=@Uv1IY)&NRK1aEFuQ_!rKnIPmA<30pdd;aTp3T*MnD8&tEkLPSCyPHA2=SBLtrU_2(@<2IZT3hv$S=No4mwzh&*J?Rp}w0u2OsX -JfT2LALWDtPVN$y1v^#bDKcY#v(Bcf?iH+t-YvLQ-$R~*SA-Fgk&m@qY9uzVgp{^PRT@FZwN#G|JT~8 -{)V-SKo)Xfa_OI&5mkKXP+`EpnDTAu=T32RP<06uplD)*oR3k-fouE?pYLw&{F;!P}n6lT1#zkjuT-u -kk{6olxwM8TY6}o_vUo^mV0C@##Y%uxbIIE~v%*6r>9hxUY1r%Mh*V0C|9K -n+?YwJtmplrm*@?@yWn8Ru}UI^ovJpZi#AG|K?qd!DC#UU)S3!qwqk(lvg;ozb+4hc74PdYd10j`b#e -|Zv?>%qhj!hn@SqxP9j_3t*De!9_I{3=yu>A1xT=>F3rnwtw3CgFCQ~+KyQJ_E3Yn_pFz!nSQT4`Y)_ -mvRDs``}V4VbLRehiM4i$%?5KHGDROx(dtykyj^u}w^AFVm_C6=|$ap5JL^Kn8=-v=XxN*V+J@WUDn$ -zFKBojSo$l8g+~IU^+_b&hJ+#aF%W6-!?T;H%2}RXa;XlX0fU@jgBjlBz5Z@t#C~r~+5)UpvXRoVV?c --uFt$Na$#k6zQojMpFEa(@8ukJuBPQRnf$&!KGj+D3azB+N*n9>RwH?hssf6a%|HHo?etowd#7Sr%F> -{cBTqOggBSfNVH-AD$e)WaONcA7960`)xP5=)j~-QfZ3_Q)3uBwSd`^)O&Y=fuAi6iuZ7<@)B9ej$uh -)JR|PDAYlsbqqAESh`5}YFc9UG#bwS9+&P^(aQ$T*s1N9~N)IiJO^^Uj&xf?|{R36E1>OeP_rhv@Etv -HezD*NdwpOcy%=eT{A^2zPhQ#eV=$O9KQH000080Q-4XQwDh@YEBaX0J}^80384T0B~t=F -JE?LZe(wAFJow7a%5$6FKTdOZghAqaCy~SZByJxlK#%Gs2zVexD%{OFMIQ{v#|^Yn;9Djz%l1=I66eP -n%20HSV?V$`}JFIPi2;*#`euj#Eshx-BP_|Wj^_2R!ZX8v*~~0hvOHgXX4fA(ec^UvH0QD`O!~eOWS8 -V&&2hr6z6SqTh+x{95stlRGoO;{BN~h7wY@n-KMVQMc=eyzOKr;7jxC-U#DC*&*$aGa^2jQt!S -2_pBPiMKP&rgUfh>#*s7^J^|ifkTJuE>EH0YX9-p^m(U%Kx^JqpKPgS&ftouhb)v6b}M|(nk^Uc?4h{ -@&$;&8ndm%42yF3YZLKb8x-lB1^X+v;Ycmbu&QeXEWL@w#X~h>L1{+g5jCUnmKC(Kq7av0pW{Tfb7(y -1HqL_EBqX*_Nf~nq~i>1p7`rHXAW7YSEU9sBQ=6*D}Fw`etG -`pS{$Ce6+a(dULKxZzx__NRhm|3mHq5U)!n^<;6kWXwME@OD#6tNua7T}UaB64Kb*cgy?(2uKRLZVJH -EOSC+C;qP+S~dUY{Pld3AUxF5X;ToL?Ou2ys=GP_b6#l#;!QmukqnMrm?U_C>Ys?9$&V^L9$W^+K$Qk -EJs6ysQ+WM4?>d{_%@!xNhoOvtHGO6#AX0mZEO@NIbNa!j3}U$hvA2vTvk7KR<{>E>)wV{;*aixKiCu -s-+tGWZg7vBz|bRUN?PxsQSi@&02v?uc+SK-6;JiY0F~167y1lq%H4T<)gLo)v7nXW%@lTm+H&grhVM`Df(qlx(>&Ck^Tl; -%jrj$}?*x|e-fscg?o3;J91@LToxxv#_us%6jJ+pZ%w8nwE~jmDraXbrkUyWHyn-!k`G8vR>4#ZJfir -yVsh?Mb6{#N52?QT@|v^#A?K3QE|-=iF&+tU1zY=3Y6|bnbMK8~o-Bh0 -idJdKDH~2J-2)UARs_h}oD|0LxyI$7ct-YjQ2JDcSF+v_E)Y~I^@dl&PH>3{SX<$%tCY-|pK=_F#79t -rtUg8!Nd)Op?Q5GK7&@ppJNMj&nHOe`3=y_N#_jb}~=FaC%{}3?s=McjX&r&B&I+}qqPUbLbBa;M1Qw -C^Ia~7C43*HE+(hVvHh-gEP?1Lq^OLok~B)@B-4`7Q-`h8;R~Tuy7q}1Kl4YPGJVcW?+HI)LfTa)j={)F -=`H;_Ny}-JvU90(0{D5T*EAx#rGD<%&%`5B>f}jnnukL$G$(cGOe+HigBZETvYs-JI4b~gv4;oIK7d0 -@T%Ke>Nq4sC -=mh$vZtC!;j*T^K!c0+7>%8cmr_aa4WVhSAgy8#}WBJ@EN7N?E%kLS)W75kWxmbkZeJ6&NG&?Z7QWm> -#2Co1{^WNy$7OsRpzG0tbr0sv(b3%1qmmq1R(8Vr@cz@ERrE;f>6RyII0M$G#;{1Z`#h8~SsP4pR-pd -dbeoh~g4LD2w&dBZ6}ou$g&XkrvSlx*Vq0-0LY&gd08L2-qMGK7rV>A&-(@@|`Az2VS5>GCA^5JE3$( -r4tY~;Q9safc9xe!LtD5qbU!eJk}@Pd->Er6{XEdgsles&K$z6T@aIqhNQ=L7?~)0Lbee5qbVgNB&hT -g^N2$%kVX`5$geYpamw*2Y=Ebw$G@e0EHN;5CwXbeK4UcoQd_^uKW|+(0BQeMb6%Nr+F8VZ}^;w9y;V -6P5{dr$dYa#?TPZL%U5xemD@)6yi_5tDUzzGGV4J5_-0par+e -jaEq9|)RxEpsBG;z90^PW;LdN?VtZF_U0aIIjr;A>rpy>0oI3+nAfVKQr+G4(;? -bP7K$@2id6X!EYurG(}hQK=iz>!ZRkKB)+TV -*(MCjxfa_NxM9MbDcoEQ3%SI5_{qzXwEreh@M;S8a{ji0@)#%C%;fiO^lg_IDKi}(AAYB4NQmtz@lz& -`nT7}FIFIroV`zE~0V>?eK!t!T8FnD4&)Nd -H8l3*EJP|g?3>%;tgyoyAzWK1W}CgoIQNMuChy*43<*-`R$zCdJExS~f6O|D3uGjJxtDpo}W2SQRBw8n7=gEBZ4K_ZUz -jVK2jdP93QL4PJFrwBlPh}^NC4oMnXpQ5GDhX=-DqKA7X$_vA56#POM_B4q%G{20)Zz$lqejd}~h@3C -1;bSL{%jwuOPE_-Vo{+nT*FAZvLAxHwmNMK#Qy`J-v-JRM4GB<*@5z^#4Fx5d=WIxuj3h!q7D(oi3Hp -%iGlFTDijZE(EkRYGEqtgQUt$cW7^ZtvHI1FAQ-3arXj_s;RG*$IjpMe=@r6t(bkGhf<#*|XdnTT2P| -V?XJ=$sLINi&G+c$6-9AP26ON2HgQ(Fsg{C -t@4ynXny!JA^fR(k#}V_=P}sw50=75P*qmNw2htBQ@7# -07bYX{!ge!ZtueFpto{uXfAbjFdUV;RVqI$xwiX;(TaO4>P(GU_wXq)bF#&tqc4Vc35?O3CBU{v2hNB -pivu4U2~O1C*XirfDA-XGVd5H9)c?P4~ad_tuuOERkDd_;)^129ad83MP?(2FK>*%ouLYPk{oc;bd|h;%__4Sz>ar`{b8d%qv3~5TZnwGlrJ -VPB-|h(Y@9fR%+4;%r=%pvV`eHq6%eCrs*3{+T -v^p{Cio5%DIeoq^%Y_4+yoeYa=+Ax1H8Hp;AR7uiQ2EA{1832P2_G(=UKry%{ak56drUHo4Br#&%11)ING3auY;^Vplg=bN -_G4|m^emP`F4W~ZM!HP2>Jvu3FuZB+^_=1Ss-jrLFqZ5aK4>W8{{5G(b*(@zZRew{w7?WO0b^^=-*lF -zhk{7JL!OD%(b7~LLGa^AP)$Ew+MMqDQ7>C!%6yIdEydXdVcTDt3YQ%3sfUgc2wp=kXevj#>{FO((f` -FdlXm0lGc8|$VlYqiS77D$~G>#ijDEY -f1{Emq_041^v~r?|tv^%u=2F$F_ONleSSpCHATFmcCOo5i~88dyXeoqV3uaf6+bmjJ|iLhqNumV~`LO -7?djHWJB-n)b=0t$S4p`&qT91;?ZQbwdGA)79XCFm?Fu$@bUV0lR9|+&i;Boy})gzFu#c#6>rVTy4l> -WW{aXPhC$ZS-ZnK&r88`3g;vGj%W9z=a5yskm4aV3j@up-J8CK_*7J@2dl!OwD>m%KL(_ifEOnSa3&A -~ayD2BrrYUj<)@X?Rv1Q`-d*ZLdx%^;bU~>ifgfP^R_2@7@jZLsGBV}cmmtOsNdNs^Rx8$mBq$jh(jb -gLii7_K1_uhf;NGxMDppm8G_^dAOH2lB1dU<+sJyQdHWtaF*MV4yp{?W_8dprp?-tdd`W&du6KQn0#t -4xC8vQku||K165F+KZ#xNxp0^tNC5*2}7OKBZRdc{qO01=x*${aT8@f6p7MZ}G3+oa~uD@e==f98aA; -IU0}}3jxWH$alFYy7$-rjnPjIKWp^umOQ%nCQZJ20@zG2$WoVf6zRjwD$N3yz@3LhSU)Og=mjbic6lw;CK>z~JOj()uOatHmhpnHV7W8m&^67? -s?pM&aht;QY=eX_M@`e#L_Qd8#PX*D*6XZrc@Jd~de&z*k*P)h>@6aWAK2mt$eR#V|nhIlfW^40?6Dw;s4A<^$Y@8e2y}q_^bz|@M_cl(hFR!ef+SfQUx@&J -wAqxw4ZLXhgY^-ctzjW_#ZFzn3`jdALHkXH^>$gs*|F<<9ZEUWe7;ZLJPH(JljvB|~4pxUZo*NAh#y> -tj935U?9h#>}%YCk~Aw=5k#dehB^8v7gP7aH-uZrtVhM&s(np5Y!101U26qwUVVMspjz^Xy>R{Gb)k52BwjI5hB63 -^v_FPqW@z27ZRMUNrEttaaPK&l$MG`u;g<%{qCW!A%3dz&hz0_!sO-*ag1GI_VntCDyuU;FlT9TK|%D -!bb2F)|y@5uNcfO@Kpnc>?2=et=Uk$&Z=Y|`3AcZ*2$X;W*_+$>x7NTuNlm)`!@_`SMpm1v)Or@-GuA -{-(fK8`#shP8`$>^%=-9%p*_04qYn+-H|R%fSepj^nDr4D_!BnT2K|)v!QjtW9~}dK&RUy`{(_-h)A& -o)M`+-$7|gEvYX-Ls{0$pagMP~<-JsvGE?6zUH}E2%;|m5g13i9#O_o6)WF4D(Z2S -Q;d-uMv)o7oya%3yZYk1^O(KK?PgKy#yxA7|CDk9>lS9)q7`otUv6|CDuNp1U#QxvuHt^2LPF?!Qj|k -D}xAIc(fD&tysR#w@4E8*nl5Mr$ABjlPSKH(D1XZ?rB(-e{d7Z@?+?2Am>qz&^+uea{DZ1D+&rpegbO -oFZ?)ljIFFMc#l@_+7H{cX`15S}Q;7RfZI!WF@Q{)YJlDt7aljIF_lDvTyBX7Vd@&;UtyaA` -k8*qxe0sA0tz@^ArvF`Z{>Ds9~uu(Uj>tfU$=2JIXr>GmPQ`8MOMcwEmMcrs!jJg3Aqi*zFjJnaf7Kq;51WMBV6OlDflO>IOPV-9S^+4Mm!wZnQ3@y8)Nd-9D+?iSAbPdOpLhJ5b%2% -oKG4E~dI=)Q#3Ys9UDG0hgj~z@=1o<*D1Ey3smC-MEqzb)$75>INryP~G{K2|lol>PF$ER5v;)p}J8^ -A=RBv-T9V@MRlW#VyatanK)71t$ganm87T}aEiK7WjX35%Y@UYJL>jnm{?S|%rHSGQ>1Rdr3@39>P9D -}sN3RpTd3QKVS>JkQ8%u;5OwFf-5#l1q`J{ZG3vIs-4^Q3ce~MelDdHwa=ZPhZYR`@k1R#q=%f^NyK% -cIr{sL;ow{)gPLaCN$rM#LIw?in=){NDZJ}CCn -d-I}Cg{3^>ULn5>{9A>qPo%Ai|TF@bu$Ls0d@O~DJ`m7=5@KPscvJKFix -9QOzA{*SI6s?scuc%H}*iTty!^E<}1ngDHF -accI{gh6qThY|{3`@D&O=&Sq0MFhq0X+Md(urXrQ{5Koc4C+~i77pL-7X9hz-6dAUv*okyDElBzUl_` -%=2zandCbF1PM%}WQvhvg|Th~2}hB}Ycxc#LPi!X%%%fqFPO5M9su#BSM^oP4hv=#)+lgfH%6OAg>t -XPBUMbx#g7ivt-q)G=js3=>OCDWh(SVS?6E+z9VebvuYDWrj)qy6)+F-58HzhrZ^k?tJR@7E@YOx5Y4 -#QMW~PqxBSHO2A&I+bj-bBrq%Lw#1Y&!(;~3jn=b2!^BBU>C-ToeoQ%auX}gyrgT8v`G!e8bz8h{uzz -K}Zc9vQQQbbNJ2WvRd)jxf!US_%%|-N*b++U -Hp8al0T=(n^6BpDi+LAL@UbkpncfRTtZOM@sCKlB#qweZ>-7>?(4RxECl0A{LI`mbhx}8wBOmzdE{R_ -@4)QtlW@s=DJb& -CuW8Fd3Lp}KK(^`WngofsxQW6F+M*Ug@>ioKiC33boF>vm$8Sg0Ft{i=nelj;~I(|m< -yE)LFhM6?RJU1S!X75?s(a3;8?9^VPT*Zj-P2dyE~s0!!X)1?0Xo~ahKbL`ftGd -Mmc@aV6(%z3_PIC^um|b}IyLGBI`da8hzyf_)s42(qHe&IrEb8pUmVyqUN?IVydOVhQQfi?CNk=_sBS -0J?ZoT0P`5>OTVhHJbzjM<+d|zKsX{AEM8{8As2hD0iYaBP+n-@#p>By`0<_S&ZW(o>lPi+CJLb?=cE -is~bvvPMnPK9Sx}B(QnPFm~ZrPTcnz~DE$&smU5p@GDMcw%u;q$lTSg1SyGhbP&TgKdgeOTR2m>X~{y0`z?H&bF6NTIh115PnF;6e*b02g|zu;mC7z$xYiT~}k -v`|W!e}qZ?K6s#ojxe#DJ_R_%+<;Te4LHTzfK$v3IK|w6eK0rrF2~$vGbOu+cX0ZYMRfb5?iq;gn!0x -%b&C!Tv{1K2bc@b>wX6-y7u~Ycr!1mdbox|2bz6=w$@fbn)D4;{b#Ne-qxg6syoI`Dr%&MmeNeaAOUZ -859f)q3T~af5m6;opHrLFZZ8?fhE~M@&LUiX-x9spK7cW|vzUa;mDZPpA{KKb!78BiIMjtO)Klg-5!?)dR$*JKdO5Meu*rlbaLXWlUL+~#N6yAaks -ui#@v?eIMrcp%XS=}%x&2O?~}Q&i1j7)qV6g(cjf&OCoea1{gPUA&kc23_T%J-lrGkn$c -~=M4=H7v;4Rc`c|(q5X`sk2$$!m)h`PabrCurQGo+MR-Cq3?qq^C=d-qE$o8V_a-MEsPx|1uJJL(oK4 -V0}f$=?Jov$|cZFWH6rah&)i7V5UBZYS@hbh4`34RxDU-E4wqWth~eyDW8E&M&E{+lygR@uh*H^Gih3 -oxdT+f$FwUw?%c&!0NV8H#$jCH(Gny1#e!nz-Lmfx@T~JNlo3g>YfsHi;kX}eoARsU}90-4mRZEd)@i -eEqWWgjJll|CcAYnrLnr%l-27?D$m?_{A%V-I&rff$6|H6NGUDny?PYg7OUIaR!V=O+mcdRM7J!Z^eM -W1GPlv(Y;x-TIF)DaT5}g=?y2v`nWyz7(>^q?^6N{gvmYlg`*GMb)S|nl?zy3EQAjCUU*aO96xk(c?7 -=R{KdIY6NICuUORBR8-Xgl4Eb6A0Qu6VxnY(80d1vmbi0)l^e2L^83m!8{ndr7Kw?%YU-sq--y7|b~% -w040yfgRI51w+NxvRq561yaS4}9fUmw-kqYjo2Vcs_0~!fXsDYFORc(V>YjJ%&d(@;7RxB{ffYS -7P_!@*Xd%^&>)82{Zc9cfJ2TM9I~HUcDJ{pBSX8%H>NbW6tG`y=HFaN^)NN7S=(*7H64~)3E({aP9(V -`GmpE8nl5cg(mY4XXZnXAs+^bjWrsGR^p|$F+sr$;L?uxGrl&NmfN!|IXyCNrb%T@-;PU@C<-Lm6eLB -?K=dku)XSw%H<*VKJwQny8QyD&^FF{KN`#Nu`PQ{5871cc;4bptM@x&iwzOk}DXt;UMHew-du8FshqfOdV6!)IB%U4PM(-ivw}dvtAtNU`LLN#eq) -Nbz2q(T6W}E4ln_|7Nc&*VH{X)D5(d>IPayb%S=xsBWOsQ{4_02ZDM^7$#_3S=B -A0Zl8up+fX;_zNYS)y61+vEvh?zAH2md5$&e5P`Bv7SBvVFojoOrDf0~zd|-(KUq$P>WvW|t_LN0+%T -|~;F-)8+4h-n5Zr)FQ;%iOab3@%0)$L*6f`Ys2Nn$-4)egOj7-=2l~L_h8gmURiFet&bYRvnyMp#>!e_v^iK -=TUk5R7!A*k^zX-I9vp0Lt_)5MZ#Xs_o!MN|a6G|V=T4tkUu|5oac1r0=*(cWvc7h0((U?iOZR(XxVo --7TW{RAzPYS_QC)0fZ=uaOUxEph6>bei&zZ!=J8yo6^ZX29f9bU6_?Y@Ouj~`v?^*a5f&e -DyyAG+nhv0D!uSvvT`1ILzbJGylA*iAPdYwT~F-yRxHk#+61dOd5sJ}8wi9#>XsX5&z*9w@1m5DkYywRciLd$~Rpnj4C(8_Iz$fbr -Fyg^vp#g?G=uhGp^Kf!P0}OgNS#5w(4=1Y)Fzn&voCX;8aI)S210PP-8}xyv>Ky=2)r-*&Q}tr>!&JR -cPOVhE82?tPUW|WB*PB=k4S+2Meh*-qfmZ?SFz{-CT?YOUz(ofBF~A-J{{&#)fLHCgDtUIAtmQR$paK -S73y@dzeSo~8KLyAux(*<(XfHrs(e(g%MK=J{6(#lxZsvBMfw+@9O$OpN?gR|PJ*-bf0Z6RTVj%9`PM -d+abvqpf;=b*48Hk&+A{sJJMqrU{m=jg8h#uY6tZes_4# -Rc_~Z?zkN#r5?2+t@*1YX4m`X?PRBxSYkL?wdihYVTb$`RyM7R8Ctn`RH2!DoeAO*j_&b7+15H{PwND -l;_w>9>Tu{7#Gt^e)(^JDI2YsT+ktaaW#wGZR{|xxSD>#!AF3_#q^TjJ&LafRZNgn{Ud;?m|oK3-vZR -d^tQoc0OMl%$u%Aa78kRaeDrO=REQEJRo@OUE~b|>{5xQ3W|B|71DKkbAo-NP2dMnfUNYeSfNwQjO)` -%E2$0`s{{)cVXm{QTl;3Ip43OVx{{oP+!4g2u2LB2$uBe+_5=gxtSJX{L1BBjJw^}k0^i|r73+g7ru} -olGP&fG$`WkJ<1$C3zq3_V9GT?(`%1;rh?zU#q!QF($74?&*D+H?h4foN%5vu&-W`f>BsJi8nLRSe?_ -gqrIX%ZB1RhT-XGUBQhlSy7DP~CJv^7|Wvswe9ZFvGlTx<`RFsf>K4~AFPzFiUGlZ&p -4>#n!gsPhlJ={lnNZoz?WDd>}svahg%{fBV{Rg)B4?_9CUr@bc2L -FHoteC?UBmT@k5_vL62lstH}l={sHMM{nB}iN&JHr{UBt+691r0KWMR$iGR?cAGF!v#6RfL4^+)fN@I -JA^aE8kcrpG#kA9#kZ&KyGKK(#_do@kv1Nwpb25XwiN$zT;(KMCQL#owA(^O6ms#Y9LQ#n1XT6Hu{pXNiWkAN!g(|l+J5>VxRnh&i)0;;@E^P!bUK$Z7tKC~JMsPaC|hgKv3RomoOQT=mCON*oPGYKP6a&vMA?ggsO -${c(6)cqWxae?@gh@9O(X`MEDc%IO>x^~*v4V2#LU}k?osH -zQD_9CHiZEcL^ON6T0Fgh<2s=|7-g{2!P+0(|Dy+Wvp>eGg9poC8gSN1BQ>JG)Hy+){t%`rN!6RK*%r -@cXFTw5DGyh*644WITFp>b{aHXRzeB-386Wg3`1?Tq&KR -B`bZ|+)SnZ|mREd1pyCwN^(CRIuJm%bP)nh-_Zw;{gjxHBP$gUw@LK}o!qDlHn%LH`GQ7(2IoXzhe^=92{yBCB4of)D}v5nMJ4#6kP3NgsPh{DVN;dPz -fGe7udlpJxi#5*ihkf1S@%t#^yS7g}CbH30BhwS{}bZu$n*c(HIOIY9#|g8ox-e{u2?7_)7%q -KN2yhFB8oE=)FWlHDR%gUm=*w^l?nX_b=r8&WDfQbn~Ic{>Jf@wNtCZ8_$h~%DO&2935U?9dfF&+|`X|R!`7hN(rA5YeRFxZxpaE4vH$d2&yOzOx -)qjh95N0AA|0LJ6hA_8n?JYKCKKmnI8km;v21Q?P2PS#lo`Tv(Mke8=b1fJ*17sxZyLIQW|JAO -nnyVXZL8W+As%X^cf0Uov2o%IH!(b~I-$0;k#Q1k9qpe1TkME -jkk4=*aro5U?&3S98&HuPcRzLf5_>gi!Ba5w3F(7$bCtZKohGMyS;73c2#uu~H-fT(&<@1fWDoFS)==^Z;}>tSAvunkB -%qJT~z~lR1QcpbVwQ?Y&E%63p$rOP>+U?Y&E%6Wr!Tp6R=j=P2qJBx45Z7=--2OJ5V5>+k6xg#0}dlJ -zJ2JqmRC6aF4$H#!T+WKBxZWV0rng=DfOrD?KRlM*%AtVw4fnXE}?A(^a6DVt2zq_dDr*0i04q_ZZSg -=DfOorPqwCLM)jvL+pcWU?lmgk-WNorGkvCY^+2vL+pcWU?k5g=DfOorPqwCY^<3vL>B{WU?k5hGeoP -9foAHCM9yRS(8#Z*{n&aoNU&#ZJhw7tVzk7Y}OR=KTu89q;yU;Yf?HVn>DfB_9ko6+xs(Flae`^tVye -!OxC1Vg=Dg(u%DAPX?>H)n)F(bOxDz5$1PCSq?Ju3Ytn&8CTmivCYv>BO_Rx*l%mOGO*#%qK+2kQ8j{ -JHl$6P2O-jgQvL+>CGFel*m6tVX4U@^5wr!FGrL0N&B$=#fdsRn*Qr4uMl1$dLoq;4MWldVVWU?kDU9 -wq|5-!=Sslz3=$(odG$!1MTweYOzeyfWt3PA2#SzBJeZ)>mWV~4?o%Mc_l1}S2em%Vc5QHZB7#C04}l -|bZw0Z>Z=1QY-O00;p4c~(=T(R=Fm7XSeBgaH600001RX>c!Jc4cm4Z*nhVXkl_>WppoWVQyzeEeI!b* -@Y{%F7-ue -%`*afmFev`>nS|`CP?$ucRid{8p{56~PSp9vh#)CJ$607lSjepFpB -JN`RU@XToKK=qwFJDhzwC~Jb2@uF_}ekZA*UO*2(`U -wr-@++K9(Gg<;oX*kU-6i^4Eui{Z(27=&SbK`s!6odv~k3m9e))pf&8CWfmP!xqDIfkk21wHP+j4TIU -%4c`EU6)!ig$E9e8tC<|GS`J$dqZ4a~cP59;u)_<=VFkvG>$qq+Y&mQ>Jn2K`F#KPN!|>~dau}C4D-I -(~kmRsyIh^Eho%}EdpyjaTu;uWik9Lf%6o(-j>W6W8({Q+!&0+MBhr=N56dVT1lN?4D`8bT$DGt|6;j -lv8#&x8#9JU;`9G>*C-SQFwhao3940Q@WoXugtC>Mv(MUuln=v00fUC*7vwKy-qkhZWK#==Q)7;=ik_ -{(xC4x{fBhasmp3^~PN$SDp(PH`A=io*yeABWLNio=jo9EP0YFys`6A*VPDd3$hp_I?->JdGcQ#m&Xx -0{k#=ndC6k6o-MuJRAnkQXEFl`TQ_CDTBlK$b5b{<}hQI`5eYlD8mn9xu-Y`IiDX!>wJC~t;_Jk7^-d -JhlP3x$SDp(&c$IgF2N6@>k|Ahx}JA_7(M6Nr?dItbbh!-9A=O*pThuGJ`M|hSl}>PPsL%u52KSAau| -K*1=*DH -;2(WABTaRsr)cHnTEsYBUjxT=B9-Fu#lI)epslN5I8)C{4nsG&kxh#5}YsQb6C$0V*!^@ -FCpyHp>;kE3w~IS!-5~41BWr*wq9auq+-?7dWmd5ynv52>*XcD%DLwy6iGI&1Bv+@E_0u*ki3MzVdL` ->xb>#WOU!}8!akiAhhhH~D?hBq;qvklW^lO7yo4Tywdy4lGB&Ow#=>$K0LbTug}g+W94<32Q3!_%@x$ -nQs=Bo@_vx}ZT*y9Mc3#35Kdi-JEkCT2AJ*bEiq_L&L&ySkDg&^~2`)VO-!0{j -grW1TJu@{II}b^j$`Ncsu!FTen7C%*PMoBd6J?^WwTS#(*8d;WG0Qdi4^5AD%;A0&+e-EaW8$;jrL`_ -3DQO4sWNrHGE&qJ1`Qai -s?8O{rx3Bqr7_za?HA9|1hxMMPGu{su!ePkUAunMHhoNp?4hw!5@>J*i#2jY#>B4eYsF%=tjzjRn0*8 -hA;W<3VA#fP-{Ga0x>LrBdIP^FSdG7VYGq0Br@)EcXBhOnaV4qI!ISyFa8P*T$<%j9*bo_=ipTl#=OQ -7`(_vse3UP8|g>zywl*Xa7dMZC0@ -6)k6Z@Y0=s2?t~ZmrDx@Eq!g^*F5e9EZSRp>C}ZKb+0sLi}(xhfDOsrsO5ect4$R?qwl)34z0f_~H0D -4t9%eHx3Jacn%yE@)81v%dD3WI1Jg?^Ck2+TtvMD4dz*&gHP4A2x%-1=LFj94^xj>*a^_{BXQa$JV>~9ELpq^M?fv -LoVa}bcN(4^f+8*UcwkZ40Su_CCcM4)G6}Akf+EG>v7na^M{?7!)##}!(rDL4(s{hIdE9d4?`}4!;p7 -kUP9n7HlXsoj{|b9dI`w+IE>aMypIENuJ>_3F8qDXkdqvSnyX$y;4oUJI1D+(VaT)d!%kd3%vSJr^TU -?ImcuzXEYwQ~b!%GhMG`oS#?x?E@WV6baCW_f0zNO7|t%2m^Frl$LaLa5_)!6?=cP`EfF8L#+I42!v_0yguQ!y;qVYHqmJuEy;S7Lg&h^jSH?C|za4+|I;(h>rOh11sJsx`J2tR -1!-HkHGAr>zxn>ZRd!*p$;HbnGzH;_WcXB4%iZ1rDQ=TpV^{Kg@Ay{jlY*p&W*6YV~mTHXZtyd-ZS;e -psh^IQ#TrTRogk569KRY<8_5wj4H-$@V4e~iPaKX9M-eLp!Uq -R=?bvJT4@O#JFK@&CvaHrrAQ^(VJAKWp3RKqu;sAj@T3oRdUL*~IM90D97f}O9L|0zQUM&!_QL?(yg6 -)4TEdB+;^0%Tqd9Ez5|+c%hn$xX{4iug)5DOb$x9f)VW|0@;t -pmb5=Eqk3)!VZ>ly9RX<@jC>S6S3svWMyc9@Oo&bGsr!zOdsR!i9G;XlCPGT%)na9Hmtj_h~S)d|CF_ -^cha7&aBdkf%E1C)*B}_%K}|7}m1GC00vh7Y}d$hv|efegM{K9;TzC*7!hKH*7I%DuzwZO3d61>t!Vb -49_?#Az(QB&2(CBST8G4X0e3U5kK@oBt9e-!xqC9!;?PBeIe59-LTdp99nKzr)EufL1=bTBKrlQWxC- -)FsxTAL0JhNyv<5j4x7qhAu9pdzNon}r{ply`5r9+xs2p6-Y$}Jhl^+(;5 -(0;{o@hqb`TQ_iCpnBQJrfUF)APf^YnsdS!+P(fql1U}wO9^Y4lfvoOH2>%1P*7PEFt*e1>`XLF!h9= -dYqSFA8I*lIczG2_3{#MboNCYm9ncPvY+9|<}lPLI1Gf&@I@R#UIMMBIsiVdT4TLi4qFbJ%3+W-U)7r -MLL{`#Rkc<`ei&Vs@FEU94r}exY58HOi0W8TrHY%II8oR9We*RQiKa#lO^BA60t&%4pc_^?UuZm63mGH2MAT0uOYD!(R8Szwd-Ux-jfd -x2f -1{41TVv~t~3bDn+KZDqg#hZ5D6x};j*79cDP!1Dsfyg8JHbfrLpF`vk-3pOMvAm5Er!p2I|}Vb5hEQKQC0tX{9qL@eFO({>QCZoLK*v1q*}6R~1FK1cf@vN_uQ7a$g&qrZ -g6=V%!spQFEm$mi%g5cwSaHN-HYR&<%SK~q(g)#y9zc8F^Gs2VZq0K_n$cBCDI7RJ+VZE1%2O{Ue41{PRkw#yp)i9t|v{+V13Vi;00x~4S}C-+a(^j}F;Yc3MWb0 -~Ash(3Ou#4si=>Xe+1vK7s!OOL!1@R+N`O4cMl -`p-2BbBec2T0|M@83z~-$th{HEC%i9K<&dzDqiD5XT06AFhrYh;ObSJ>Vn^#Q#1?vYTlB66v*s{wC@5 -g#HTYUP6DB^hWkoHWST%P0d@`&D)9Q_yg6cS2LT}oow07mTTE^JzGu&`Yc^aBcVS>dNR@%NKZ`bl^01 -*hWcORzmxO&6zNVvf132{V7s4!EoaZFm%V}$HssW$F8xU{u_8635*{KsjI@ -DF!o#HU`*ia)lKG9g`8vt`PThQiWPYn|(oN9d_v+?bBy*>=`8LViYi%M4PyANC;pt| -$+rqpT>nY33J@3l1gT+w9#mqsvNUycOdwF6rjE9R0cC1xh=k@_*_t-m{({snzIr-2?J8%3zbPm;?J8? -SllmN~VQ_VP+ViA_v0>OKG3_dMQ^TmeK&px@G98qbc9p;JpuR+^iVf<^q^hi7L@dflyUO9z@oD&Wa!gOk)$ws}kg6sQEAvfK!{EGhlG;^9r-ea#o76Bk57+PxsV!#og8wF2c^ -wxM3TU{>?BMSbN^`ptw}YX0k5qNzrHk*}Noo_(eL$-EE6Bwq|C(ek&ggzfqCyKM=_69reV5LAyA!LE- -hOm>B?&m;PM&&B7lF9SUNc(iDry4-LA4T_)Cov-L7&o(Ueo3+pV*`zpEswsM0SKaVI_opY<@QYQEA -nAFlE+_4HPwJ8>|$F7Ve?{-pu+F;c_8n$awb|ASN&83y5TQq?zlG_;fppJc0D1bmHDFZJd6z*1Hc`RM -V3hmKU1D<^yXv#b8?mq)(JLZ9?UM+YnZ>dh)az3z`X{qA{xdH4Oj{>tF~^*!qMcW-Ta>ixIc?Vr8SJs -Wm)cz(PYcGd>#y;1L?- -x;dygXQYpcFS#r-*Gq4<$b=t*6FUF>Ge9R{z&a3EPuDVy6)ddU)|NUbKTSaXgRcy!6U(=!DGSW!L{J$ -!4tug!BfH0!85@xf?o#D2G0f02fqql2wn_c3SJIg30@6e3tkW22;L0d3f>Oh3EmCf3*HYt2tEux3Vsv -(HuyOBB=}wMY4BO_dGJN>W$;z-b@2P~mGO_q4~~B_zB+zr{P6gv<44AijvpWYEL_01EG^w}@_47!^4g -uI-RrP>9d@t7?seF`4!hT3_d4uehu!P2dmTGwujACE7CEexg=mq*N_mJDd90L)Xw~QkN!Qe>(+`risp -ZiRlD?_cpdTcSQ>#foNIIuhi++%_POUcmAQgF+evpcstZqFOIo+Q1ROEDv)>DzwZCX!7PV-SuM&73Rs -3#+D(|pvEk+*3+>dDC4G#~Y34cupN -=Y_d|-?4KZmiYG89pBv< -9N*~njz_)QmF;`OoBiI(k;>7pAFW*LZ+3e(PE>Y>*N?WOva)i0(7#pL?rtAjza8~9`-5ZGZgvKn(eT* -LhWx*sXt+J-Z$yJi_ttiQFsxioI@pR%-Wx`zlE1zj4bS&Cqpg*d5UR?NusADyyBg%jAtNRVsn8|OlQOeY}SYy`p+iYI43g2NLn4LHA_gE -k7pxzfVr`>ZvauMb%3>{N|EWHPJfBi2bXE9c`(W_^6Z8fPGjf55J=tnsJpOvp-qc% -Pji^^9;2@3SeZHCcN$4YdYq&*q`tV(r=3)&kaERr>ss>3vmsB#Y|>}UYyoC7;+mNq6y&q>nJsde9Zt>cz%zSUnC*S`qca=olKAZ6m`&^~ -htHO9-8;ZmOP?K1&1{=&B;RM}XCvX%K3jNZA5LaN^~^4w*|W_xCBwr95LWFdSuF -@8d3Q+tislxH)770K-UXOOeqQ)A<^{aQcIZ0f@^+cO)j=jXE<q9p+*i%RV%0L)Nejd6K0gP&I5r*02p(!#3n -`Y@?LPu#G;(u?=-1Y~ur`+P)51!#3n1w$-5ZM6VoyI?*dfpcb@!-4V7WefAE($NZkCd}s$2&<-r19aw -m68)ygKYcC75ahc6(y`KTvF1)rKwDY~T6|~V|>JMhr<&>nrDn} -DQ1XhWTu(>6fc;Itw8fMnEqe$cMcBJDApcxaC%%tO2E(1v>GQf&v&hPr6bwu(oV5ZV$Pdk47Tp{*v&L -)$~Uq|k=!A4R5}94e|KIhO9vw@g)3$Qji?QUdO1o{LCw4Ff -PMW$UGw878`9nI_;UqBL%J=DGRjioVJIy18B=jYVW -`m<)N)6%tPA_+Ov;Gpf1!~{&3JUTc@4Bxu(b$M)|k=6&R1u3#0g@i4bb+hN6G^2DnXlHBOcmn!sZ2Sz -;7z2jZUV5Hsr}NZ9_a_2W;(vj~Jkh#29`&%+bSM0K-&kXz>U?>pip8e0|k-M2dW -7LoOoH1}QaWL!Q6Uc9M=*F*`pU!NL!X*^o76L!M;u2vm*PkTqsQ)|d@>q7{FTHD*H|XEs!g*^tMX4OL -?{WR2O7HD*JeEFD2>joFYVOGnUJV>aY*W<%AO4SAf|Pzz$Vuw*v1A7^$JjojtJVs&QDnnZ8c%bm}r;BX}f@S -Nu0LbCUz@mTdf^&0d2!7?INLVaN34M+ptRe(4-^uia&mC`gBCi*H;;B1GAkZ+J-&skY|`^8`k?dSvs; -r%q}n;F&OQVC)x&P8=gXzJkd6#BSjnSFrV49v4)?8%kHy<3uYJj6mkwLN1TKsApB(Eh)=X9VfJD^g)} -hR?kObXvNF49pKW4xjiAlWw`GU6lW?Rwk0A{kYYd5YdDdtbXS59)YYd5Y{>BmkR{t#V+cn?25oj?d1$K%E7{tSIiw>?wcc-;9z&KT(Ka}3!y0Y7$B<=NJ2J1wkfyaGS -vJQ1|>x3Cxd7}Ax~ -CT6qG^~_fDwTzLr!Du^)v=8ml5g%#$NLx+93?l7p2Rl0{Jhat>E%bUn$VI&1&j4+-p7+;7qU|)V9m(I -pjLPv-yUA#0&}Osmp{*utp`ks8NZT$Qnfc<8nMc|#(vih<+C^ -?*cae@biL{G`HtE{CqKMh7f6r_+U-Nnl37Rb$vwb=;5wpvkjyQP?=_JuE%VWrrZDAK#q|H0>&>l_Lyd -FaW!=+6}AkY2l5AxFysP5nI2eqVMe_%*Q7WDmoF1D~6-tU*6jzBHRZb(CtEIJg479J1s_+px37V6>g=tbsf; -qg}GKBO+U*&HjOhwwf^i7;-VULwcuOQm4I`k@mctHt8C)v#&p3qwb-tCah%JA)P?mu(QSv+IBl@(8;{ -t%32(>4Uu+PpzUH0y8+sEJ8SILju@PFaeLSchPJEckXby!hSx(|P1qwiZ71;v;OF&42n(Q -`@S5tGx-zG;jNn1{BSuw`65Vi%8~^>U^x4{bGJ^ -MbZ_+GTaxP$zTRkjuG=9qJ67HsslDVmCN#G%oemu$$tMY;O%40uOC9Vaw>Wi+o*FS)8^Z(=KikyWN9G -7q9rsk4I+zAkqNsB3F;Nco3Nn?FCsqk~wWwtB1Cluw`65a){sYR~)npj7Lfi?eZ-7oBf+d$O+H(L1e~ -kR+MM9ny;^7c9_p>CyPhym<=Qz#>FGczf2qQAzY>n`LIUXroA-*L7N}GhxTZ~=Jn -JfA@)iQ0go@Vig)vHHbIc-~Nldd^!Ms4lO#)Jj6MJ}~PF11B2wZ+t4TjWwZ%%ygiOKst)y?E4yTF%Wi -kY~2!532v(n)Ju~y)}>*k=nIpMs4j&5b~)lW=idRuPrR7Epn+XJhhjF+WGs~XYREPYyJ#g8}dSVZL}_ -}*LJWaazUuw$f$jYz6Bwl+CCv!Kx(^4NNl_|)B?PA$*El)uk9is$@kh&%T4WiE2Fl5JFBPm;!zu_-xO -(|kU(8DYD0FPkj#nN^we=euN`F6)^(Wq?MFLlzZ=n3Uv}27-02TC -(|;%jqjI#;PXCZh(W!oKI7qt54ox2SDg3E&zO%h859pcB##Z$B&J!nAu6*;t>5Eq`uZyI(^|NPAUOID -S{nXi~F0DUvasA?@(`PPK)+&3W(UBuluY=10xGctHq2zE}4wYOTm#a#yjmtG9*T?0$k{jc4L&?o?xvA -vVxZF~5o6EQ-iL}U?QgH_qT8$gfwIXqUPT#pnz2M#`v?6hb7Fw0pp^uj-Xu!wo6c}+aUZ}v3i}tvVF% -QQlRAA7<@oEJ|JshuAVA#X)ITaZ9aJ*iDfe**)6>aERz3@PxRxd_B)au3Phg!YxM^&v}jDJ -s9O9DiEto{2Ig>6CZHCCvCm -SM4Yj8K1U}YvN>A)JrIk}(eFd#bF>DL&(R-1CzQI@3s`7JUMNl7pUnY)lBsq3jvVp38=^|?)=`~^v3{SW4`28X(j~anb$X%Si{RxXx^b;NypP`pM -QR%Y)y@CDM6$fLaN0b){|@O%nKeBRN@O{AEy(YZEDH@9(BC6nmI`eA=3hycwFcD9XQay;RM0+9lB`tF -!5}sesgqD3X3bMCAr1^o>Lx_>fGp=z0gKCWLKU63oD|xk!F`R6B&oAbzlXg}a#Cll3HdFOlQL^GSZ|X -oPgm*$zY+`SE~+HIOtQR#;jFm-3dw1es`KB_N2ZyYz=8iu)_GcHQ!g`pHH?~wr1$@!=5jto>p*ikpW? -v0rG6s}4#;vk!voVMJx3M4gk(9INTWkkv`?p2YCJ-`3xNts7cr?LT-|@4WO>gHa0d6W826o&C~<2sK)(`yZ2>-t*LG?SDdYde=iIh!BLdBMBKs@~>YdS)R3!_y3*bv`kb_7r*o+ttJf?- -EO2eY2!%#lhm|I9QS{boZd3fH(iC&p$Hn;&q+=>grWL^ctqOigq3~0*(jswuKPFigSw+hE -6O!dz2H02kQS&;)i)pDtdqCqXZ4Anw3|B}WQ&opX5w4I#7H?Jd5~&h1 -YHBG|55v~MeVHNKk4lkxm>L~*ol&A5rf(sD`YNd@MV0y*sVu*Loy641DfJCfHWQOm7l -OeVf#zE)@F?iLx$zGiI3X<;KCjOR6jk{JR=+IZ?4ndFe9G!_54b1lz -jf;=`-glYn990-i@v3c8wiFtaIKA(%C?e)&iwQhHPD;mnj3Txl!Z0$sk)LCb1`(|e&8m=Xji(iaiieHXjiN6z -nH-0sKEq*Ngpv<-8vp=ekO2TG0001RX>c!Jc4cm4Z*nhVXkl_>WppoWVQyz=b#7;2a%o|1ZEs{{Y%Xwl?V -W9x9LH71-|wfGSb*$c$yL=mPXYn5q)1Anm65C%g2^({j-`oLGt2BqmLklNgL%k-KztlR;y46{K=26(@ -o{j#IK0@|d8KcmPeNBu-P^a@w?kjobk9@In?$$EHGR9PfAjDEt*$-tsb}u&51+Z->p#=&->vQ4AKe=C -H;&Yfefn7K#$cz{zj>l|XY|ssmW^y|yfhr#uI=^qj&I-X_IC!u<2P<~hCAKS@%`)e|Lu22d&9x??y%O -oy*C(+YFEP!cDv8sA9YWMhp%==mj*lC-Hna#8-f$H=LY+|QSWXy{FgaVyV!fVyL&(U|N2DjT7MV*rB2 -kgx;HyfyN&bLw$7cpa^ck0_Qg|M=e93hK6CbB?PTr2MlJl9)`{AKTJ1>f=&7So0D|i2baXT9RZpYO#? -ksFMo*Kr^%FHSjh>04fx)(CNe^+qpCLW8v4?Y{hZeRyPoi<#3#8uw+kTd`ZD8Bak#Ex&y-3=|xm_aBI -JYg*LlfIxCeirYo+Hr!qgP13ar7#QHZl4<$u0i2Yoy=!>U^HGtz-1}Nz`D!Umzoo1K){*I_~y5Y1+W( -4bnC~-5nCerd`rOoWo0`X*@@6lD6?2xfMs-80?XzaR*^GQ3zf8_6{IIQ%v_ZE_~>kZ?T?ze}zt>HEjznv&Mw`30Er^Xbh&2mh!-80`AXY4h6$@f50I_C4tXU8%7Q~tbv0*{1SP*L## -F_=M_6D(HL993s8y3Wh1+ij5tT+${EQl2gV#R`3aUeGNL993sYZk_EPJtLYNr4!`DG)D#1atuD`E+Vc}1)QidYLk%qwCEh?PJQ>k>iCD`E+V1&UbB8N`YOvF1P=*UuE> -aQmAhv9LkVa$qirC*HmRJ)KkJzvvmUzUxB3=ZKm<6$BL9AI2YZk=b9&zBSh*@hwb099pBNkW_h6Aw{f -VgCjI0M8W3cWy_p@=g;T%<>w0pbD`aUDHk0>Nw$D}f?b-XNA(6A}>nTNAt@=0Pk`#L8O{ORR}9Jz^sO -@zQ(5#VBHbYk~*yEIi^lfLNx8WgrHlxjc{9^6o(zcjXuB{H=+_1u?IP%e5v0i(^gT@-+gs*??H0h>Kb^p;!C}RIr6TBi;EQrfoN32*7^VWo6LCku@DnE#QJz -~v)xEPPvupm|}hKLj86bvEygcFz5X-EI27s7gX;D0435d&GN6dqG*_N*@SFw&&#DOP>1=a+si0AJSOBAuh -nkdpE2F@+)3mq~LLu(%pTLL{u187k|T$DAzgIHoslndfgm#;0?I^tzoH8FohJOht-CLo@JB9?(T!z0e -HCNex?ku?$En{@;=vw5L|FV?NHBKG%)%T>fIh$Y23-Xks(#J-A{wGod{Xy)nhy}~n3R*Q$sv<7LBlfl?d=)WoO-Mkz4$Iey1hK4GC$lD+0AhlVMR~KXC= -gd~*8LI?`>&eN9EddwV$FhBvmn+Sh&2mh)|xOJi1}3$`4zEdL9AI2vmUYLK+Lb2kbqdTAYPPJ6VSTae -Jutt7V8KoRvp9=MXU-^#5{L9xzP5$h5aaefdNrHB{Yn#cfgh9V9`iZ}zrGf~7D)`UzE -=I&BQCXA$Ag%E#hG0-p;!>>A{4RaK& -%9cI6sJaMXbC*EGgE3A6osXB-cs7#X8*dhupf-SDaNrideBARs|_ywE!UI6)_KDe?_b~5U;|fKZO+QR -=^|XK`io!i&Vt2V%7@V;eg|Cz;T#&CIXhj0motXZ-)jfhxtF^9IzbLEQ -k3Q>-a?zGK(0(EAqEP%jd90i$AH;?OvG0p@h66Fb)dcTMu -pnjyF<-0WK`e15Gz;Qc)avE{;*9q>Ae?Vscxat((FBC^ZN6rB5Nlo#dG-1jFu|N^CizZkQ`!1T0fS7%;j#b3`8_xXdHGdGBEM~$q!=Inh=24a3E%_32#MQkTt=Bn71aD6vQ$`4B>o=7{cW#V%&|G-XRr-r)}4f!@M9a$RZ9{4p$ -bjKoIjBHUbVWsYRT@;U%?*4GLm%GgKgUZPyXRyddVC32%#-uhkV}5$pVd*w>j*`9Z8X5c3vsQO<-~DG -+A}Vi|}t1aSt4GXyax-+Y1?7fsMOZQwAS$`yyFZ7UAj -&rB?0p2OZ2G5>xu>r9kr5u3RSVty4d?@X+gMJ#*2IYSUm-tT^o2Rvfk;sY6`QB4U9t;VXz)4lk%fYy= -$Ujfu5*n?q(y$Vzq4+K0pVG6$K8tIlEV&0)Q69Om`#{EdluuU+Fg%o`K=U*@PPbr@daHN=60!y5lM2b -p!N4&q7>ucIJlD|G@83ycYV5pjNFf`6;d-kvV?wla4Y&KDQ>o(cw5^cCdKEZa>PvOr9G>@z2`$jW -z6P=6rMfj(F_E$Fnry|y;=ELc#srx)6^LEiN)X%6f^7HWYfK2<E6QimAOTEzaB@Rj)-VrcEPAw2FfK}NCyv1_}IA -m$aZi9Ji{?5cYE8>MI)irRJ2{L9Ch+W%t1Tn9Oy|-P{0uU<}#EJtk -zsrOw4aB@67JyiDAm+DS;}x+IfLO60R?7+EjKyn3Dq;v1p@{MIW->Yzh+W%t1aYZ~xS(2{?{+`S^frg -YBj!Q8q}IgZyv;F-x9T){#AF;Q5WBV&i0!BJeb<(L(S%^%wQA9X>tq=gO%(WAT^&J8uXr|zHA`a6l2{ -8!tXUEpmc*JRv1UoESrYR*{uq|TnkBIjkXW-MHY|w^OJXe`u@;b6b0k(Qi6!0yD~T0LV#ShJU=pkRBx -bz{fh1NOiIsrFzTSjlN$l@U%)%swXZ#XL4B-@sA)F#Hgi|Dj@Z7zLP26dMUfLo_tOc65BJq^OrXq2a^Vy7w+F93;g#|fIJl*Fpi#H&tX-ksn{Tu{Z%R} --@&F27RM)^i8~R$*+IIgNbK5HB(@*vJ9exl_OIBn?gUR_Uro%C*xM&&-|P@<6VGd6e|KU@N -t~gHmz2btxJ^85Uy<0gtw?M?(p@pVNzAV&7HDFY#O(IOzM7c#iHjj|rHMUAEc1zFB%Xy&T*rOl;Xu@T -MdE4Oip2IalQ%o&Ph!@c5WLwTa3>T?;zfP4W3@;O;U&_^^g)JGwLMN8#`a?%%(CeXv -`5a_>(6#^_FG)Eo4la@`KP`%%BwySoG1*`W5yV7L?ghc<=Uv06L&N0|NTL4Pz1yNOTDp7&|^SM5?~Z_ -l34^PTIv-6!{-Ig{?D}U%0xhOF5F6nTQ1zDVYrneMONQ*QDBKqslNoAu;lLBp21P4c0j -875OtJrXp-TsGUu2ZE;cPg}{yfu=39x04S8%`g$j10DsMwldU6J_s~z9;p|+o7PR;Og;oO?ZSzA@)4j -J=CRpKJ^-}BJT@Ci`QHljXu5oV7h+)^o9_JJfuLpca4~osXjy00eFr=Yw8A{t^6aiML;!<{ocEVSv|`U42kx%J|U5T%pr4n_@k+;K2NP=(aL7yX6!8thS|88*u$7jCbghYt1ub&H -7{e#j?FZ-i!m$rQ)^hXRTOSqV>uBfX%8>owX6iH7^?KgOUf{kddNTUZg{ro}$S8XsHIq9gH; -@{Dx!1nDltT?~fVG+dXjpx_C!X}Ifi4}*4W+{L($LHnGSdMRBSI@KOvl+Lw3!6=<b7!&cQ|l;{S?qJ90 -3dk01UM#_fY9-06=WzJqc51l(wU>}&1Qa#KO`pJH>HY||w>iF>!X0@EHp{BM+_(S7EUonWrfE*ecYWh -Xztu+6pUa-IAF!#35XOLY?WZ8L2)(`O#-DAL44zlF_hqRlqMzm0L5X$v!b8{=W7aW~(^u+6juqy6yv5 -Vv}wfxW+pVVh|SFU&VEZc}Xq>64GPB5An`^Z50+cbjVqt_%*I)fT#ili)*)+gw|25=_2>;V4y?)8t<; -98uGq`{aKyY%^`cqbl}oWrl8RxDPt`$PoQyjJ8Yl(O+WNX4-aB;1Ldol^ePZQ{XiW+eF(g(F2?rR&wa -JOZ4z{4BI^0F3$tJ2dwab`Eu|>j79`<*&qBHhNCoH_6J}YXe&UBg6#3Ovl_%Gm;J$yFl@=HThHaYggLv?B4BI5($Lip}Fl>|5F89Mf$FQZYhTOl0;poQ%vcyYbb&G*t@Iyp}l -`aMpz$3hAt$H!=>vM=RJQ6VQn?3mm!!}F!0h{10Yc)(A9Cw_sR>ZX6uT$Y=jZ_T$$REaGJCI-%;Vh1X -4E!!1BHyf-F$S*FUtrj#Xxw!g!)<1)qA{Rz4*v$jRx`Uk(6BR+ssUMZ_$>_EaRg;Q{%Z`|aWruC-okL -0BmC^4>1QH!6F`oCi(#8%;94KXVVfgp|HH3i*yae*ck*Wtj+_%v;uB2WN}W2K^_v)t#134iZ(!KU9r! -K5FSL56?lMG6(nPKaJRyw#0mD&>Kv%qak?etIk;zXmY*k#^)-+c6z|ZRhZfO%qADB#UW7JMlc%T`77s -EC|c;=dXAH#MK;Spy1Jq+7<3YYHhF>1LA)8ieCh6%#Y@Hp<+=C=uBg@a<{4+w(s4=@_$2S0m<@Y`V`d -juYH4sigj^Z}7D{vk%A{9yj$1dD_ZoZPz@wduL&;kPjwrUyUy;~!zv-i2_dd@Gm#yC30 -F|~3BeqIm%4x*7s0uQ%`Z(!ICA3Ot2UdM2h9?YV@!f+&C-50)((J(#GOXHtmG)xZ$WDG%@c&MJ>g_~L?Ft`2#!dB$KO^gnBWDM6KzxOa|^Mh;u5k{l*po?E%m^vquf5tG?=a0g(dvz -nrP`-5e%-M^zleMe8{>|O)v-d|`>q=kkjxG&$y1P$W!+O6v+U|F5cTXODrPtpXyt04H_HlGt29*!Ehsq=|Ao%=8#~>XZrE+$q{lVUFe{|>Wm&dP-zdHWf_|5S*$8U|_9=|h -wcl_h=`{NJBAC5noe0}o9U1U{&y_3LZALHU$s&9aX -`jDtOeYg2$0%FL;1k^5VS~YN%<&L*W4wQPY|Sdd4kOQPZM_!2vQY#4$wDCYIE|%B>9X!w`<&y9KirHxaB+eXf$Y}{A&Kz&bX%QsO9Pi9&8 -6?ge$75Ou!OYt@9@A0?X5Pl}nASot^EQr0R19fi=4~90Xvgp-X5Pl}nASru^EQr0^mj8_n0Xt=BT*6w -58!yvnutAsdu5@nI_0FFnZFcKcX@kmrg!UH%SiP}hb0LLRy9SIM_PZ+d55*~;jF>+N%NEKu -D6%sVW6BUxU02ftApbt@nB(C}8sgT6Ay+nm1F5=~@ki@mXUs54~*_lxxi7O4WuaLwgg+zrUuDm5GByn -vlPlY6|_T{ON#8tgSg(NQD<*ATNW?vzJXr2lQ@TaPftb2t7ern6BkN}|aR7fD2t3m?#B`YMrk6aZJ*e -2VfLINEpDpzR?BViB6Y!AoGC#-19t~T=V%EYybiPCjbBdaA|NaUv_0~WN&gWV`yP=WMycaX<=?{Z)9a`E^vA6U2B^hM|u6OUooI0vI(-cdv<1alqiWT8&J@NNH~PVVY8CP@`lx}vOBV -+D9W=o0wmjDFe?ED%>8OE=4!6ydBpSN^9(;hza(96_j&8Bt}Z;qRL@PdU%;nZJ^l9O%$alQt=j*#hcE -AJKYVt5^Wom+(~V14`%i6c?%Uru_>O~(b6e-vH!mD&T<$-4a77{e_C2}1b+K`2{nA6LPxm&@Z*4zx?y -0ry^S%BgXjf4H}CHT(a`p~mBz8~CrKLyeQY3v0CBzSEDNIdkmD$<^aWPQH8f#HmLgJKi|lxVEp6{WUv> -8rK?){fz^IPaPlx=+ZkaczO9yBPrldi@lo~ep&3@*6?QpoM?DYz$+TQt>Kn9$Q=QTgM3yTB-QZeG~8a -&@aM&Oi(|hc4${@|7sTG;1ivWY>`c-E{*r*jrFvDsEe(HJe58QCBK8*Ld{yi%%K4hOCSvce3s~HpZwO -d?LM_h05k>3??s-LRY1e|F2`=Uzm4E{iTq_{gj6nl%0{E@hV8hu@(skmo9)^ -M_<;hzY&tKpvtxT)cviDT=t{<%QK@<$C@#KcQ?Ym*^ITmr_Afpj -C_AU`O;L(U1dzXkC@L0qRcr@b1M~+6^*t|G*mz$M}aTq173CE^BLB5uGX;>HP%McjZ#B5s -gOiMVl$5^)1A5jWrxapMzB;s#-sh#SW!5jS9yxN+7c;>OM;;s$IIH;z#vZtQFlH&BzfQA&xpvGXXz4c -;+{8|Y}njlE064R|!-23#U;z+(|N&bvh1VCWKYWA74iWA74i12&0Uk?u8%bDR2`ay;V3-eW25I*B`o; -`Sr%DJbqbiQA#L{fHaz=JQf@6m`GdzXkCMUF+>4#izf+=^YVS>*aB6t}}F!4oxa#61PY?MK`TrnrMxB@W^SJdWaa5Vu2dR}(k -T8CBdU(Ijr5QE>;fN*u)Puu2??+hLU?Ox%J+gIOgG;&xaic+y7mxTm1FagYgGB@V@1Cvp3E+(E388Wl -IVXEfq=c-#)+20RjR<3vXCxFd1%E%a90Qy^}f$>gjO#h2GCy7X7vF`OJ%apNE*;s!jLRf0EaG~%AuaM -FvovGZ8O?G;Wsh&x0$i9?6CN~(z)r%@tq>}(P@7-kf!#6jGEOT>*M*FoG4s|4g)M>q+19L3$zR*B$6f -5klotHh7E{j3te6I9%Qqg7H=S+7;%@VIe|(LC$6ILj-`N+_Phq)EQ0&u}ZLa9af2hxa+h^aO@c!$L&Yl)uT#q)>9r;f&-6al>`{a?I3Q4RpK{}+s`TiJgZ -iT;a@btq;>^+uM;s__F;Bn8XRpRGyJFF6i;s$IIH?H3}iW_JRRtezXRtXqp48 -;w2-V}Eb;&v$RYT|Bck6Un8-Q&0c&#cECgt&19=WQJKViC9Bs1np(!>E#pTP0o%IMv}~fKeqOIwu{(9 -VDEr(<%Wx)~J$d;#Mqm%_b>t;&v!*hgIT7+zzY6p}75sJBU?+V~=H(1X0{|dfa|iiG#Ry$4+!GHc9Uiw2aZg%t-`B+L2q&vO?xr3NDA_2K$6cr5o*m+@)8h_8+TnY1LXRp5(mCl++z#T_qe>+0)U3E29(NF{1m`^#abxe9ZNRCMxPyd~K@|7Q5Vx -0A5@N2B5X2q8;|@UFUWyw8xFCuf3_Vuoq=UHiBvX=KYMvbs;8|_J0Xo{K5{ -Jh8xiF=X@Da;CSn4D#6|pRNO&`d*&6l!{e@zxE)rBgSfH -xA`mywF%&n@NwvJsD{(u*N$g$6LJB&Z6fa~XZZG0?C~oXM)?6hH;+{e{S*PN5gp-q3+<+H>xC4ZfI7I -N_fPSM&W}dkH6t{!8{SQLNMAa2JvZorYa8#@K!%6wP&#}in^T -PpySS59O+%r$yA*>PyaXUtpgdlE*Rf1!TGaq*~aXW?sM&dSz+o8At&nj_i-WCsb-HW_Ccsq#Tp2DD#I -(ge6xB*9}q}bbcG$3YAD)Bbp5^n=8@iyR*=9mIp;%&eqEr0{K#M^*P-UeFYZNQ_fqu>|~h@D57jvMGm -PKjdy9C$D#-p1Y~-UeLaZNMeo23+E8z!RLR1Rq)AZNMgPr#hPykMKMbx1UoIL~sL+#9dtPSt4%7EIVcO>owC -vJ!0c6i(l#T~*bsh*B|wiS1E$NQ`(ZimP1AZ~|M;>afJRNP)xiSC;ek6GmH$lHs;+w+-CI;NPKbT&C@ -r^GQ+$%18*L3n$aiCa8Sk+>sqFA8z{32w(=zz`Pq#Is4SQQTgGN_^UHs+|&W*I0rZyf~Iqf`d$djdzD -r0=PunI-3+vP9*L~+zU?JlULlc;*_Aa2K)631Xb2XXsFlR -=2vPjKS|OJhoO`;EBkk+>sqFA8z{4F+^5Zm)j#$vY)pg8_Xk?f_27%#7mpYrkcI52^=WW048;4WkH5Sm}lq}9z!0LHQs(IVb;dTgazqZLb1$U6KfL=~Xq6Y(t8ySf^5_cqSz;36+ -OK>~dCg-vHrkc2YoRT_;+pldh$OKb<(WJxT_KGH3%-iCQMc$6Q9eEq@ybdV|Fc#3k+qF5|L3rCQnsf+ -mN4qNqv#O;?%;uzyBTIL{b>^# -yE3bUO|`gFV3I86z7ao&uQ5M#J`dMSCiBX39EUU1&_o28^i!98)KB!J)^-{1~nl=unmAoBwH@wUS#aR -_d&Zg*a5QeN1(;BDxJ$lJv=iM(AKZ`auDK94<y8!oVbH5S?1L=StD^*j|I -e$!z*r_#wgwH;GJIz?n$Hz`*P*x_9=C(Iagecyo4eg*L6Nv4ao?ZB?U)x3haSf&afFj0tP-!mfB -{xes3z`es|0jZ!}L-P#qDR61espSp|~C4q=UEtk4D_weIw625_cr-`;)l+6t@>~I}~?)#69UzB|gL*W -K>BIt0aix_9N~fR*4QL#VJPOj>Ns-#EqRNr?_h*ZonpS106|mgNse##=c`HZt#vt+(1Y3xUu(G;UwTX -)+}=nx1((`gyL@L;eg^?B5_CJUKHX6IvQ~U9*4O7!pUml23iAg12%~ps7c&FM$*8mDCwd`VqHZ1 -CCbQV*g0oQE@K{ar?Erhgc@qLEK(eiDQ{yz>^|whgDMB+<*?n9b~mhzqv~4Y-V;I8vUo%`i=AJ=Np?_{YLM~`cA*GzS-z+udQ#cZ(eBhdsq7O@3V4_u5E9xuU+Upe7e`ayuC?q_L% -Kly?AzOqw&C{%bVx=m)H91TbmCShu!M!&~eZ9Hn!Bkwi?fDZJ(!qs9x{J!A6(9`I6?PXaf~O!$JEbtbstiCJiZOP+L19M?QGZ)k#xo|@Grxaz4{ZGy|5n)fupbx+N -D6I}SztT*WbFV(vSyi_l)ep;#*S3fP)dmfKosa{aMSy<=u&cqhA9#E5?ktHtAHWNh2=GCGvZ8kYlokCoKv~f{0m_OF0+ba!1W;D=FhHuPpjAA~%U -uEDNp39(5RY*y5g?x7Rw_Vdjivzc^wLy?0P*OyS^~s#+iD9C4{fU>Ks>Q6d5;bO6!+-B-+-~?J^EXK@ -*W)qDDTnV0hIUX?*Ynt^bY`)nYCP;=MjMFcgS+F=OX}>*|c1c)KP%S-(D`>g+~D@DX`qm!N)+h>X*=R -CkNjJQ2pp!Udh3C15`h%m%BOm9)RlSaI5%6J_b-pkyfEP{t=+62v`2S0JDlZ#kn5`rV7IKH~}y#s8iH -)5}2w7^n4265~?1s!21BIdRj&0{{&Fg(<&-I4Up<7DnA2|>M1IJKR{JayQutefa(sli^~5Qpem?cRQ> -^g>JGJwD*pwbs;FI5^g(>VsfyY~MgIy=K6C#DP(E`{JONZbbpH-eK6D=fD2ZVepd^O>0GO53Dar)DcU -485qD+u`H>;>!TzLLkT~ZZwibI}fG%E>Z@^|Wzx=DCK_#1U8E2&c)^#X(HF5ww@iqWhlT+wv~)osE7{ -*zI4pE|_@@nJ^QlUINn3}$7a)QhYxsI21I(V9b*RXjUe3}$7ui-TTbP(6E{V%Ps-G%E`SWeK9R1}@+Z -gQ}{P;)!G>M4jv_#l5=BsH&>?sHYiJkKamB)-x=QR9P#~QTYh -|52NxKyw0e62>+K+`StX~l@322w1{VMdD($Ul%N1nvH_DQInl(=x0e~s$^%uh{gNmtu?#nEkI}3=kR_ -Whi4qf_9QIxkr6$0PY`r8(PJry$c}bL>XqSzbLC9V9}!9pLG8?`>uWrA*Q#)O(w`M2VAh3H9D)<{^EugnIXwgGr(+q24{_P!cIisCSPaPSPn$sCSRwF -_J2H?(gmC^HKHU3A?wq%ebl+kJG(9ex8(QNfWM@c~9wCSM?@*f4|GP -syE^L`z^**y$RpnuQ0CaP5J(Qig8tM%J=vCjH`N6zQ5mQT-BR$z29M6)thp?cy3cNQc|w>n~bY^Q?B> -(jH`N6uJ?0{t9nze_hrUay(!oG8OBw;O|JJf##OyduJ?P4t9qM!fA#08dhzvsZ|_CMiJnmJ9k`b%nJM -_fyLaaXvJgZ%Xb-ab9fkrsSQJ9o{r|P70JS5m_hrv|EfSxq~x%o>4`@__P-oCHY;Td?}UOX_md+l>8Iy` -$zfxgdXHS~_mo|Irq@5Qb-uUpfO4{Tdi~YSwTr#O2l9V=AP-VU|JzvGyl{ -E#LUw@Dmv?s7*EYS6piX18zqPuxeZIH7dU5U2;Xh@Z|GDS4dQsJU7{@$**IUC{?$wC=hik>FK -z9t_t&59t!}GC;qcN!-A=omeYJZY?!&#_rPZ~abL;D?8@;}Y6%M~^ZDXhBdz`h6OHZwx?e!05Z`$Bfg -HI1$9(-o7H@H2xGx+S_bA!(hUKxC0@WsKG2CojjJow7stAno%zCQTI;G2VQ4Zc12&fvR)?+soXe1GtR -!4C&N8oWOE@!%(epALRD`1#-$gEt1h9Q%ngZza9K;@aEw6!`${8QvIvbojC1$A>qEw}#I -TUl_hP{KW8+!hqo+P_W;Oc_nZyvJ5krth3_)TD5<`#}g6xkWNDM(@2tr^CLGGgvgss -j2JCw9^E1b>&JOqkCw9&b%gM~yGokJrD(LU!c`)19i^f`Cgmv1ghpL3Tp12PEM=iFsqlq>@FIiF+SkK -_gWoV#p}lfr<{Ik?3z*_QYgXbA0*AlRzK+x_?tt1%1DvG-W+nRq)Fgz4%u8LRl;@^r&6R#QhpQG_%{Y=leBEdP;|oHCfhN -Tov^hzxXCa%l|`ZKm*nXQ47ES9&rSIWT(Z0T8c=`&lj|<$Q*Cl7@$^2+FqxD^op(8*>X6NahxZ1Qksu5OnxltnUJ4{-T76a5CvlJE+_BmWgu3vO$msVY^QQ0KRewdcgo2&1t3q#Qw(|g_oNQA7Qckw>{G*&~Q~qHk+mwHllkGhIC@0$#f0UE$RQ9RA$zZo4*{ -1NrN|uzMv63YvXRKsNi5WXtQd-7NmPpE2$r1?}D_J5LVT-L6w|Gs8xh>{K(rqW*D#KYR5zB?5yu)r5A}wNdz9=zbo8A<4z(f*>3Eel#)~Be0hv1v7BYFtF**c84?=#l+q$A});Z+496k^N@Jh}8h3B#YhCMw*4 -Y07~5=+yzkT7U3>{Qn!fi0x-KpbQgfxEy7&@rEU@K0x-Kp?Di$4WGZqCO6mAInw=utF;MCh;f{e)rwE -?@a;L~LcSx9>B6!`(og#P)%AF$k>7d7fD{*5v*GpAhhLnTYel|_4V94ie_0>_TF$S|=3m8$mFydT8Mbd!k@bG}w(GY|E^TKS>b1)^c^E)d}^dv}4 -fS~JrH;xfr&7fAKr>_r{#J#}nl^>mCn8Zqi<#Hb@i9Wm;NQOEumb;PJ6MjZsksN;T&I)u4pWF5kPG&? -}p7BSNfVa}N?AljTWTR`^obXGW?k8_4Ve(N -ln!Z6JCI5VH?N_{C-;i1v%kMiA{6n~fkZ>gl$OMvyxiHyS~1>Xrw`2aH~jyF8_p(F>vn@*BM%f6#MM$zBlcf|{)$!reA{LG=7Il!@4xUfv7B --Y(y&ycxvuc8hKh_I5egpmq>F`wb^`c|QnyyX>{+4I%99@~f41gs`{Ew>)nN;j6&6JnsqNtH9Y=-W0- -DfnU+QCxq)|k3Vk;;d(hI%ez9jUQWa)D$!mxKeBmW2-nL`NZuG?^~L0kAzZGWG>bYzxLh4+P-}=axlz -#@!sY4|g{*Zhm*Xw7J%lG!GTTG+q)KLgh_>|21`$1pk=Y@lCp|J-M6`8p_K4{IAW}e9z}Rh8#Dk=OtR -RpikQMoLE5p66o_Srzc-^WAi6|$rhwf8OVhtu&I>;J%pGPDG$|LWU4zfqyDXSp)zS*~nXq8*s#JTW0vQ>1uz`6APjAPDiqgj6k=;pxf5NOT~>(~~Tb>_CL4JBvtoAi>j9Nl}V{0IxKy73X() -hA|2;kl*RvEXptt-|0>+iZD36yQL!rO5^b0aL*Pc7~G(wdo(DEyo-6TD2KyK%CQ}V7i?0q`A*pdyVQCLE6U*Td~%l;B^PWHo4p=v;d9>>PtT0KxJoAcD^0p1};|`CnZsee!cg#oZR;Thet6pUyt?K@ysH37AB+h~rXTh5FS+Lk -Xqv=<#SKG>7LC1-;b4RakuWxLupZoJ>uL?8?mYsXNrDHzU>4u#FtBwhrq3E~7Cd+rXO3=nTm=u;?;cL$VD`-XMEJvJ -K0;K{khE8(O?Uc86pe+Pp!whh!T%yg~MdWE)m^gKQAVHgtJ|>=4N|tnmifB9d)j?Q8UiWE=P`HJU`S4 -g8`ST_V{Aep!t+k!%Cs52H`yi7Q>cA4a1{wt?@5(J7K`;QL{;iewx3ei*$X*#^EJMzct^f$xXWEs|~E -`(d<;WE=Q?82uvI2EHFg!$`J)??u2&c9Xs~AbY&L(m|G?PZ^43=#vn#n>CUuLtiN&+t4TH -r2yH`cXx(V^mVi=4E=ls^#Wx>KdGAWj1m*e&`&F;0aH@QGW1Ca6(Ae>6qQ(peq1meSxMasrpsxwlDAw -jeW?31D9S^a%?YBhV!BvJosvAnyqrTCE2a;P&Jev~x}IfAu9(hQvAJS8=g{VY=^Qwl3#RK>)mSfG=cl -7gl%NNMXmC8Y-0y`Z58Z`~Qqc9Qwo{hRv#_LKSEY?JW)7d<@t~%IFo_a -V>E~+12Pv)1WJIU_WmbyaweDKNX{&+sVpQyh@=Yvb*my_Lx>i@&@!3&dJ`?uEl;7W3PWZG@rc<#dU!% -G*gJUhI6_339W1>3=+tw884Mn^L;W*x*9$p-NdB**E75q5&U0BdwRS6l}sBA>lG^z+{YC-6_ny`rQ48R@=F@Sl@zDE#KbwaGLV0954+!JY)|Bw*=wBmO?ParhK -#7MC)d3-%9+~}vlt^BA()>qLxg~xw4|JMy#E;vUBYw1BB!0j-;s;)k_<`q$A9z9H2c9E-;7cZcw9gSg -+7~2#%%>mX2c9E-be|)B;5p(4UXb{KFA_iC9PuNpMdHWkbHtBsa>NfjNBrnMNBn4?BYxmH;>UP$#1Fg -}@uT}3@dGbM{OBe}{J?X>kJ-r)Kl;xRKcdYMKkyv!vC-^a( -9Pgw1g1qla@Pi>sIz4D#(CNW-bG#3xEhPBSI>-CKJqi9^M*Nh5d5a&nB}e?YEyai*t8GIBzm)hf%>{{ -HXz_Ez&pAElrljDPTKtUQM>pjxeyP)g?n?@O&f;gB9x3r7Tt61Sl=!7ik1N3syrk0uyrAHh5kKJN62I -nJOZ?=@CvbWg;s?CpP7g=?oW&2kpwk1q81Vzn5kDR}PsES@mrDHTrKI9_Mf|{vIX!?25z1@n4-<8#ae -u^o6L|YS!pG)eg8}Ylb_Fe83KosHr*{j3o23l%>eqJn$7H6wn&IpPOiPVr-Qa>S4J_1@d%h~Kl~ -mnQXa*_@o>=ZIgrw_6eM11@Iucuwlk9BGN4QY5eVg&w~v;^)%%IpW7^+YsUxDt^GLna#;rJu6Q9Od3C -$m$Z6>ieF0nuB;y51+5<69zRF?TpB;x*LicdOR{<>74nWMSrr_|l_LHX-`uT;`0=dxA%3aVBP4!T2X}!t#P5LkJ$w9`!z}UBo*Km;Y$WkFioa3(=3brP09Ql&E{Wgs! -QC#Y(MEowA%4@(`VzmiVuLHM{KsL;PUQ)g=B^QT&+ZQp8`kJbqz~ -HYxFQCwCd*ca_ys)#mPsoZPLK;*UsiPC`BIsxo6$d8*31>WUw6m8{hxEZM;2@k`S=fqPQ?!dg98ZOg6 -IBPD)eRc2QnzpzFd@RC)TG5msg{L+#QuBtLiYxMxn5kK%kdHfX>=M2f_E}@tc)an6VOz}&rGH)93bG3 -SKTk27j**EcHoJ+0M4jaY -g)`)x!}#XY~k)pHuw4vwEuW_@!14cYw>c)x!|KE5+~V0GF#GZPM?spr6&WdW7ZpJ$w8iA$~#`Cpfvwl -x5~>^)$pEoTxgdl=yL5{FG&uDt>8MW`_7V#m`thoZ?3}1&N=tdeDEtvdo@KHhBI37f1Y@)r0mW6~DAr -kMEPa9PzuU)#F?7lasrI8r)^|R8^#HRfwNc{J@)9J>?a@)Z+)f5m`N)$ImH#S6^NPUQqF){YGT`0bUd0_bvEcCG~K`&j^0b>E -X)pyCQzh;ujLXutuA!yfxLoX;KgH<$i$65%F_h+?DR_au&bm4{)KIV#JT;CHHn+IXxIp$uxeB_@x%V -l=z|H>s*z&O|m)3v%C7=VObT$Uy;QxCH|UO{2cK!Ru6h9N&K!Xe&OD()Z*ueA9y_~HgJ2pXkRd?XGJW -2#_9pVFIVyFhtv{3c}AOx4W^&0$;DaT}Q+ZykIsbm(;^4evbG##lIrN&saSb5r3O}fQv -qh@@!7V>S?k$4~ZY~mb7{};^z+Tq8~qBUi5u_%Z=jatR6I9^1)qB@e8dU&mKQV{I1S#ahtoG)#K{?me -lHzrtvc#KbjXKe&03PNE$zR=IW>Txy{{GA%3ai_k4Z}a7`3H9_m^sevbHEDgJt$-;yeR45wr{e$MKVd -i?yjlT<4Eyy#E9 -}G^t0L#?S5TO0zkEm(1n_GnY!^uPCWUKP{H{$-{C?8b4?8GsN$zHYeH_6#Qsk6XNFtf6a)WOXHVjbGj -1zLgHrxzx3QLuDay&TaL0h9i89eh+mq`$yxlaD)n&0&pAD=()it+-{OehcTwhy_sK)y?)a8(-uEr|G5 -(UgkA6z>KKdzW^f0_HJ-3VQ3m)I%viKdH+r@CoZS6`OevbFiKF9mOmwRrPE5=`u;OBCBTnT>e+%E8vy -f59_WwQ8%=XO2wevjnzkkwzmt=&y?_&MU|9DYvl*UaGuzSQGefS2s>bHrbf!w=j~GN*5cANWQ%{J=dq -J!ET_tny!ZS8W3Uz*Iht~ouvogRkxxh#I)7QdAEg#{a!(wx$*UC!x|p4)Zg^l*wF-S|=bT)_t -4iob47k5uuapITSyAyyAr%_p#Wgv8(M?H*73o(nce_jY|-{G8RpD1OfBVTiw~f(=sQ7gp-2$l{k~@k< -pyNBo@PmzHSbh+kT;f!o_<6u*%8J@4&y2=S9uyZT>YLH{)&eyP>tIg5YA3pM~=ZcS!sQV(bKcrMt$Sv -^b^zpK4n&g$W6b4slqX))AaWEGyEHYY>;TpItT6>Q)XzqA@Zm(_#*muvO- -CVq^knAO9i@k^~9&f|Aguz@3fVOEc4tH)I}e&1O=oX5`*KWFuD#P3M)3rnNueIHs{9 -|{YLa@67yKJ`e9KW9zoWffSLb#)-glJ7?^*D3Nj=izTO9981wZHXaC^JHk8cUj?V_7n6zb9I=h4ySv% -VI8-H0Ffa%(aJuDQj}2!6nR()c;zNBh+*)C0Up5!Cg-AyGrBd4(@Wq@8;mHbZ=Lx_@%@ztklD0b8-iFIj3h$iQiQ=r&RH;H1X^8@aX7rIDy44OydXc -$?8G>C5c~})Pw65OzM#mKd1PG=eJy0JyOLlC4NTn3#}ekieFl>K}!6M;$LIMFSPi56Mx40zv@{D)pfMT3h_w)~=NI(SEstpF6j^ro3O3;1?d>s+ZtzlPrF6e~( -G#fA1Oe2XD|G;aidorr&>ot}v7?UMWGKdHx+;^&CJZi ->Guiyycj#n08|go-KY(@dGben^Q>qoZ@eY|3pbW+__!9P7jyVhY}jJ7jN{+!uGpw>ZU* -<~6r^d=o!s^`L!|&G`|;FSL39Z}w*w*Od4{ZOz_papKoE;L*|fuU{Jfx+?yPh+mq`Su^726#se>zx4c -8L;MZ#A71sC&0UhlPv+TwR*!GRUzNuXB42V=kMQ6wNBmN&N9yrA+T0aZ>hZm~>#8;MjveoRhHs__dIKj4P=CB!e?+}$X}A7**{bl -#33e&FTOIk~JJ;03dK(0|RX9*+1cI=Cy%>fwl=Q~a*-_|d)=CE9$S-(s?Q8mng=tRBGiNaxIoGSm5Sm -(}B{V#B(6{G8(Fh#&0>5`HgE^HTvd2w@k@zcX!QVIvf} -56UugAk9zUA<`TiE?@k@!H%j#ju@i)ZZ5dYy--4TB`JHSQ9U%xzl;A^J%fj0-Z94LN<`0J+lfj3z_4e -?i`__G6Cbe!%yerZ;ZE8@p6Yfk*!0WQz!oYKwRnh`&@x$D{LakaV2DSn3do6X%yKeg^CkH22TpB3k%< -8jCP-1#k6Nj<)uo~nv-a=h>8{FZcYS9*Sn%jT4x-(q-QSe4m#jW#Lo3rqD#vpI#IVL|r=KfG8s-fyZh -pMdv?-X|Asc;EC>L%lEMeWBj3>fA1u(<9`4M>#!Ghu>9B599EA&f;Hn-gi}^ZKH}Zw@DU1y@H(hg*iP -4_(>K&`HhD7O+O9sn_)J@A2h_DcHa=c`BNS7gUAaKKQ=&eTe~EglT -b`U{HC9V_{}gI;tv|)PrGl3-~3q}#gBe6;(ujp>y&z`)s((4*-2g+Pm-PB)}7I8bZef>_JjGI(LC50? -*xEwyD+i9|I`n{R#PIYI~;8WAtj`>A>>w_~vZ2qF|)5YmzK2zOfL(}8FX#W;m9_{Vv0o@qg+)Ww-@1d3U? -OaP6~Gy;cg1|7U5nB_ZQ)Q3J(_HK?=tdp4(tdkew8rSTq;oO^fG(yk+q=?PX@sT#$*yb5Xuz@x?GhO! -#7)p`Gz?G0@OXc^EI^cFv>48yeb4j~1g1?W{+O(S~-~qs4m~+If!_;|=Y^M~m@>vBh)ojx3&w*Uo;Fi -`UM6l#6%AF1=j5cK+MBcd;c>=N*l1$zYiH4F9$`0ExN5b!rF7-!&Pryon7 -ofc{NxP72P0zP3u8qvosNF(}93(|<5v>=V>j0I^#XDvu0I%h!>QOd%0F?V7D+LhdG5zsDUQ(e-6b`5t -U0)jQ#1hlKyoOraLUAo;a0qwdq?+OdrMceHY(5~1ny+`LQNbb?;-?EHF@6m5tklv$h3(|Y^I~Jt(=yx -qh@6qpBQ17=5(s5p}p#Eo1t9J-KWkJ0^+3FvH7cHo_(^~19?`aF_m*gP5&@+~7wI?=6Iqnk{)cZk$^i -g`&f_kg3mA+-4v!IG7PJjD(i|L)qK|0Lex1b3q9p)cc&;&F%1TR_81QZ{Fmo2CR3e$kDSWpKPrX#v)U -lS&vw9ijk&;*qB`G*$N0mW&b*DR<4vJJ0WOa&CDvvI>>I-W4SK`&TP#}lSA{6`kl@xWCr*F;Pwo3n$784X&n!rn+@D*JF1Z(9v?yJ3e_=to=Kj)xloy5;q`dG~7E~ei(ji%Tk5xqdG$2d -uu?nc4{w`jsEgevp{`L+~6;VG0@kVXwI~1lfkN0Uy?>>g<`nnBNuew$`fIC1{OoMdp#{hLqb|wEcP<@ -}mbcMbQRE1^N$Sy#AqgrWTccCs+VEuIAWs*b1WuN0IKowV<2CxTE-zXcv-vHHXFC8irh?W}tbdl}@R8 -iRn89K;hFSJth9#BIM>AcCNbiw_2nTSiY<3Q<3R-ZFy1(un#21eZ}1_8C_{XyXSu)DRaRbnpYs9s}TA;5uHr -=G6~+_<;^NglHM`@dF*TUF7Nq1N=Y-ZkMV0L5v^hz{Bj~BmBSwp3MkkuTdOPHY0dMO+EnW3Qws~9#J+ -`cv6k}h_b1|?Jx=?!c@h$9Y%#jn35Q`Lo1OOqm6Mlv=|v+refR-tw#pg1#mBnB#E+Lz|AnSB*N6jxEV -&8M3~wbH^a!22=fu+W*CVQWrM}dFfyf$sf}?nj8ti324ma|BUjp(!5BBgNS1aMAZ~_{E$wUuvww)_$l -F=u*}p_|&)#8wSsBGa`-f09df&G@y`I)5qB*7S->XVfyMs`xC*zAi+&EUzR4E-J -Ww5J&%$2-tm5n<4anLDp_VjljO10oI?5v%(V>ww_UZW5hpz!wadr^q>ws05U5xMzz$(lxT7Ma^in4>g --vq3pwA_^$sG$})UChZ_fK`~CBcC=3wa|%hf8GYHpIuz?Yk*al9ZbqQfc3i+@VkIjlpRF*9^mdrBu9r -v^&s~50UL(4@CSf(-aE$kb-*gf9`2+>mYH-ZvxgL)uP`5ss#fU)wcoDEYI -%%RWbICB*ukW2!*)dcLAFx6PNsV;955vITsmeMbtS`>^#zvsDlCi0I*)LxHmrpte?vxsdAB)MSXPgpM -X`An7J#lKm-AonnnE{tU1tTwL+zfc0tx{0qQ(wH`59B -V)*5UH%fVJ_3bde+XC~=*6u(0Ic6G7~uZ`))bpwGh@=ocrl&u2&`W$=^du9jxv3+DXi~g_N@M&y{5@0 -K5~9DGOEe02Uxw4mQ9@_Nqv#gO?G9%=#8{$vhSG8=Z%bJ!V`61**nsbse8o!jm)PA?2N>57qI@P7;gQ0fK`|SL}}?eGI9uHfABtVvuH8I4} -hD0W*%Akhi=W0rBD4D3KOYs;8(U5&FjlopMK_2upL|9*OJuNk}CZl`>Q+P)R-|@065NvQ0@erN)%B`1 -`0P%=SD0`>Y=?|SuKS1)n(!d5S5^&VF5T=jlc?^5;NRPRXjK2$G1^`cWRHT42hFDvzCQg0#k#!+t<^( -Iko4fTdlZv(~riij2IDuPvHs)$pOq~b%xeTwH4sVN3iY^9h -N!=c!Jc4cm4Z*nhVXkl_> -WppoWVQy!1b#iNIb7*aEWMynFaCz;W`YbMoB>{l}BO#)P6FZJKBkdp!TJ3C -RXJzCKIf?Udp3ce3d6)w+c8p&k;0HK{B@@&0H!x-(4wW -zMwDk8TZj?xz1=pJ`p)-JyS}Gp*-`HwM}JHm+Q~^xU(TpV>P9`14P1UAXwXF50(&gwAMRc=>o^#&Y=jfBpaf|3vMYL@=`YGoK9gaTjd|I2M&p1aY&iC0O+EE<+bP?6 -Xai4RJTb$W>=X{GJTyWl79N~Eb$r3w9h!>P@LIsIOk -A|*=L>O7P)=S8M8C3`7p)jb;}t9KERIi*5a_Yol)fAuJhJnBFD}d6hpgLM2oy8MX<-g{UX@s;MDo3;sg$y5h!N-j -+0h#r+(3i7UTW0bKGLe?>Z?I$NiFXeHB;I(R~b#)9I0fr64u=2&Qf*4i^!%Bc*g)pod#IQmbRtUoiV%P-2u!0!YHDH(s>V; -tnSHm!UWOEqS7{e5v$M7a&*nw#CYFJ00hP430+8e_0dDUl!yD_;!zs^L(kL=9?~;J0d~1o5_#8 -m2c_Q^Od;hB54bF(igH#IR}D})u_=MQVGS{?A%-QUgsF~U1u?8 -4hBd^nMi^EQ!`fF38($17h+*grhcP80SfbPZYFK7Un1C3jV|%G#Y)Tjrh7H89rQP8o>`ZaVRZ_zk!~U -j(38aRN0K?dnFaiuqykW@_9mcR(z8c0DhNeWL-Y~{6^oB8pp&G^*#%frHP{aP-u!b1Mri92FE-@vDiP -h9F9b^H+bdUy32@!^gx7C*D+Js>TYf?2VF(ojDu^Pr0*4}CuW7vO*PJ&@L`P(;gkiEOd@$VRx?u-k>%K(iuZ9)Gu=2*R5@1+C46860zR#BE1eFpP -!&nVNQv#}C0fw6`SgQlW1V1kf%hYh>1#6a7hl{8>Y2V1t_hgCgc+5Ch$s1M>!vb&E8^d^sPJm&oh84n -an7UzTN?;7DMlq};7$(ZC^%7kPh84yzh37G>*&BAC6cWS9iecD9fMK(I3}ZE%2r!%=3~Pwt1Y$UW7)} -s|6Nq74H=KB4SW+ngO^F0yI6)Xr1Q<@dF^nrE>R6(S2g4+(mt8l!$pvdJegb0H2r#T6hOsvsgBms~ri -KOW8(0lXFboUU0;%CZ-Y~?lq*9`~H!N793#En|FIWp%DN$ldP}rw#SXL>4F>KhBa6luil#pOpHL8X+# -IU4pI0iM03)UJ{!+43#_^RPRdrDvoV>Jx-l)&DwcinJxHLMVZ4a9J$CA!G#hLbwfa2=*ZEozue2?r_7 -S4!X|x=?D^e~AuPN=PsqL&2Ir4M$KZA!y%F0t`bntgB-Wx2>xR8e30$xy=%R~24MPlj*A1_M8m_^VAj(DCz7cSVPNaqt?xJ&U{a7#@N)2NSV^hMvZ -a5T%{Y{D0@`jtlF!qKahQrhiLk#0eiHP?bE-@u)QNyw&x`6E)B};T7HEi$l!^N9?ecCs0rG&(kh@pKW -g1TYLb=DN%yq5;ZLHhGlibC2E -+L=$sq3ylxm5tofS~l8r9?F)Z*z*=VH%_J(~;i3Zg$#_-zgH%#xYriNu04ycApFf7~X -lDp`f+wMdS=jVH0s$qjLjO&JR!J0q~2f{FJX2uv6s9}vT>|H6L5r%bj3^(fyua_FGV~MV$eM6*%d4e`{DkVebvrFogqcu$Djy3pQAT)-W!Vz-7Y;qOk7%k4aBg-8kVSGjA5*XHN>#LHH^!K4 -aBg-8cwQXSVIhBPXb%RaGy&JF{~kmalM+Uj$!Ocgi*uT8ir-VzG_%Q3}a6MmknbzEZJ_j7ByUA4a+=< -5;a`X%S>UP-$0V7VRDQYJP9t8aPN<`+2_(g3^!Y^CQ-u}!=cnLHYL2(FfJPws9}s@fhmElVQfmk?hS7 ->j7^E^7#65uY)XVu!vQh8qSkONYM3bO+zF<*kZa%A2V29qdBdN=SPV;=H$qv%lB(fQ*08rHabgYMch+ -z;6qb1s(OAPJ3v}#BxEFmS)^I2cubMR+Z7Z{+PC_7taVxWi7>3rch8PxDqZ*RaAIthlu6iQ%I0%EwjDIu|ju^N`F&_%9>qrq?;rUbXp -xtFZa8jhF3xLhsN0v)cC@E5}pPr|=ySU}-Gm+Is!@TF -ab9Jtd7>=Q8*uQzB**Xbb9mCj@@UD~awuU7wbiTV>hVdiFYPh;JEWxl6VA#KTV+qzU3E2>Z5~AjfI(q2X8FsF?kQBz{YR -ytutr&$PC>mCf!rtX-xOYQRuI5c)=t*cuVeL!dAiW#7h0ecD;(lnME9u>6UJSD}>|9(SdpA}M!_D??_ -={l)h6Po_CmTxCQ8m1D3`-X1BF8X=8?=VQiD7nzoy#ch4mvHM@TzstVG2iEG~8$h9kwJOg|Ran=>lDK -XV`y%4vS&z3`b7kXvAyiC36-n%YoI0jNl@75hd1zk5{9=LcCL++1-k -rvBl$JZXffPq?}m4s1okAtU|2y6Lr(&#VT~}X17R31&_!M+A;WNq8jht-f;;G(t00yi1NEn{zb6sH0v -+}wbRY_QSF2$P3p@!-;g#|v1Qc$vXjnpFytx{t@Dj=-mQXZ|DO^X&`69j5ToTQ~eEj9WLBpoMFpaPwsnczv!<6n5rzeH;n@2D*5)a0C?gu9A>+Zg^ApzS6>pfWp`q -mT2KsbtIxGR^u`WXBwryek8%Lf3cdROafzAd5dAZK8G>v-$2)Fv6?{`ZnRiU(#I?)Ruf=Yt*IC`5W@; -$SW+gjs+;H{+`6i`kc5-!dnevPeMTqD}-SMG3>p?rJ!aQV;J@^!!n5o_Eu -AItKnKa2?{T}TCL3sbk5YQJBB4{SVfEBPz!Vr!`d6es=6AMU>KH3U<|A37{+Q?Aq;B)h84oFLKuc+5( -;7%?=-Bu)vyG^tEz_UD3hS57lwI-&Y6Ue7}kwq*x!^mQNykBMrKK!1l&#nH!^#xVTm{Fi(yH%8pLpQH -H@p(lmNpLH5|D&91Vu+sFSecefW{#j-*f`6oyyLlnAw-1onmlm8*I0H0-@ZC)ivq&=Ots&DEe9j(1m| -C17~1mgo$7!_L5#Z#8TnhH(p>ff&Y1bhv8RKnxp*VMz;}L=8)p=nTZLq=imH4C5s_12Jp_7&g^0j7|!=zZf)G!^pxu0aF!n8^)bRFK-#~s2Gb2v -ZWsLkOBTnQzR!wPXYkSn1ehZW?o^5n1*$YJP8D8ylfIIJLt@xDI(u7rwP4%gyJPtW-XdNx{$tvsC2j%4)YFcZuZUl5_pTXP`-o?q=&ID5lRnZUqZrRfiHnuIIu4v*kVmmv?kEQ-oAu -}9M*`#VT#rSdbn9%q7Dv|IBk%_GCf?vVeAjfe2Eecb3?P6W-R`2sC^}x<*;g$!)o~)#=e9%hgE|d#(G -%b4-0&W>Ks;U!r>+8VG1|b!%?zMM|fUnrQ>xvH=mQALCViJlCm|)&Juz2u!0n4jQ)9ESD7*dO+9u&ki)osxP-$5#SlNiA=_J`WSy>r!=n1(5)Mm{_e9*)MBAjU7fovx -0q=7j6k+|MlMu%w?Zlpe+$meddXw>3-p>2SS-q;5^ZVgG(Q<;&qf^%CB`gy6T31bSFO4hz=lG~%$pmk -6bYF^5HZm@pSY56d_#)59h85=7ie92V)}P41&qw5=y{f13B#POJF_RtS=$pu!bD=t -{;~4)8Va$1=~wtUqVnn4EgSN~4{=}&Ylz{+@yf==gXyK54 -#3sj?cs}~-QjlY`pv;)aD6(Q?6;;j2h-N}XuGvLp0!H1a-RskX!E`jF|L#@-Te)f+ -v`E%pl=_EZ&accIw&(XhH7Y2KK_JpnsuI&sT+JE@W#ul@rcx>!7(QY4)%;#TLDjzaEAJ%!;%!f@LPUgc&9^RY}Z|33le7K#5JM-aA9`4SEyLq@bAM -WMhz8j{yRnH|!xX=qZ5=!+#k_1ydmnAVnFC$wIy~{>ehVH2#U3Z;OI% -vY()!o9P1-bkls0f^LpKM?p8ipQoUk-7iqkN$r7C59Ifbo7Y2hL%B&kOhGrJPg2m$=ocyIX7mUJ-Hc9 -C(9P&k3c49RMnN|tx|lcJ$)YQ{?Pf%maof#^uHm+u5mUp>h_2qYn-N{QZ8syjZrg4~bkVlmjOdDOyBV -FKpqtSz5wYBiewl)9MrSGLX7no*bTj%@3c4Bn8U<}e3qO?Fv1+C -Zn7bkk(o3l9TbD?dB)j{@B^mCg42gFrXUq@Bz^26WRzItvd0U8@>9^N#@CG>^^x`~yJO&dY-Q@1}Wdc -IL%@H_c;np)#bG=CRqE7yi98k52v>zfLim$L4X29tC<=lO(vh}mpE -vX$*-|n98A;6kGR7zD;*b}KeUw;`4I~}VRdT$*JBRa*~zDIkArDCo&3*#mxF0Kd5SD9tcu9TVxMDaHu -<&80w*&z@&g`l(BAnAy^PxGhIalj^em#SXlUo3^D+l*Itzu{I+F_v4#*D57dH6?`vlbGv+A|^1^d|5 --Q*WK{DQ?^H{ln${DOUk=^B2a$1m9V>}r0Y&o9`_yG7;$e!*tmDKcj@n>`S9ip=?r%pR*cMdo~0W`0V -i$eeG@%(&#UCEu$i}ctOh(E*<(6=+a7&`!!~dF(mZ;D!!~Qu6Gz|Vu+5piijTgxR+hV6#g-XZITIn;7>T5B}vD -Am&5iOhobLsG|P~N^`{)pGNfVsFAm$8OemS3aoElzsmY`FIc#T=WWdn}9JWc)SNYL@Q@EeW0~*x-;jm -4TzT=Pnm%}zm`bMAq0f+5ODhmH0hwT?DEx&%5qiK@;{H$kx#L+ZI`u>>xF-Pqz=4Ubc6OLvX(t-b!!} -c4NPJ-+=bF6>J3;Qz;rwR7+L7M$JN7MZJ^xnVVs7;T=*6c4iY$uV_^6alTZ1bZx|20SL94b2JuX5PVp -{Bh5hQn5?=4W^BHICXTB#AcrTMB0@YdW9V+23*4W=PS$=ct`QdK8%b1BY#jq@ia2$l)|a!Wmna#)_jp -MgNJTHbc@vvw!ArmLVnnFC4aWNV;_PuN=-&q|y62g)^N*XZI>c(+oEk4N7B`5;2IKNMof^kJ5gfqiKF -U8vTFcXqq2=)z7}c(KJ8$wqs+~SgAzvX7){vrrD8-n0vVEnK*5%Lh8{GzRgkl1i1JsN5*O+(oD -1OaMUt}a{DevZEiFj-{YtiwshR@b2OVfO8EyIw#m`3{|86y#kuG?8>@r*bP@z-V?_{=i>+m26%Yxa** -hGym!_f-{vn6c>`2_re#B9m9UbDw9JK&dG{isQuzf_NL%hq;G`l`!_Z~;n?0S^lPdS=>5iObph9L;oX9{qr$cJA`=;gQJ1O+FC3NSWE?>C;nwgp|pzY#_h+!o??_I^R0mx;)ywu`_() -?sRAk>&wIGh4J=q=OOE5?+>S2yMx=qv!~L(KW+d1bpO=x>Zt6$ox$#ngTamTKvxbXw?_LnKh{y~DQ!* -1TjR<0aI$rKuy^+Vv(bIdb?derhqH9t^N&CO^zoQ|oP$p9?v1VwcDDA$`=jaT&TwmD*BZ{MNBdosel> -rLBiF&na -_T1aP9i`@WmT9M=#ylxxG8yo9s^y?%e&-?8~#S%)UDN+U)DIS7&d`-kiNPdw2H!?1Ou+-FyAs+xOo2@ -U`@!e0XEy{{T=+0|XQR000O8`*~JVcR(3*&&lF&W~DH+N -;o6ik?6o)Qu~RIO^Mg>JwVN3{p`#(c4462uhL9@D7bXDamEB}36-u?o27DlUQ$ALUZW=m8DGK*(6`N# --gxftPh;ahJ-KWNW!bs=0!)aRK#OO-B=Z)vM6*eHfB%2tyV!`Nk%3B!&o-#2{ -h8zoxCHc2ff6uStGNgw4ST-Euj>n17g@YfhLQwfN`3w!UAd<0P9&+lO^R^Yh-NH*bN%#WE; -;0BYgLLz=4CVaXk(JGi=ycCvVauKFU{@x!$kA)Ztd}jfZNIzJ;_}oKqk9#Z#8Mq-VNfyrYRGQ?7oj3x -fK=V=h?}2e#CvGY~N}yiVQ?|rn1!IM>tH^&W_mY4Y==2(VESF4?)x9amBb@*bBC~TW4&IOaA+)^OPBkDl&P7fXSwSm4+aUo3r&hA}GX25OCtqk+fH(D;FfR=eBlj- -JSSvpec{Y#U5#2rU@2hNG@A?zM(67!L=Q-Kk=68B0@4$=sLZEX7kaxe%C1+$1jd!#MOOEN_So`V*MDZ -w$-qG2B%XaIr&{>$7U;i(nSTi;z5__rRwR2vvc1$+35^J$I^$R`U8*a94QM3S8@`SGJBwYsj?F7-xD2N>HZIrM=}|KY@!Pf63J5;sjXdABU3c){qgdbGz36xvJ;B4MJ)f)zG ->qChJ=nAr-uP1DR_=O4#tjhz1Y5&ncLO&dR$cvAx?McZb|12)mD(GL{rBotAHw`>CibrQIt$2soAC0C -eypD7is+qmAy*TaSD=c~p~B8yf%tdYv#Z{nT#xNhP4^`7;Vo$xn>_1JuZwh7(bS&gwq4}D9*y(Jwb$g -tU$!4yPPdYx~X? -2a~1fRb$e~q#_$^s!5kwPO?NS5QD*626&5fAx(^ut=BT|s`*%Z^?MlxoT^xkP?MF4ro@zmb(pvvK5GB -CZoP(WAYZdh#u5+6Oi3g})_=?o@We+R)Uu*!2SY&!d_}KP@Vy79DpWE=YVV*&;W!IA -PB~=$G?CLMT;Wt$E}E!7q*Bcg7r8PnR2?K$4jo$^FF9ECMZmU{aLoJH@$k@1)6hN*iNmKt+nI;4VB5U -|jA```56jckfu`hq&Q51H|GPRNceCpcP%?dDu$;j&G!1GV&7J8|ZkRqBY2H0XKY -PD7)G6(mV*XNiF7L10XUI@u*p0TO!#DZbhr+{Wbg0?b?v-3Y`yWtC0|XQR000 -O8`*~JVHgyz`sJcjB?J;w0>~JhqEloUdkm{HyJ)@6mHX@a)_@Jzq!+1HqTt<`H}l^72v=9t5 -8d@2hGQBHd;RgOPj{n9?{B(bdsVw4cf~Z(Vkx8_Q%{9VBB3YsL&UL9b17a~C%6e#zE0Rs$c6)>GzBni -)eZ3SHmpfSWGbLxKW38z{)Tb1UZ+w7z7a|$c-0%Fvljr45nJCXrqKQO{5bXRD;g3SZH5wt>lqrrbI>NJLufdR;3j9k~D`R*(C+}4EZwrO= -sY6%LaAqknmu23<203e8dM^Dm%DNQ&^<0cSb -OJ9cW<7Dnqcl=CWx{lKwyU#bUMSXCKs_&4*dJ_u(gO&EB*&=ZOJoTO4BRsly!_m-vw%HGc?zlgj(O}| -Gm!@6E9roslMZ~MB_X@ -Bbc-tG|1XE?d^P=9>?lN;%(R*N(yskL7?8?6>#kLuaM>)~+5W||%D4OymW`n`d@5pp>*=r&~O2P+{td -#lxILF^~kZE0-8-PZ64SpDP;vyZu@4<9~cf2&<`lk7vt5qX}#w(-1r!g16@%gdz~i5QK4J64kSsP;)x -Y@&KKZj-Egqm`o_;~af%6lkz-<uZhGeF4 -lCHuab91+uOYbGUE01@!m8^$5T&AYu0@h8plO%X)6rIMwPPxSbcUKP!Udf6VSK2ly&p8Yc)S%qht2$q -=^sjx%vigXYS|YPm#JLSCfH`o~`q!q0yESJ?h+NfZ7P*xq(E?_uYQa9gZJ9pATmJ4Tei)JeDe5t~`7j@ay(FTUg0$hm!Szl932m1`P8*cR9@-Cyv -{jyPo^#^zkyfKi@G0*|%G~Sb%I&EsrP{i|;Ip^UZnWc!Bj9i`u_XO9KQH000080Q-4XQ?1hxsB!}U0N -e)v044wc0B~t=FJE?LZe(wAFJow7a%5$6FKuFDb7yjIb#QQUZ(?O~E^v93Rb7wTI23*7S6pdcK%x}rR -%&|vCHJ0teD687xx2glP=7EQPiZm^2Ge*z{mCr& -LBGM?)$VA%;WRU1EfkX!=#-PN^q_wUDH$!4c;Ut(lWdr=+)>IMPnuGuFdWP#1v|?k0*vR$5UFBJI3p!nDJbBiKRA<>Oq6L?REzgH{mVnNlB -5%T8g;k=7p#GNdm2$5vv08)otT%M2H@kd8SavwGmr-N9fRz(-f9&qty-(Z%X`_)IP7@=G`f@1B6EFEZ -0K0CfZ4kszGNdR)(33<7ob(;~M&d&wG}NHqd2&x&An(`qdjb*awmQev4vb9*Ky~JP -|JVb$FKnRck|aDd$I9ChoF+|4s2%MFj^PD_81xl;l2)7w%dnZ!1i0>qfgFy%@#4uXe46nsNPuRorQ=TE--;dnkB#4!zL5%nnSMe}j6nDinF7g0Ei2Q4_x`B7 -po<}H!mB+fv&)ld>AhY4x-qd&o3iw0#%8}`CsbHZ`dQexOPdHKIQlv=HK^P=P==sk&yl*X2OH3EAFd- -lp*u$=te@69HC>;S~nvxEgP%7-EY^-yYUn$*|UdFVqAwe4=F(|*_a(C!c|VnlxT&|v!Y4{xNqS}ij;N -v*?$vo^ZObE8*~02cbw7a$XJH}aWqmpPOaF8fr4F6P5e6^Q*QY!woJAIB94FOr`ZD-IUH_pkz5ueu*P --70F-_pwW^XLH5%BdUOs{PSO*FGdUzs(@Bv{jrMaBXYTzd%aq0{&)b@YKdePcY8mG&$O#w%ZZy0w-z> -RM$4tZ{kdG$EtgpnhghwbnUFZjU#42|1JXaXz};HEAa9bdYqZ{I&I!)m8z&mzTl*m_eW8Ee68<^aS#L -BenUjl`nKSP$Q|E@=C_CD8nL0D)GIjFg2H(+%`f1vKt8*Lt8#$!^15ir?1QY-O00;p4c~(c!Jc4cm4Z*nhVXkl_>WppoXVq$t8}_k(L@Lt4F(?t0=mkpp{v=J(w7?|8uc8HCaA?fyZ?m4y@f+agN19S%JvwZUKk7iP-z256tNGOvr-Vz0M -9?mkq_fAsh0L2lsp`YjWg!QQ%DGksrJdKl}kHjGbD+U+lthuLI|nX$(pjvB+l^i|EGplKMl_xXvW7f` -w?MS-?39{aN$?1^dVr{+!vC#{xceLYX}@M8qs_`Y%5K{;fZCW($!t_oh%CQR;{Xw_)0bU(PPsfQL|0_ -Jw;qfZANlC$3`ydEnUG3pukwb|=!pn^j94i-L@jg0N#BvX5K$BjXO>;@I~*2qA?v>PC9BB*z-_1NPpE -MET)t-U;FdDD;B4a5jRJStpxdRnsYRyg1t%XnXhrGK7jgKsRC&&gLPX&D|R00Q<7<9a -PFdQD7jemTEeW9fRh4UwKa>j2Qr3Q1(p{rpx}_fX@7Y1Com2UPX;IBAENZfC*!mJXvB`shwOk|91O -=NN0)1pgUzZ);)*QY;y;C6tmzGxm -#>?4qe~7j_V7QRfB$H&Ww3}RAUc;1APa2U*YURF01ofgHCVPYpiX+PL}tV3#VWN^kZiRSs%N;AJo{PA -4X!=(*xk@4a3kiqp2H=U5+3{^~3sSpGyuR)k9*U+B#mOJMKwho_|Z3^~GnY$r!H@XPdv$)_ -JYJNlT8(lRn_zg9`Dfmq_zbW`l$#2xMAAz?7ztvUag5Of}+k)R#^V@>oR`WZ8-%;~Bg5Oc|yMo`P__F -?8!S70btCsx;+!TDXtHuT2RP%d+-&6B@g5OK|&CEyOmf%~G-=uKCw`NLwBKQ+Ee_W9bpv6ag -hHg-a3&ICTW6RFa_AI++jaUo3`9^7Tb6FOnqm -XP-S%`D(OZyc*BasXD8{US$Y{8$`}fer>EYLTwP2Hj6N41&e6tjNF2pVC>+O1vS>ROE0N;?3YN&9X2x7LNsswR?|>p9&*wv*z=NSk$Zo!}Tsu9sH -Dxqp*VcUG(UV?6mdn(Vfu5=NdT|$6ZRlE~ySf01W}%U~gJ$d$^wqVZHV8{`En^TmCHz5pb(Zr7R8`jc -18E9Y2I1Q%Qk5=Rb!!m&djapVvvg~1Mi#F0awaO@C -B961ENv_qhuraZqxAi`l!=n&MwFf|`L1PaFvA&ZKjSl%MQh_LnZlz6Cl+J`4wJWqL^ry{$G5=%_PvNh -M;oUWRDtTPvT#0EZGk7c1#sbQDt; -H_od|d##a?W?-|C>6S4v@jz12$Q0_a#>o8C_}tc_2We%#95$Lpqf(LEvJ(^k5jV@!SdP6+Ggd0~&GPT -%WvFnvYJP|0lk-22mr+mmQ1h{up>XVFNE~??3ddfC#F3YwaO`DB9C;ZE$6kiSk(Z%x>}5zCc^L}FUM7 -ncyi778JjLw8-JgTX>-5d=JnwBj4!MgT(qg{6??EPKi{SlJS)nyEN);|=eS=tivAh^-0h#OrooH@RGfHN>KlmROr~IFFaHIH6dV!8~sBFceE)B3~?hu>mNs-=fbmPpTtFI@cN@WE9|5E0U!r#`NsLWsSNy*AK3%pq{f0Vh#=8wX$`6F>;{;K*<@Op4vJFZ -;va{mh5jvPch)rz+0^Huq}sk0Sz`Y=3>}jN42tYKhoK9GpmgIIg;!fYB^2?te}3IC -oAMcIO&B%MtS)qN?&B9!n+h(slAs+qlR|auT;bzhy9H9QbB&Ky?U&hdi)rO(q4*>wO6J#vpCXTijTEd -uVt^}y!KLjti2?Tw3ouM_L4Z#UJA$BOX5g-DI9Ari6iZ$aIC!~zE`spia1t55=SaX;aCMp9H}6MV-+N -Eq=FQVRglDAX&z%s%wx<3UXl?3gfKUiu`PwIn{HHhz{|OgTT9zNMcYb?TfB3J-joeOWMd<$+^9^+LKN -l^uFJ5*DM!(WOgAw%SC>3)5<1vez9;kztlHGIW*2q-HM1CX`3JgC)UIG9uBC3$$%%}4f3UnfOWvfDHC -lg@emOqYjLu;nS%pDj6$Wos;ms<%S%o*Nu(S$;esR%5cC7AZzP-7FtT4B8n@2ns$XkUHS#Lnpt|5C;& -CI66aaFFNbTe#v2$O6IZsJjGvWuG-taTHOjGGv2bQ1%Wn<(QY2q!U6I0@QE6za0>mO3{n>X-%P0G -_}DD}`l;N@r>x)Z^-~o7J1v+<1I3vzKk^sUlFX-k{S>w2GC!ZSfFplF;n-i0c+OwE<{Xgp%_k;$0}A# -RE2Ou=k-sqD(lUtOyw5NiHKUpSwrWOOT+IT=+6^&yJT5^0NY}uU!pf>)Zz6tr32tQ6Fsqbp?1GxxydC -Pbl2p@C_o|5WOpQH!5s7N@#$Fb6>7915UfE5_6Dh6JUiY=8sWl&Y#XhEmTDsQS^A>8ET5Icm)1@~h58 -2kW_MW#&+tk`mKhp2iS{+^M?0IW-Os%tZAL`MYl85Z-T6fP|rE6;47jX;QwVbJI=AO5lX=>)S{Ye_V; --=&wd%D)!^H%AZTJOdBQ~8#&bj{lHma|OFdUSu<{qPfAo9uZDO-yaFbKiQA_oG#9U9XpTBR~0v)o#jpRnkZuup(_z%G|B`S{2T)4`1QY-O00;p -4c~(>KpmKBa2LJ&07XSbz0001RX>c!Jc4cm4Z*nhVXkl_>WppodVq<7wa&u*LaB^>AWpXZXd97G&Z{k -Q2{?4x`DOy49SOb~Ko+fiy9U&$`MXrG4B^?=P#b*)htA*!RI3ap))eAjxu~2+PKNB?6q$NF -W^il>f#dx{FKUYX|FSqu_9!ebqmkK^puG$#y&J|-6%DH}n=3i+eBl6sU6!UqQybn)las$6175G=u=kt -v@iUJ?@8nZN0t1xr&-C#<67Qz&|V*yim{xO;f?t!__Jr;zF+>qAhEL`qf*)@ucQ$?sNAwG8hS7IFZ=4 -}&xoX~~U%3ez))?q8vTy6A6xYvg997=CUAY3Zrv`SsDv(zUZ#A25S4t{|^82X0vqZy+pWb;VWApx}Xd -v9_-oKC1cc%t9iqfvV>dHM=kgaWq+a3YDX)`CMYtWs{EH&7e`9_&%)9vtnPUcWbaGTL{0lYu=RQ+GI` -Ha)gSlU`@qZ;$A4I(i(AZ41F=EOW7OW|@<>E}lc=O2U&U5|@iGG5rbQ4WXd$Y3aT)gxO=LDxX|r$a?d -k7z!CIVtc`|bLdy%a|)!c)0^VR9i(R=x{$JqZyifyfPB1VS(ddza54x+|nqb-IzEHlz=2@HQ -`6t>(|IPt6w5bc~btK4cGWPXwQ{N@cE4G@cX!m=f1ILT3sra97ON(OR&S#!B&Lt>g?IO36An95ekGAE -tyXh*se~f$Jd~J4nfeRGT^(Vje8U8Ygvx_eaD6kM;Qr6MQM9U#V2axEJhZqgndE11ee2^`2U_YB^E0U -ZE2{&(+i($A~KgI+HpgV -b4M_ZxP>RPx?;U_XG~YWpCg2xaKn%aF-kI}miq>rHv-1(hsVC{jC=pG5$Rt(CxN$G0oZ^)sZIuy0r+c|s|@B7A%K{dXni0*GXzg$zwW&;_&i&l=8U^8c7A>(rXY#wuF^T3o??=YDs<}QW_q@S5 -nLe~rj=%VO*i(~F@CT{)*G6nwBo2}FLk^9j1D1t)DUF85wF&(L(NM-Gd-EuG#^b!8JmhznMD4oIi|54 -#glPPi8G)-& -%}GIem&9CvQTg5;7uYdtZgxR{O*EvWxef6Pjscdt2z*+2eZw&)si1^IiLZEq}S?G3XiRJls~UX;$imc -TEY`%xS$iX<5_3t=;Vn?A!C4urvDh?b}td5WHb289zPT4Es)}J#mJkTYH459~z;J#bd5ZudaxgfuxJe -X5DN72h!qC@%2QkSv7DT0RT1-@CooIO`F16L7m}XG8*>F#G!$$Ouk_e@q{!Zc{);Ww>uO6KwI(dD+36 -}hBjx)AY7S1W9yQcmCCFgCMTdojFDhma4e!&_HcmpQ;L23f-B18?PK3M!rB}%Y#aN8NWCO%e=l)PYI; -7(=2HOHzhcrkP#~$tE{1mbMvLn6!dyQ#1G`;f8tj>K-}od>GJ~$@0i^*fvE3@AGyB+5>vee5Xu -xsHXl8kG)wp^3O>)1HsnfvOitEPZkS%oct&iR7Cbuo=D{uUOM7{{n6n$k^8;qijpZL{#nT(Xx0)-z_ZY2 -v(UvjOxM@BEz!EQCw6Dj8{AzTW>6t)zozjn-y4cP+$)h(O*0kkPHX37X|~6eK6qr=IFfKeE*9wIKAT0 -0TJ@(eug*_l-N01*M++Bl90W|Ax{l={P~VeVv}YI5)cbkocZX;sPH$4+!x)+KL|SS`_No8L6EkM;E?^ -7S?6p)q(GS&dDV6pjjsAh5d%2XthrDV%@1aw@y4C7bj{oM9565FLoH!5muGETS%yzOnNacgkQzh$dt5 -AgbdTJg_P2j54T2>P~MG0nLd0aev%6`90<)es}w0WrURkCRXcA5(Z -*OO+b?-gCcveGiwHhZi-SXSYiox8-zwhnk&U>xGWUUS3?%V9Ked#6%=4rk0F22~Dr^mxyCV!$d5&(l` -mHOlfW?;)aJtDheT5yI3Q*i7*0Uk9@&nIp>Pxn9Koa$alHXA)9k-&1PvLLS}?aG(Oc_s_X;6)R!vX4; -5#IM>JYxi2J6PvRLyKOic|{T32*?b5jSzLGcnaU)S@TV*Mu~^!3`5AAPp&~&@X)H#Hsg=#{SaD8Tj?EIDKhR316taXAkA%+cBU6T0 -68$L^^k*^>xvh?N-+P&d`8r^q#Yj|FxJ9vM9EJ}e>6gWo`vw184jHnb#%nFVJ;MMV3ZD=%_ZpR(GTkoITLD -%tpdg^)9pnk&}xUFHQ;Zc9+^?SZkLvcA@r`RU*P?A4WJO<=UB9mwmCSsk3{*Ll$I2cDXWebinha6oM5 -yK6cug_1vp2P&W0gqAS8$mG0^gV#!C068KPm5@SF=#Zsy&HKjII -^tA!qo7;N*=DPmvrcN~Uv2$#Z(|tVg@wvRX7%MbQt$14|N~ZIIHqZ-rVkE@68YDw|Dhj^{X -x06O`biWcSnTU%|8II4_0CZNzWeCtF~7%q6rft-GM@G(G37P`jip#WeYC25y7)uv^Z+<(Dbgwtce;u$ -=>yYQ1l}`FanRy`k@KtdiaR{(Wcnt7>c&atH3E=iUe2nQ*U-R_on!JGjGVYL+fVNp(9Nc;Q4T7X#N1z -Te&igLcE+tGP+l2cGQ1SLjDD77elR?@|kIiN$gPmJkQK;zwet9BqpeTKmX)VzC{ -0B@Z?ZV!-ttdq1;jfwwr-aLm@}+Scj%zKDzRGA^b1&=_vUPDxBzz(PLcTWI+b<_}FQG%1VYd|)qx#s| -q{Bb;yjkEt1`^mt-Q+cjLuAj?6qp(_U~d%7!!g5R%7&wF${H1=TNcs{iq4};ycj5e8I4BNa)WqH*?)6 -(*fCAbnM;*4ojXs;@uWw_9bY(g8ZK_bT(_8Aa#BN5qAjPimF@EYdt%n}Hhf^P0X6|?|6XAb@%l45XY- -k^Tf%+`{xylJhO^yTD++|0^iQc8 -C)}&u$FF)4z@f+bMAc&x;6(MKFaD|K4@|8yRCmYVxj~nR#EHts*v9z@-z7h1+u!F;Wr%eX@6aWAK2mt$eR#VoIXEGoK006!Y001EX003}la4 -%nWWo~3|axY_OVRB?;bT4yiX>)LLZ(?O~E^v9RR)25WND%#>PcdqAD(DCoQk`^d)YB0l2|a!Z8&y#iS -;k&quVxqRuG7*_e{XGL8!)+4I;&E{?97{aZ)V57xTt>Uwtqhu({Rx3kDWf<4kz8e>5T71?SkAjqlw}x -F8qkPGGxRxZR8IgMSe@F6$P-hYJ1m#;D*Dq$DUavw@i^-lBvLe|Ckva_*(|kW)lk@_=ZcN@l$Q3N`3& -Crc$N$Kr!F2kQTcH@idjMEY^01RBJ=^ZiCwI-~R!KO7|9ZqKIbJSJRAXrk+`tGU-ZT6ko(fs=97`fQ4 -w}tFJWW(Ms_RT@orZWF$>@W-Ud=AJRm8p?tNoDlHXbq~+3lLt!_xlQ9LpAjLwiQ+Nnr$QR*nQXzgLL% -!TuQ!GNzu~i$UHhQn&{di78rco)~Gr<(tOyfnw0|XCwz=USxYuZ|yUKdK;*+QJG5W16qPt?63#&K?QK -QpCaq3c$ApDjadl2lapjjbQu+}gkfir!I#Zm4}^t5Sl3X-Hfouxn_KKL7}#-!MkBU=(Y%jH4zH;7gwe -?!#p6QfK@~pF6WzXY4-Sz!ys66#Yw)_+}e%0E3nCh1sES0F3&x?gJt^w}aupeYDlz4cu|xap-O`qYh0 -wGk4IP4?8oO&S%q!({B+ujO``1VpfTqOS}Z+MncJu8J|ZwrT&QaY7`iSwDzAF+8i*Bi6S3YWV`#R9!6 -5E621s21ic}?Bq2?czA28`!StPU7br!4n;80_)ui9saPr012rY0Be#e)9zKf((O}drZSl3Ypu~*ma_S -I$k-Bp`t?!a>Hq5in{$|Z79t1Xq>P;0XY*1porY?WMqKl*1c(I}1#zug(u#i_&0G&#*;uwA%VX@gPQ_ -`#YBwh!wjCyqTCr6>Ckz~pmhXF3aTJ`J2+$=<;>+G%W}H0+;kAk6o&v)uyhyE*a$3#~V0KhGI?qe-tn -%siNgac46J%Vu9EL{2MBe${HVAo8_NFjCU>YAvvVuz)abOY%I9J?43J&7x&yw~vmo(dq?KlM;tk?%ya -}aggcifyl=SCF=*YjzY`YdcGCs2b%futiGqI6IqW#kJLVnQdrOi#$1Wi@Jbo>(JvNqnBy-@DZtDw&zM -cGC;SaNPOO-Tl{kZ24m<}Q`C=9Ot8>6_KKWZ;puRVS0!zli#J!0{Z0XJ4~9lA(E>VL<-p=Ue(|Jq6z=94Y9^6f++>gjyY$!*HVzKnN`PqIC -Q4^OWdkxKTbyz)9K*Is2Y%@^QUq}dA!Rg*2Q9sNq}GE-}K_p@a4rvQ3-spi*6P%80~+DTpQpKn9*6on -kF>r%I`4yNa_{ZzPO4ioQu_z!MUukZG|?qGcX;dp)3tMy+=<1r!Q)!M^1m0!1CzTq*gG&Z>>QY?AMM8 -M$rXIhE#FqygCx$C(Pvp#+dd&m2?p!o9juYB9+^jpH3?MvaPilev5o}cw7Zu&9jBO-Zp*8hX(>X|qTk -yXC&zt_vPG!L*FwSNInO9KQH000080Q-4XQ^$+OgLe%80M{@804M+e0B~t=FJE?LZe(wAFJow7a%5$6 -FLiEdc4cyNVQge&bY)|7Z*nehdCgjFa~jDJ{;pp!vFZYlDUiX+w=6yzB7QNOH#Mxs}nojJv06Cbob029-1F^)_&jZvtIYS-5<2sS?}unN4ACb@ZgXQ7o1%MVlF -(}W#|5sGa=cf|BG;4o85Zio(B>m&cY5P580H5+zI`FIUuzT5(!;TgaTw2{GPl1iU-V}vCtqI^nD&m$6 -j%~S}r3`ICdy}PvU2=@&ok)g4wkn==V;*?T}B|WUVA_PGtuqYqiEyi;z{%k65eGXhMXM++wG$%WjBPv -KuaWaL=cDC+EHw24WIne^q&Of;Ws^+QA*W7VbO{OIBxCVaP(CU9ZE1?g}vt<4>0|8G;xIk=ALl4CpohvThTkY$W+;(g$&|_u#OM$O8svc7Z|6x$^3Z3T2?ceEtXirURl)+Y5g -$wiWrZu=rwa;t{~yGj4H@>I%UFBVizzD{(wMi^a1Y2CY(i*&my-537};kyTcDxw?lT?|HM9?-rSt_ho -9bp7N)=`46G%I<;oQhj9mq`7p}2z2)JzDoPPkv=~=he9e$$Kce=xVdoW;~s~dL8u1{}<-SgYt=?%NSy -}7;`v@IBy^F+m@Op}sR70)1Y=|htU5?i=ZZ~YU@E3rU#$`eBuh>kjzLrVY>*$y1K4Mv -kQm769PXmX5FVa&Q1Qre%<9M<%~qe~wA#$xj&eU>0CCRt(Vdz>ij|z@I^biU7m-|J#Dz)KXu>O<;w)|H#1j?f02TV3fVt9@gqp%maqCGD4EK55k$Yzn}P;@D20B=|h28D-&;IV; -caijyxXFT9um?l-d58#Dfh|lJDX=9G5P?B@O0XN|uTtfd1@I(^B@Jvkc8m3EKbEOzdPBcx;7JSqr|N0JcNOaM#@)zXZ^IaO -dh_p3yPY@#W2TCo5;J@u|S8hJKieRNcscVWxXgYYy680UsIcnBm&&7@z@Sk49s?9xM3c(^6+6UfEjNm -!Q{VPZRXaiJr!O(pqT=}lTp*PZz1K!mP)$K%9n#^V~QaD|w@=dPItfP=_X81d*jYbsE*g&;wlG~wX`N -1TQ{Rb}_ybsO+Gx>Reyt=>4ksr))>tLOa=dF;4ZLz=SB{Ck86B)+yK;wznTE@jG|?2@ -Fsl(PRLJeo>EOKE-Qky$N+mPRY-`w^6fj^LRq9s>Ar(l)uwJ5nHC-qOA*%XA3F0kETi=z(hJIdu -q+>v)jmbX)jD@P?slKPi$fkI@y$fx~)yZj#QNakP0>`%|>jN+F3kJ9{jOk&AfljQC!IC(#W-rH`(lNn -Giz!}ElRE`}c?Ia>aq8oq_NJi}Gs;&?o1keO=>76p0Ya!j=z^PRGCnzY<)=0Lr(catG3#XbTseHjUy0Jq#5mIg{m@C)<6n*E*7^`l50Jq8=8AMeM0U%c4+7wj$K)ATKO6m6)ZvB5=U8k -REV*O7JiYBD^=pZ{oznTlB4fhf&}ir*?1%DLlj*03c=5Zb{ATx+Ei^pjb;tviUOEX#PYX -!`N{saPB<+0Zq!)C0Z%W=%_B2T1@B>XhVH->?9Lv&H@6!@=}W^Q(W00>&DeC0T4D60K8S_iaF7}akT> -S9vI+u*eNhWezFNmvl3@jcS05fjKJ}xqzgT3yTi`T*ja~>(E6Kn$_>&TQ2au%)(749FO20${wV@>6M( -&!BZude%C`bJceiZ4wD;rIsMp>*Q~YeOp0$sRy6bdUr_CA-`lr-)+(J<$!qDY)?oEa588vwrvw*kL$r -vYMzKd(0{*PO~8|6Mv{P8Y3B@BJt4&g@8;tzk@nh_4sA8b@U{y{ -JoCuquy-1e)+ntq}NHt?@CKrJ6cnnwpmNDvnXHNtEE=+8SIyT+K(d_y -nF~c?K`8E`-xrF_*Z&Xo8H&JM0ZUoAoFj7U{wZ7Zmn5b4H*i%w_Y#Z$sop&KQt}6FbxDn2*?=`VVp -gS3`A9TAw9ZM2q-rPBVI9|9E1b(&2v3#l<#Me&6^h)Pun+%GY9;twteoJBU7^%T!c6};h?S>d>^5zMS&JgF&bw^}~iG&RDX!x;&;kQzXA;>-*g14yg- -CTH&d$TU$E(b?HhPuT}U-gn~M3r%pZ!*b=rDb()o;EhmhqP$;XB2Nmfl)}zk_IwdJrr|!kBbT_*yOnN4W(>r5= -+PIZ^bj?;i&{dPCr=0qNZpSJ7AoN%ISo@dc8s3S5!m)OIt-0hZT4|WI*^KVMQz6*_VD4z|MAy{vB)Mh -XJPEAj?>pl!95}n_{^p_LYB~_wmzd3`U*OiJ#~-rH2B`uaO?=+iY -*->%=diB^R(u1YvUMWx&F*X?U9gNEw`3fh`egTWvEm0l82jD(EpW;rja6tHsz}_dY1Ork%_QjHMY>1q -&ye7$@jL^-Si~40~>>;C2v94?HMD_{#Uam%*)!Qw2gOrxKoxg3g<`&O{(Sz|Ee{;;XMX)&L=O-n#n7q -wGB4cf1oKJ#cU~JgT4zY$}#UX)FHb?%@g0eXWyKtWzY=;(2;6J>Ne@7ULz-y_5*zaXpqLghJl^PtIHL -4#7sE4d^dT?=I-;yXz3RMrZdJ^f*~nxZ&ylcR=l~7JxE{G%_Tkd2!8*ZPdem$evg>{R_G}QbUDw-ik} -?t_I%wT#c`9(A~n(=`|ZZsv~t$D4(e{tW*mjYJ@3(%yVWE1m+Uizi}u;=d-Iu>`8;sPS>Q%?O&W>?(k -6N=v5b~@6aWAK -2mt$eR#WxAygfw(007Pk001EX003}la4%nWWo~3|axY_OVRB?;bT4&uW;k$iZ(?O~E^v9RR$FiCMi74 -ISB#PuNQn!nPL)ejm4Hc5!A7>xQJySgk71SdF4|ourN6%83vdiXJ*TRl;+grr`R3vwmzU=sc(R`-4oo -LQ+wpC9n!3Y3;gi_Q)+GdM1Xr;Y6A_1@Oc0ocO#aO{k1VNH5R=Vn?LsGl8Ag#Kd`B)fs6eJ*&Tvq?gG -R^E2Fa9VxnOZ*m=u~`)kdoFg@|D;Rb`JAMg|jDZcE0~RFNvUHd{)yHqah+pnrFFOB83rEf{bPUJIpZtEGPnj(XuwFAZ_QeYx_)X6XG;~SZPBGvA%d2v`5<8ru$4K) -kpP7W(-l26!ayXFpQR#vCKHx!3sWu8EpYUcTpvzWkB7NgJzk%moS)9XX -0#)S~S76M~Q^ksJ+02%p<-0!F~S>VR%@3Pg5U62aekty`kG&cpXL!!69X&P#Ww9dBRP-y~8l(cK=J^e -w1ROkn-5iAjMjcLxi#6Yp|brQU|ivR}BD|{Vz-oOc7A1IbOOemn^r+xKDYdN6?bzzZhB#dP4oeBsSq-?jjJ#9fPvW}a*EzQrj~W|BIYpotjQRq3xZUQGgd0 -_*4wv<;#L73cZKN`a*GypfwxgxbMyzyU$tv%jyY!T?HLiwa%UJ7d7q$q-x1m1^XYR5|qSj_e$zj2}P9Qu^@#eR -{}0_lV0Cme8#{dvC5;>r|64A*(qXDhIQ#+8Vu=VeFG@I_P97+JKw=oJiCgEu~0=vT@d(}seJ9Y)4P!Eh -0T!HZ|p%XIYLT6?_22c?H)KGJZs{kh=m4a;kFcXv{BvoXKHuT7aX_^RIW<^jCAI>XU`*P{DYkN$nCxb -}YJ8t>0V*Kz*@6aWAK2mt$eR#S&;{S~MJhuegrrE5KoZi5m9)|=zS|S`;Y9yF!Z?s}W?u2eT5CSN6vQ;P -z~CP#wIFF&Q4@;9CCIcQbIrmR(HYec!Jc4cm4Z*nhVXkl_>WppoNY-ulFUukY>bY -EXCaCzeb08mQ<1QY-O00;p4c~(c!Jc4cm4Z*nhVXkl_>WppoNY-ulJX -kl_>Wprg@bS`jtjaE%>+c*%t>sJh16q0srq6yH803CL>`%$3V4Z7Jwkp>DaQ8p2Y6i6zr5#+z`3@OQS -Vh7d1mdN4b%$u1v1kF@D><9d_wt$BGAm(Hl09Hf~qoxUmRa6lZN&s -ap(lTypjG^1LZ}M}WKuiNSmitNt4&J`5946jh?eOj}2#XzSuI5MZ14@(E#|Jo`qjOsj96&2?uZr39|U$T_l(rNp{9WqHiU7q>>F;HJ@i)8eFL(FU9iPe{Q -A$|Y}x{6dRCz{96Y6Gjkfn8Tvj&PiXlKFY~MJHqi5kY8bGip4+umY+Rz`X1a(V-&A>2?L_EGa(T=%H( -j~-lqfnLNbVLi-H}Pl`-yXnCsK5B}DTBwS7t-+f6f;ai%UweIFHc}Ck3#+7X9PNI7T*SsMPtQj2T`EH -vLmO`q4!!YRf4kdkblMuJnOT+P?(V{jIC`+qkRFPagWrwAlpbN_Q#f*2+I`nE~?{ZBJ2!44gEGPUZSi -L&9)Pztfi<1;UD)}(O&E&OQ^Wy=o0SkD4&FWbCKQN-GQ`JY8BQ!=9X9U?hHQ6dj2TNH}EGm_vskDftO -T+)ila0`(>EF@-xZg=1j@*#r2OKdB^7+d^=qm(=oV6XI?R0RmJFT68n=TK3`p1(R6wI2l=tvVox3P44 -ZSO`-TX(7u4J2k7+3j=RBI81S^SSg=OJ*OJ8w-@{vzGk|WnRlEh{mr=)@n5zd@)v|m*6~}M$S4gSmg2B1P_``YUlkvJ4@5BGev}@GdSS?$OY@0E{!J5u1TzZWEnue$fQ57R3}shnMka2`)2jO{@*dj?mi1Qr(X^NHvdzg>U%wE6&UCsD( -AIPQE)hvReIaCINrgOnfjk~iMw3>;nxXyIP>CR~6RH7|a`jw|OYaY-)E-8}=fT9NfPwOINV%7-KiV-5 -#Z;Nsa;qQ(Wx%N^+*GFcS#g;%XShqx$6{fZ(Eta>R-qyGU=O9KQH000080Q-4XQvd(}00IC20000004 -M+e0B~t=FJE?LZe(wAFJow7a%5$6FKuOXVPs)+VJ}}_X>MtBUtcb8c>@4YO9KQH000080Q-4XQ%t2gx -WW=( -(2XAkzHcDccDzyL2ODoB#>NKY*s0V+Gt)3K8fmT;xHy$6gKfYIwlQ8ni06qgEfCKIhkG={~JhfBRaMrLUu$@HGs&q -GQq<8AR`s%B?Hl(m|R$&=7Dyj;@vqF!`_8^&2WfW(k1B|FjOj3*^o>3&06MzJzgqUtoqHT2@aEC`*2N -cwjV5JVEv`b}8f?w}-)#3%{qBQu-;e*qy?Q6Oh#Cd$;XvX12uR%z<8s6*yL4>c89Dy0P&zz@!LWI;2Lx&lTUHne$s -l1jhh=Umxdb{9TM^C7!346&0d*kEZ0TreZDk7TVdO*+a-0z3)i^+iqKwKD#XXrU$k@$<>t#_<!~Fm0qB*7phwx0*kduyq1m1$MX6Dndig9{m{7Be*x^#5_x4R!*kD3M -6LjQH-bJxez45d}Jp3UDm^ED3irm0`G~MW=Zo<3%|Yx>s{$-!7O2dI5HjRKL44i77h`O(pIc$`3+AlR -fh{+uT+*6q$y`;-8fdl!gIMl>V-Kqy+8Fr9qY+0F70WX|5PKG?(zPKqSQ$=6^R=8CLgGFdxw$^j)kg; -^EyCyWZ~yumHBZSYAcjB}tU(Lv1R6g3ef&1tsc0qbwkBL_1jECR8PS7cZeu(-Dyq>ZvJ1alnvLUuKzu)kwn%K0i-nrYp4gt&#rsQ+zdZFrtmNbIvT3mKvUG=4%t7SX-=fPvf)k;Eg-o!D?K -*)89$d&Go~wcyRgN46&lh89Pinhksw5$Sy4jeROd3Tt9+C%SP#BW{*Tw@#Q -{cUUx=qtBe@%=X<0TO**j_WGN)TVrxQEICOgq!MwQb}2Z<>}QSTemogdiMg+_-VwrpEOYvgMPVR2*UhaPGLGEGhQSNc>N$zRxS?+o6MQ$$ -lGWRO?I`<~`Huo;~KBwi64zek}T8>*;=wj!RDR2hQWg_FnC=1;QaNfbpsy9Ay{^z -uT(x+*lR4ul4Jg={d|sA^+lEdvtA^I$uC0E1p7aMY_I>w-0IU9={wOV(v;(7Iw>wT7)JYucKzu36Wu8 -`e$hmNjeLw(eNxth?4d>%R5CdT2edo?6eW=hll*K+fTfbJk1imG#`e1#uK3Siw -FV?S_W0}FsQ092%M&?B3WM()sk{QjM$(+rMWzJ>J|Laqz8XAjSUZ8`K&cU0$WNyH_<}&9a^i}Z3F?jK -3U}DY-cq-q+O5V6DHTh!6g}NSpJ!%2npMp6Xa_iRl#Mq(`qX3&+1Yl)&J+dAHz=Ff85?vHqmbM7C%E& -sEIStqXWckFEC#Ohn5-k1XasX6iX}wlyiGYKFUCf9bNkAs_bB?$Gz~grV2lQ_W -=Z#6T-9ln0_8V(r559V-&wOri@wR9{ymwz@LpTcu+s9pTnp0X*_0(;0a^WxMkeM)5ddr-T1up_`Ec-i -)Z@;{0weOtiXCGU>hrNvWEr=_k+Y?aW+sK=1U2lT3AiBQ2wSpoG>q$m(45YRddpuGN;WM^O||xykXunZ<({^9r*9AdC$CWJ} -@7ekIZ3n)I4RLHqV;pOg~P?OZDch`7s#N`(|?*SRRBSzVy(1VWoAU?u!jR?1xMLzM`CHJpWmQ?K-@ -S2(7DNE5&Ag6m_Rq{krIZh>p}EQ9-dG*o2Y)+6hx@WKhFYbGx#acosjx&+u#f0bJTR9>k~c7@ok>z8u ->Uo~>P!=)RQa>{Ud;Gb<~#_z{9jU5c*`HOx9MjeS$8v9$pcGPlY$_?y40n3gcJBg`1L!e!%gD9jv&Tb -~(LDy;f-i;^#5yvPduq7+|x(XP^RY}vYP`_5gh-xeo#=YcMKxex~h5&<(I^pS6GLWRJD-LeTMu2Gy{3xm*rc9?TRXMeX5s^BqVqx)`j~)l8NqM -%j^GvX`L~FZ{gec0e%Pu{5gJs)A%EPY+S_0@DLusqhQF-;q!P3&*0O`$x-klspf=^T&L -xSg;_0PJEy28W!q2R%h_4ToeOsyA2mT?hM__CRw)CdXih2E7*3p=Sk@uKB64%Ee`gS=#V;0=sQLwH7o -hS{v&+5b5^a~JryZo|o3q12-<8$qvbSGVdW1S^9F6I@(?NWusme$Kn0W+|K8aM-65rnK*I#we_szDO3 -ga-vjDNuiBjMHGo%B)L2LGApu?fRsCh0b67`JoC^Z@e-d+vYy-jxMX8q%J9r^eO!+wK@8WfeNo=yf -&WLo@p5yw*WZ6TAYJCE0R)WunwH;g*jH~^9Ed@A&`CG`-0gAzc~9dKo%5X-tLcjz5t1RCBlvs5O#6LP -W?6@?D4nX>F;*yAKUgACrdC34l;_c!2-f2x_k6HfUqY=diD3b{wXOK!zGyc2bmYbI`qL_H3`7-3RK$I -7>X~l+~jHtYRi&7HeehX{68u_ahIHg0~Q7>RvB=-P#bX4nzTmk>VQ*reZXl~g}_-?jlg;9;c`k(G80a -DK<2WgWhQBXKxT@T2xO*RMFQ7cWdb)dqsuHi`3P@+<+nOuo2qy%VmP-0Y}pBcw(M%I4ya|j#&gqN6ua -XoJGAf+O1tR2nN_$SB&|>RS$hBrS#Z9~&JI>&g`lRqU||d}Ok;pT1aAz2Z9cviPf+VT-Fe{Pp~FVn_! -wkr75)DR_5X#8!x#d)d_u&psb!u~)c%2^#s|aKn(QC1`*kRi2w`WBF@#_DF9H%tQF~M*BQKzo+5&|Es -sD~NVGs}D_k8ki>{9#$ExXy}TJD#mfBRmwkUra8NLzG&A -Eg(t$c4z~u^G{jtQA7H15dZVvr+%N&hd@E+exLk(+SdU5q;2|fFNv$Nca?3iA<*QJd@aYUOboru;3j0 -BmD^ukRvRwY+Xf5B{Rc)BQPF||S)WCWr4reI(X~enu)h+`ausl`F*m$w4n|svjS2<1OGHNB_>FPdsiSnNDDerWjuMaQY5g;;q{NquOHM7Nze>tKD5 -adG>$rGU==JitRPsphaJj{XKqf=>wKKHB>$fXofoik&3zqleE3mX*SlQpUj8*-m7WEe*VHMMXq$oXj9 -D{#R#Pmy?q9B2uv5JaH0`Q^0pERs4+Sf1>RqYc!5H{6wlnzBTq%ZV_#kXt#-WhiG?+c8_THiS~eK4~h1OXpf2ZglJER_KaxHiS~kMb3}Vdv -{yuXO|&;edrP!;M0-!P4@A?5mL}S-Bz=se2T6K}q>q#I36efZ(!(S@LeiroeTt+{lk^#qK18m6?Nzzj!Jx$UxOX|uCQ#S8Y^qrL7C-&fg_dzd6sZy`!ibt^_kg-v -G8Jo&o2kKT!I}gzTrtspw%A2mO$c8u7o(%h260dT@_<>#DdNZNFEVyF)jymI;ii$t=4IHI;84k2o;`n -4dVi&pI*ww1fIer;mp`Q6h=K*}J>(F7E<>8r{9rPCP&K>MPw^y9j#UX`4$ZK`P)fRLayU$lC8&l&l_9 -}4cLR{>ZsU|nllke1n+tD69J}e5%#>Ob8_@q@HX6amW(d#=HZ?`{9<7>ws$nB^IITn5!V3A%Bb~vY&3 -umf%Hr87TK4?&KK;Wos{Yoe<9z}vozawGG!aYj+iwjPC1k>3kJ+_bKcs9zTrXN=SXn!mtDMENukT*38 -U9I#396rg_ceZrxVb*O}y{38frcG-b!c13thht>ix^4Pl?T>yTLF2{3M?e4(68eYANhl6Mof=EfFS+z -5(9);B9aZL{s#Jmnl4*%Z2|&)axrM2zZ={3Y;S7_|L~1pEx=5H02`(?{Z0^o4{ -`PvaQbNB4sg}j&&3rV-omCO#}X)sm;~r9j$%ChE)Ely*dzq{F_yRYCV5`$U+P0FZsrif#>JHVZ2@4eF -r|v0ZaOGRJcVD}uka3{s6VUVa%CFCHrMyhf#4fqTj)~a -))rdwg-=6v=s2NEzujuzN9%chd(rj%q2+9MwIu%i@kL~x`tOh9v53$EKNk3+U+Vr3P)h>@6aWAK2mt$ -eR#S$EYt^#=008d*001BW003}la4%nWWo~3|axY_VY;SU5ZDB88UukY>bYEXCaCuFQv1-FW5JY=@#X< -@-@?lGEUBrq_5<(goQXY%4wwIi4#oj83{@yZ?Ak{FBdCUXU(vQlQtHG;8v@1qkHt -ANJFFqL}HB>Hb_^24zz7v*t6C;7OeEc5=GG_geQVF&rzZQVA{Zcs}D1QY-O00;p4c~(<{b0001RX>c!Jc4cm4Z*nhVZ)|UJVQpbAVQzD2E^v9ZR>6|mI1s)2D^xWHLoLhL;3SnvaRB4YkT8$ -}rY5QEmP=zdSQ{gIC9#CR-ZbEmBNAa9Jk*kqve -?N|MXM3lR#1y0hiDr5`au6elS}GWX)3OTDEOltAt6i&Ej1OWCI)oRUpZ5ww#bCp@K#V1c|a3gDNArU| -cc2!AP136GD8;I89~OS_0gVM5+Ad`Wkq5&%Uu7Vn(}}*j($?wbl(mMl^>|i<%NFACnD;{g>14f3>ao( -tlq4ZCj&YYE~5YSuw0lEuqRnU7c*{Rr|&wcnMHD!HE|8gGeO`41e2OyP!%?p<>vmmiesnbXwfoduQX9 -!SNfjmswMwB9xH;;4N$y40=szx6f%m*r(i-arjl{M}4zVN+q5Im(17gZ)H#aK%`2p)(u0(nF_;}gmhi -T^>sy50z~efi~=594ERd`DHN$vf<5i@G4B%=B}7Afw|xSEXIP0msbaX=gHDV$QpO`VQR| -$@nF8n*PUNUG(Tf`Jlk5e}(J5rlRAP1+{}n#LQ~P%ZnChst&II8ojzy4|iSygn7h4maDYL7Kg$gCLbj -uFyvyJ-tkET07lWjohx{O}OaN8Gq<;9-PnInx_yInuPosgNhJi&)g&q4$Dq_7KPaN~-heI$dxoobnb(##}I!%snohD1TPTdnrbx42kfLLf^sbwPL$ -^Egh#XZdCi~HfUjP$BA2!~Vscf_$Q{p>OD@j=wKveNYC=mqXk7A5cpJ1QY-O -00;p4c~(=)#1YeR3jhEWDF6T?0001RX>c!Jc4cm4Z*nhVZ)|UJVQpbAVQzD2bZ>WQZZk42aCxm-ZFAe -W5&rI9fu28zN+tQN(`lV*Qd!o;P8{dPcE(L@mjhFf#4|@F71F2L --PDNi;<{Wgqm;C6r)Qy%eDT -`8?X;potc!_7R32KY;p(3eceot+(vIVP7p2GbWtj)a25T&kzXI|Tj;Ni(q52mK(TLV&3$qwCw0CJ9SD -{`fNsWM8ZGdOh`vF9o4QC{f~xELagL;C{|C*&h%Z$S_{wBT8fnaY|^)Vo8#ezih~Yr-Vk4f}&EU;bSH -l=mZRgrywec@_EhViqA>!ISnQA`aSS>6~aj1&YVSTNhO;T$qCO=g*D~zHHU_Y -$7RM6Q?}*oIJtd6cW5btB)mQe!J$6)C+IWX2@Pb=>W@BCe+kR_9CP3k*w)542;Q;iZI`}ZZK; -@n1yd4b?vSOu^hZn5v*#MN&$Uwvavm$twU(__7U{j(eUV{gIP_Xk(O0b1wbgk+l@kSb#>Z~D;DMNfqQ -yCeVv|7u0Hu^pRaFbr#)Ddz?~x&e+yF{7f?prAUael@Y0A?wkQwbq8i`^!1~-jjrC~i2`@cih0RfwFaVCO@3pW3~G -f{K0H0Vf!eur-=lD1}GrMuy`SfZLM#`!vl*cuiDR)n0;EjcJ!A0RaX#GF41tZY!Xz{W5klgsOK60B$n -RfTD$7YtTD@Vn530tpq-$)q$a4K+w^45Z0+RDr~aCAakuit8ea17s5tKy?!3+JQ#+Q}}l -^gIK2w=_i3+!Ry`c?;pIo-#l36Ls8KTI5xF)VkaN -7BXMyodjq~|>+BwdHclWP*`T%3xQwP2H+a;eNcc==$^+rhWb;{Rkh>puWot1EmOK_m4z+BS|Sb_;A$1 -1T)bbnK?X3BYpbMKGasuc9o$FhY}I(#rT(M$VUExqZr`s#a|=5|907o$8V8tp{kpl$p)^r0ZBonGLBiR2z_ZR^%l|)s{9=Mhzs=+Kqb3i; -cT8_wcvjo%0jnVIqAca95NI0NiO(=VUK&6L?(aTbcxmA~!hSAHL&zUINADx2&)}n7jRa!Vv!-ej2zh1 -vNR~!bZa7Hno@7$`T6~LLIS0KEVBcPsJNXt;BJZ4lHlFSU~kvy*s3&dgG)6N`yjWijjDZ<3<5V+B9LgMJeQ0n4(QLXtF|GvKT51zI`l -O990*PfnpvH4GWPZq7t@zYoM$wj&P616P!@s9y_-*3ZdbO!lweNpz0MxyCDK-2CQ|6za}x=>|&;1C36 -za4Yu1k+Z@CqR4g}pS1AZMq168?b`6NO -ZWZNY!XO`_tt8?DA&P4qybjFs5H89U;KCUD~&2=QkY=Km@;b -PaZ6QntGSU1tzsh3V-&GSJGynSoc*lNRm!0c)ul*N|;V8C9x%%&Yn&6f{wqTS9-O1H{Q740r=Qu;M+ssMCylkvX9O*Q9Rxk+ -i)xv2us%}olU!c7%`4sKG~Rc@+Bu#KC{9!?SBR4fC-ZELyxHBD?~&#QMm0Odn-YtM?!8L<_rA -8fjtrP8r-tI#6K#L8*mt&Mq8-Y`Jk9;>fc*74$LcemaEh)urk_r2b_Hz3~rpzpc~2<{#jg4ez+el;MK -w%>K39`MF_bhkOTsG|0W%Mj0lP}F=ItDMW3Gd;E6qwz+aAUNCJj)PUZZG%0Zu}dAW+LKhh(JanzibK? -@u|bv!^~q|#IHpE>$E6AKxdGj_BDhc(x3L~abv?DOHhxKn@yZKRh -{atRGx%eaT4hCHm-|WmQW>=godtu)h7sep)`+TUx|HH?1uP#)w9SOcBp?KZ}nw(5eUq!-bRco?->n?{ -P_Zl;*IhpXgK-Co$Xw)f!-58Kurg}=+><=Eu`RY_{T+_HySP!jwppvWK?KjIA=tAz`xLMGg?TvL`lOM -#6%jdH7$+O1PKZ)D7wdws(39W6#SV{%+MUb*QE5R-NO!=BIwIM!s4iSYh`yU5Du2FAxZFNbw2-U~TA) -$|gN10@eCSbFqReY;rZ22xj^jc{#BxMnm(4LeGiF4AaCe1FHfKrwW$O-(;`ExuVQ@DcMhdG5sa|$=uF -qgDm1_Zy||Js{?9k9U3FzydX@5J1xn;N7bn7v$}b8y?yRGHi1s*~A8I~AFd5bz8N#t(0QP&|L|3c1#c32HX -#>_Q10;{O(im(aR~GSdHzB!nq|O%~JNE_jSBtRvHgbx>gC2y@5}Y-w|4E^v8`R85c5Fc7`xSB%udrmGOa773|RB%lH8Z~#=f&7?Irb~Sb=`S;k)m -y_+%ja1d18IRwadE*(Z)UhVMKN$F|Br^vl;S(bxu!ftLuNEo1zyAP>7c0RkjUf0ArN#s_B7C`Bt?%yF -P;h~1#LJ^11I*xGZo1u0NxL_KZ##>wbrcn(N=TX1+^7?miyLUY@2u%TeRHNLij)Pm(I!`wfeh}H#NLE -NohdpIJ03BgL*EB{d=LdOu)E73 -5;74Mg|2*An0|IZmb8Zq_o+LoK3S^XMfaIVu*LG^lj|vo|lmGpd~!S1v#?Ehko>NT~6yGWMw?!HLiLB -K||b``2?^d^gqCXx}OBx#5_858L*9e2H>!T!GMJt49o+%#%Ptl=h`5}{Rqq_A>F(>akaydqaTl9Rzr1 -!vwFUHLG(01Tkt0nG_@P4GkQC`bZNURnPht;4|b=7B381EbHvwnPILm-*S}A0)lqPCrY`R>R1Y9(jw1 -KFf=gSfz}1zcf6yisnh5rCk;Vtkq#qJL0J0|XQR000O8`*~JV2pbF$Pz3-092Ecn9RL6TaA|NaU -v_0~WN&gWV{dG4a$#*@FL!BfGcqo4dCgZ_Z=*&Oe)q2!CE6Oc1m*G;H4lm7bX|Gv$as~ksujX;2(*~7 -W`;Q4U*9tfhQVC2Nn2@AWQ8;5_8rb`j>(8_b}W911o?xeo`}4ZeDat`U`NhR&n|3o}cRDZ|7f@-J_eg`DDm7}1o|1eWk3N)5=8ceb4WfAqb4gKsnS+qv%g -xk*T)8Pd0r2rCU|hrsX_~I7t21b_L6Q6NmB^LA?=0=LDz@Wg={k2KpNjd!_qvf!!cEsyT5~QMDWSOS_ -M@KShV)vz*0yq&Taj;DPVi-Nfn-+&1E9p%9K-S@vTLeVPzwqUVxvjXSdUFqpBC`DyX7JF4$fXRZ$w?|A~k$n`hOH$z36(;)mi%$y%AsD -!a8srDDDS#7bi&}H_yZ-6C<2Eo-cr1w_xkxepTHY5f&4$w@MEpp{IMDy`JKIPQEc(eQDm!{F<_VpX~p -4#NJ*v%NQ5VhyHGmv$PAo5|Zit=)qYoItpEB==C(8{Mrg-& -MSXQ^G#kjBhPU$JFtWCiFDIX#g#Sd7Is!mBO0o;ec<;7pP2y41N%;AI28kdnrWwKHpScx&dw*jX= -TC8q)lf!m0s2LFVdSvq!nH}yy7DGC4^5vQ7@2(`u%BA<(Nv-N+M2pG1WB7U41RzCMO}kW -Vc*hWxPsZ{7hITEkIh^jVdUWkb;`zBF6ATNVoeX_NU>FV`8xEk#70z#q(X{6ovZi8LJK$&}}^@yt<60 -4DZ9oECOlNA204+FNQ%}^{-l=oCTq?84! -rY!P{FN47!iceyqjd!GoS6?fu?aq)VKXI1q%dsb{tr=1R{-HE!o4|6%GlcwYu0V}luwWeg15ir?1QY- -O00;p4c~(=RDAwlg1pojh82|tu0001RX>c!Jc4cm4Z*nhVZ)|UJVQpbAcWG{PWpZsUaCz+*+in_1^qs -Fb5>gBa-h!Q`Q6(bTm^e-ysK8O;C$yKrY_iOt+1X$!tKZ%;mtAI-1w#^7eJC&9Gjs0uaUFVWvEuRFOu -&0aeKGSXyz{w8$O=x{ol_6a#}{yV+)Ml{C6L4+p($xWN(fwE_4dk6l;-@KmifNA}j5k>Dg@mH{dkye+etp*a1OIlRZQzf6P;f`Vm!p1vpx_e)_ioPy`u)t}LeI15l<+G$WetrmzPT8FylHyNGa+~!WP+qF%an+Tk9TRqk< -hFbJee|1P`W;pz!p4da)Bp-EqJL6X@RJq0<#WR6c~>P2f={Mv&0uN)ureApCP8w2@zYSEm#r=80TyWV -n*`NDsG;|Zp`r77i$)w%={F58aXr&F`!I9n6Mx~>BJ9bz>qU;j;L=RIn4Xkg1s1%chTIA536H_aRI1w4AR`lS76(V>JomY6$uz~> -(wwUHxhMPAn_~s`W*~nLuF6t7VVR5_HCQBOn{>>YB*{s0Ib0p`|$XkJD{Ph5(6RR*Sl{ryLCy7y+QDl -wT#k&JWH+0{xFeFx}E3l!kyF1IMB3RNzMj`pBbZLP??7sCA+2PlUssl^YSkudlk#y?|`EzR3F5v1QIX -89am+NgAi&>tbW2aD23#G3s-nRImTMkiyO3-Ob(2MDrgf5^=)FY)BU;&RQlf?BF<>p(n>1s4p861)JEva-@^!nSZ##9 -GDfUB__~AOJ{T$G?a6L4FC@E+H|hGLQGsC&i#_577nfpJXlPnvVAGv23zo(rX4LpVqgv+X_xmg>hS-a -iy^N=f|7mEgGC3!q+Iv6gnUBK|SA!V$WP$@68(8}|@KX}m6IzbcvVAUpAomMgz*6XFYHve(?=!r}w_f -M`);%|)HNP%pixOJ)SPt%!IE6DSa|bu2qq$yLGR_GV$x81gF}+h8QBIEaCU+pBM+RkJip49jvtn1|W1 -)T1LDq)<%USA1u}ITgMJrQ{>;33UmI~we`xdNr`TJ>X*y&SK00pwvmqZhbs$u7zw7||$xNjcntqmFZk -#We{iG*WuOj1>~#)osQkm>q -8J)O{v|thQ9Ji&q#T@r;i+7{b>O6G=4$~k$pha3HVFD>TXf~Ix6f`}Y;Tt3r2!td_Qu#zO&XP?50>y1 -DS>oVx(*zckkh)+G9>;$X%&F%Nd|Q&dU1~toMwv@B?p%~xx)!gIO^4jBf}6ss0IF|3+X0bD3SC^cON% -jP(L~LUwZJWNXRWAU6IBM1q~y5cDqE}#%hIH?Lld0>z2R_l(ZI@TH}}zE51A)FTV`STjxDl|$%hZNI8 -titnizHYU#n!ZBJ~~|BtHF`mRj3eBrtU{%)}xSy*ver=5_=}t&;vN%XGVrC|k-YtviV-k)u>?Jo^77; -D@1FHSUttV|r|EPUUrOKc%)u4`*{G>=w9G)0`3#k;uW^a|&ROgHCg!*(Wh2Puc1v|DVw4%WSr{c^C7) -)yto-RXOeS_|c~Mz0GP%n|`dF9HPwuZS04%(Z2e>wf*GQ<~)c*EQ-go`nH@6aWA -K2mt$eR#RPX68&8P002b-0018V003}la4%nWWo~3|axY|Qb98KJVlQ7`X>MtBUtcb8d4*BIPQx$^z2_ -?|?XVWHYHSy%v6{ki>WefCo#I1Py@;C%Nnu|O>;7g%*88; -}AqaO3p)an}3)E`KJ&hyI<*8};!`UOx+0|XQR000O8`*~JVEYrOXU@HIs7oq?F9RL6TaA|NaUv_0~WN -&gWWNCABY-wUIV{dJ6VRSBVdF_4ecH2gh=zl#$FEw6*4h38GOg^;3>^QPK(T-zzEjh`qqG2Ea6cK>{1 -Avk_-kh_qv9EWZS65ZnS64Uo#lgb^5tm6;+#HEjGduVkf7#pL+ZV^> -a$RLN^F{>6Bk|<%;nM^7=QHtbRzz!Y9{rRpLumFgi_@Y`6Va5SnWy6Qx|x@SI4@_-eN?65MY$@HsL9| -r_>fj2g7J!`%C1*U3Eyt2G+m@cgRsuiRJ=SrK6!P15;pG|5fzCzK6`cX=Jcz#7iVwI@keO4H>=8pm`- -P_W>uxrsmK<~vV!KATB_)NXbxlgCpt1NG_0_X{Ca^t~N6oL)!qEpiZZ!t%~;ZPi0onhkBW3jeJa>EnsiWK{l5cLd^|!jcNt3Kf<3|11-xY;1nIh6@#pJ-%R -_878d^5%|&Lwb@QuC$(o?V+iy<6nfGIoCa*HyYA>G>Hyw1oZo;+vOp5$KWBFZMW#9ONZy)*utSkgDh& -G{bV$0H5{e7IN04Ra2I^UbPu4)rJ=vf7;%57Ugx?cA&2;D4?s;=nixT=;^E$QH7dW%s>ej<^5P>g@_| -ul#M9TG^s({>Q#jd!Y=T%E*Gh|kJb{{bOrNWR^^RSf&4SdKdlLT9L&qQF=TvS3EndbW5SJERm^R@*2Yw@ -I~W)BXBnBTt$(c+Ho$zOGM0ZIq?26=c2u+3Hbp~Zwv}vQB)H$?45%nm12Q6r1+E2?W7=XN -H!m$ph^;vj#Fb^TwhYVIBp}EmsY_k=nzNc1fIws0;8160sK<8C1B6>Hy2a$`TDyb)j1{AOY-(UwcQ&Akl5+EB_iZ -?Hg#ixf){s3zmtr$cgpN78=pSHRh1>@+^BO+s%rOhlXtD8s2hey>c#;*Ia=K=RLhBPM8pc1{jGM&AmY -(!GnCPu387p+C+?iKYc>d~Kp!gn%f%pW*T~;8~Bgq;t836M3vn14tuG+9mzwbW>$A5npNmb0q); -gSf=ZchYSA+4bmG9W>9Hp@^G7UF)M#d8B7CJVG6#Pg_%V$@gYUsb>WP>CXsuG3tU(6VCT!9FO{2PoA1 -I5f$lF)*>mJJgsX7oR(bcchmVi3_-+U`QB&EJQ%(!4i3QU1*aFmP4Sa*p!sZvY}GQT@?k>L)k2=V={B%QR3V9+{-CJ3jvziq?IGz9fo;W__VqE` -8;(NW+^@+~40n{E*SX!C2-IMEuq8hld!xpge#Qw$@>kBt%5k@TfbLVAUXyHw7qCVQHDx!zx*Q;_}k=; -b;d0v6G1ew6dcc!1vh@Ro3u_a2TSK9U!C;|Mc%ev`69TpI@E5IXQlIe$vTNKw~57L9UGqLvQ;V0cybO1kQWsVv(8G|1Te$i`WA}p9%K1MYSt_b){jH5Y7OLf=8;F=-hlYo^Jvnb -0~KxJ_Tq4x@a)gTUt@N8AkT6bt(;{>;);|jdtn(-@U|O@W!#9~Gh{V8x*ZCWujj -o9g2o%3z1{eZKQVb%^NWpJ|a(wdUVtV%a;`Hp*vzG{net2{8uWwJ^oID@2A}GB`$0@5ptxRY-C=9_6G -~`d9GoR(nD`*S?9T>U-S$3PQ$KnpjLdlQF@#cV@(1HvNSb4|P$Oeu>OTfH>Zt;V-*?FX9r4};*8@cRN -tz1QiYG2M6rK~LSHR$RTc#@zRC(8ulZG?TbHXAiAc*M8F -Mi|@bxzU$-Tg5{+_wjVsI7)2JsMDnOQShnC6D`*cM;tJMW=xt-LnH@Be7C(Bj{YP52q6 -R2Y6O1$p>MT!oZEq3)DL_sagda0T@0mjq*g50>IZeV@dFXGVH+AfwjYaV8K4I|){_{GMjg+{L>`F#TV -Vg7+Jdvf03xi7{zD%&u!Mj;RSAA91_7U~OfW>qAEQ73q}_IA!G0K&fY5}XaRh -L|A%cOdnL?ptDa)i)>!FWVsN&(FjS=6+zE#i|3XWl|@LRo-NP6ff$UOofcbl%e(lr8K}4yr5<}4cH}r -`0(~Vs%~m`s{|f~(`k~$&@@~v6CiaUTXu6Dlw(x%;z;B}2(ypPj2M%l%B{|)GBJR-6X+mbfMc)#2W3* -$;fv|nU&Mb1`1Cz|8dJV+f_Yq9{~x -g2nEl|2t*ji%#i79-AV2ZVuj$yWSC`%^Hq{cLSfq~&0%5QrBd1qpfT=YL9}adg+jUTQ5{#=vX#herY8 -(-IWSoyqmI{Sb3!k5k6oASfF?dJaoKAWGQ|C1p1>-*qw>OJj{w3!@9@gwugZ=V -}ztS`Tl|yX^QNN5TQ~o<32snaPnhMCwHkAA{eFwfP#*D-g7&QSW^pF^#F$V_?Zi|q5qaDXmB}!n;i+Lm -$S|>W7W?gQ=3~`~TBqWeB;j|XaQ2jU}=K-lLxeXtE(L$DUWC!rBtfyW7%xy0|A^6>KT0q_;Q6W>1O7d -&5E>}P@bSaq@7spqxp*(@x7Ts{J`$_m2XNHhc^!?L4 -u3GzBw&eM;E2;p2qwp0_TYjZ#vUyZTO;ZKN?+{B(rIJ&koxE!t1m!P#M#Rw -m8rY2tGJ8CGtY+KAtH*ZwM?Gy~5NvGF4GsJ*cfGYNU*u;4W%fMCXRy_tRSJHBVV+srq%9s -pbLR$)?sWBjoZeTtRu1=q?)i`~3^yVf8e|v -Nohn!53n*-EAoa%xK{%(&4ds(5=B;*IX&|A$;g7Jk_w;VPf!@XzO>PKdO;1gQ5}v8%K)EJm>v3h^Ve( -T_3C9Y1vRCU?#<&lB9xB5em=8y1$*jsJR@J&7b-vAMrMbdc@zX878w$%?ULS>0qR{Bv^f5KzYym1+WIHG|D+Iq#>1J26Gq4KXja^@HG@KDAN -VYLrM#pRdBoFT84WZBfc%NcR0d!td3+Nf`~7(&9xZdk65brfBy5oC_I7q6T=e;&&oEpi464Ei+q|##S -QA!;P~wM$@$5}$W5$BD88n&!;>Z8mEsiwVNLv^{mIm+1HEQ*R6~4HRYh#7uW0V1O6^E#S;)c80- -mcvYlzWNFoOcX|txPKs3=ladqivTts|H}{*AP$m)r4wfCxX3Dba~=tNUDC?pvt(N-1dYnmdUG~Fp9?* -eWW{9N|I0apdN?^y)GpFA)YOXz@uE*nyZkuq2fD{ZA#NlM*j)(<1wuc3zXTfgz^ -~^A9#BIQ3ZP*uOgaX8QKx#pj&LS}jeJRVkMao@D8?N}av*0b_7^G`aerei&RH{BR#$eKHytvM`0kKE* -&O%4vQjVS62?WFB2l13@Y;x!c;!HHSAgZ-LJ(Albc~&=EL9`YK9sCz8#i8>|r0y_QnNz -Vyl)yA>?3kvtJ?2dLAY2cHSxbN~kIG%cGd#mq==K^O>jE5EZbxGwRUww?^5F54PoE;iXV9OL@?Yz=U8(Ih}^K -sr0(deZZ^)IuK(@lz;~>bOAO2ZCyI6hnnETa^>U`!gN|z)4C~_fun~yGsHi+TGR}kU|!Db9H5;xxGp< -!qXFZ3IJD#USym)dmY=}UUA_41i|10?)B>y~I`b!)EaOaRX|gD`^a1UZM~qtc?7_aSAJs>cSkIwAELX -5vo}C|`o|5m46`ZoFgGTlM!>VJ&0EIxe`UA0?%6NT3-;_VpIhbb<`7NL8%u}LL8{&2S_innu -?}<}>X%{2RdWG68f!@16e?>2Ll0tH_lzHQn -&q_j=jNHb=!K%0|%KLWJa9MfB_UIXScSJV8(;{*t-v2UCVxnV)jc&T1i1@dQ9A&dsiF!-vh-I9^`UkB -v`>Mo#6;tGZyD_FE=1>!HS(Vp$x{&AThu(hutl0q3U=M?KIpp64>7joIB{kY7Ff^HPpZ}|cO+VyFV&W -Jr(}$-PrWdY)DK`OZn9+-B<&+l6Vd($Mr$_eYpKDKT{H4O7uKq(4+Mp3s8`SN*=%F47KOx{ZUokr~!m;dzu5-$cug -NI-O8eZw2X0Bxz)pk?8xBUV`&cOq12yaGbpp>bZ)ncb=V6XybZNGw;E+Xr<{PC3-cFbqR?4`4X7wFmk -`6cF+eliQT?z<`YoZrj`wgC%7bwa8oGFox`m=PCq>cq_^iPt`C1CVKTZZ_UoKnpUf#ka<|zL+`YrQa? -Te?c0`9e^favvV_}=DQiPpKSA$?HFlxX94#7Kx2iSG{OaETMf>qEgnxLiuhfF6S;1Nqnz^?FN$t}RFmcuc)g&zNoIU~W_tkK4y -vl7i+aHS#xTfZ5r3f8xHrrQa+BcX1qjIlJJ76E7suGCpDfO-V#1}0~g8Dh~F*jv08Mqtc^RTW%kK)Zr ->S$c1zD=;HR*B0fpSF7Msh6b#4#aSKBJcZaiYB&o8UgL6goo6v+c48yzq6&+r71#7eqUuxS}QWuoS;k6JZ -0NN^YO=4hRJan@RZqYZAp=(o4p-o070s(U^4cBYb)8tx6^4Yb|a{hLR&xt6Dq2OlUf -qG8HS~P?_{+W@;V$Gn;!G?Ps^J9Lex(Y;vG6!`N&~%XFm+ybMYTNCLD8G-^Fg%D&8r4*7he)}^kqH7# -A5@dREtkKfYj*KTk2*kLCE%gZ4M5Q5N|JD4mz+jdiTBzHeU}MAxr8yUtmpf<7^I>eN^2(@54n0+dXh- -=Zji!WikhY$1-e$igOe;SYH5iCuyLzbORgMPU6X2}GlCK3hG&?xGCJGD#iCG6)Uld^TjuZJ!o+EfKK@ -3Lq3Z845Td+|bwD{)%QWP}qH%tx88{lr30CO}Cna^)JgVpFhz3?5moPb9z|fAHZ7V>+2E8M@&}211y- -Ap5y-cwVl!&lwT2k483}!vwh$jN{b^nv=-Hsf{i%9( -9gt^23UYaD-#Pt^!tHQbr9!xXc)7+R7m>JQ)e|iR`yS8P+7Gvl^(77R! -;Z|HO*FJg>`A<=CQx5sG6_!CSSM}Tp3v5yt(uYO<%Lohl>n-!!FheW3(VZYknj-s76X@j>FWS0m@8l^{Um2tn16IltZs$S-r) -P@=_1Z<~)*Zu_Z0@&fj(H!#C(db6zAQ7mE@e%R>|{Tp>QD$6g0u0i!Z+!nyi3ik*of5p0>mJz3|_&)5 -pOKy&__}WlWu_k7s_GhrcEqsO)3<WHp6ha9(F -e3Y+(JH9ZtImDMS2ex*4M9TnHU{!AyN@^Vh#8(l#%>CV@^$eIlFfkd8g8ATUmo=NMh=n33Jz7w_M{SG -R@XPE-ie3BDr7;c>Zm1ny(DOzTH~`0Ue9zj*pc1l1R#y?ybODo6%j#TYmk$;1c67S)P^XrYT-e?NwQ{ -(%2{I;NUe)F#XIa*cN+zviff8^c2|uyj_dUS$pI>#T^&YKi$4JXr*u0qwa*)Jlf%VUHX)bFPeGQt5Oh -<&5>Wt?6O{m8=$v^>jCYJ=jd6YhXhU9Vl;h*Ck&OKd;i8RUTDsasr_+*hb)8cU7hX_8$u$QsopPk%J52@eG!k)8`|0Qg&s-GXilupKU)v?`M{%Bms|~9k>05KQ3*ZNIb -#9FZHLzxhHVn`F1Q`oqT^W7JoVUx2te- -hO9jglWfV9NlBFw#-tC0IL~g12yakF1Rd@=nzqYnKHdhb!z)L1wP1Vu5h%oR0Y4?q^?fP{5986{L2E5 -hQBZZAH5FpGImIS#3n5phd(8=ip?J{3-Jz63%5_zicCu=RYBHxg?gR -6Gmsf5x`rB_Mv`|MttocIk%Oi|kF7e}7@V{u!ulfeY^*9mlf9X0})2TdtXA9cyG1i#mMKzdEa=>Wsif -oyfPlIeM)+6!9$>d8>CYRaOWNot_4^?x@mqTI-O*m&!%b#fNan;xj9&e6Ry=2jo1SM+9zBwi@}*sm)fh!Kh&IgXXoAhRnUlk?Kw ->3g#%CLd;b=GTJQ3D&U0f51_K&L~W_iU1>?`Y%!QrFcJn<8-zZ^)XkVb7U?_YiN!b-+x7Hij_dy2M)) -fWgYL6?j+YrEpG)RY!naIDj!V8-e(}JllFUO}=72Fd-x;AQ8nIP(g{<*zBx-m0q$~Y$0D63Q<<77|b6 -%&(rdb+oQJA-g>4&TR)x_>tyLXAaEAJ6h|`#uBMEgVz1|wK1vY36`~Woi!k|PIMtxf?LqGqj8&gOQd!J%N7{A -*kfj1!!PWG=?gJ|FE3ADoGBM;#x?|fK0pRD>@TdRbc6VI=Zh=TyjILfCE13P(t%DMVhgt=%H5Gjr*AN -ZO4mufx3NuYHT{z2XixLGKXG9qci1y20ZS5o9x_*yP~AUR1QV)T7ww8Lc1{V^M4g3!qHA$s^jE_p -Ds0xO#?BYRw!-ZKC!Lb=mIt${R10!gN$ODd4`L`Jpv(M1DI-tf(5dSt^BZ1lqP3f|HK -z)NQIKo2}pySn0p%+d?TP@K--0o{s2$t-p5cuW^enR|wMawkXV<=>+2Izd`@pv@_1qSiL64;NkwO}5( -r^o*?Bi9CR$=Tpj+*McUXz$OPvTlb$9>NZq7nKb5CD^Wd_-km*aQIybLsC8^Y{g7cwg4ZA9#CPOebkC;?It4VY?`#Jm`r@UP(2AR$FAGw@u?-p%l%g)lXAd@e -m}Z3EE8_#*B6kS=s_RHoJhyNIe=k_o<3H`u(}tm?;9zrK(mxV;=FSERG=#wtc=4;&*Gox{|0xoN# -D3pyOa`_it?l_jBi`xo#rPxGP((`q!Y1vEGRY5@ECPm=Hs5Rc6ukBDT}nu1DvkvlyRmzS+)d}aI6jzI=nOm_>CHK?RpFRKCl4mmZm_VYbfM#E)A&6%l=l<8KzQsppaECTj -ZN8`4$S0(lJ+p!K46NN+r3S&e)=c{H_>19ti=P9uKHTA1}!px^FhQqVxZL#V3CA4K#i8Y1Y{Z8Jf;UX -}4>#_w6jiihf)1O!17~^ie2za}|yvdv}*<#u7L?@QRdW46yiJG2g-Sj&-O4WAmdHh+cShB`mS3x#wch -n{m+yOo=7t5&1YRtZrKm_(%&z{pOLa17x#tVoW_xPR6U)#O)HnBcHpn%{muLQH*Dd;-K#zO1AWILA^M -UEyD{K%x>EgD08p})>Pt^C1DV&1Rn5Z0x9k#3~Cx<^*3a6R-+*)giuz;iWpdrxK*2YuGbh~bf?z8uW} -(^cxZTFGg1Koz>|ipq);-b%%`n2dwf&4n2Q-<(t>jyH7%zxU^D6h3AO4eJxOV8YG4xU{uEec1FSYZNj -@Qtbn%L%qBx>5?RckzJIQmp3<9amrUy>I=rAI^`FvuP9qYx-Uy)h13U}9X@@2^6KLB#p%f#_l3om(ZS -!QSNQMagD<964}T|L<7K{@%NW#pf%yV}wUFhzy^JDLb*G2#P?GU(so^VnMFL-ZqAp|5@0U9g4=VIU47 -ie{bRB?tjWrhQ9P8uc_(~uZn&GmM=MeP^u2GpJ_-;BmoW9JmR@YC;z5X@5$C7R~mxtBqA}K7a7pBl__9FErx>{~xtM~L7 -L#mgdRjC?X12o=8ldn18+n0~1PDv6?U8_U4nn&`L%`A`Ykwh^s^F-~AM{xu8vYme09`Et2`51SLV`v@ -w)I9W+;0*?YXXdQ|rY)BzS@Gs4M=5VV<7RDZ_&Yd@+6Y8LbBdwF&V36L5kua_Xzlhoxii#@9*}%{QtY*|AyR|!~6c-Ob_2boaei+>bKy*cKYu``(CheU-Q?hK6VI)-{~f&El}-P{d9cF*CHO((jN&d)dj8+CMhVhyw@Cj# -VVu)&=f%U^b;HHY?`phYoz^<%6qLwg)hfwF7v|FK|KPXJR=G}m*;lRVFUM!EzQtGYzJ7Lbaq{MsbLsW -)ufadR{v%$epS}AMW6rBjMn{hx;g5V)>&x)r=y&F+%kR$KJnsdD7ia%)|H%mcH}-&^pY;#_L+I~w`s( -?~_x-&tjb1-oIm4a)`Ecg;D1k(U(96Ecd1xpH* ->JFQPHUID{2iFzsZYU=%9~Dw*CwKLOYb0ZO>xPLYH$2yL}(&c@<45v9JZn!6V^mVY*fm%Nn2 -Cyi~{4SLUxV0^&_~YLjZmpwee0e1;nOfeyqw@qZAwxGZtC-iulCDw=Hx4JS39jY4;fA48xj=cn3BS)C -P21xg&YGyZ5jmqN8HSfOCEX1k^VRS;h+jz&7I@!dh%bA`_TFY2F9uUssY|5fmNC6z-rH%%Vq5jM%WOX -i7j=6Ywy_>=Iz*`3j74)T_(dJ+YeX_8xShwmrGt#6wXPz1g8y;jw3V^+P}gIl3xfd!?oR8$Q5fyW9#17GX{~f -fh^<8>yY6pb&img}NW1JQCZgM~f4veiX~H0UHBAP>y3YpZUfu2jXz}MBv|QAbhp(ege$sE0lmzIe0gvY`0r6b^rGx=Uv8tPLu_^8 -a9innGsZrh_@1ek3b`S8P$sB8RXA!*}z2m&bj4MXM7#|J;xAt8wApMHT*duGPVxscIV;cr)@Y)$D9Qx -S0FOKO|>F9g&?tQy$Z=Z?e_p{2Kmf#XA-=v3ntBxCCP^zjQ?2?1q(N#~1Fb@2$e#L7maTYxXQ|VoIx- -Ohr_7{fZUo8cT2OgUcyrSaEYe)5z6wziA`?ksQtXa2LpvyPhsKNoZqgWw8X0#(AUgfhzhc|s%&l3Fq3 -s6e~1QY-O00;p4c~(<~Kx6#)F8}~@#{d8y0001RX>c!Jc4cm4Z*nhWX>)XJX<{#AVRT_)VRL0JaCz;0 -YjYdPk>Gd!inbJr251nPw;jBrZ)u4$t605G(s*WrqX3&gf$U+U8`Iq&3H$cHUq0&nXn>lr&yHJ!JtEL -mSyfqC`Krw9$Jx>0Q8sTDWqoy=ZTjWWAMuxi#|MwIXU%%smRHw(HhMPBzWD6(FOJ|pf5<*ui|p;Tzi# -U6eY5QEidJObHk*1;^kq{|q33%cvX?KOoxOg4Htp~Gtf&{+vp26lynFHI??1eG_a1+Qb_b6hJ$ifgHv -94q-(=-#U5S;bdjKnG1cjjB&i -~GJRFV!#CMR#p|t(vPVK!W<(=$`AQEAQ7uf31G#w))v$%QgQcCp%Shd|q}q*4h1BtZ_k;tQB3eY3HJo -E$7V&fU5TI`xXX&@kTzp6>SH-)YB``&${_F;AfJ3Uw4}|{wfxK;Z|pD+qAOdO7sQnL!o+(UdbOO*{ib -;r_WD6oX);|@$&5T>8rCz_QSuQoxPle4-T^U=jGXVr_cT~>@b$2Z~C&*Bd?3L6SG$QvMF2I*-17pU=f -Six^32??YEPR+tkHM%z#E_a@o=GTTv`#;vQjllkBdA%{O14z5mNQcrIG{^9Ov`jt>qFX0xKIfO1Z<^B -lLNEiX5?9r+~7{|wafqV9TVAr|gKd;C1nCd<#Ru3ps3W}vJ6=q*rC{t@PcgeF>;lBBpP?CwkYZ8T$4% -wXzgKfZnQ?!$YHVrZYQw+jGxBv(ECxq|@@p1pbX>h$&VXiK2672s7hJ$U}&{Re9I9vY4E7q8!cIDPr@ ->|GwEj{M!(vp4Ua=k(YA{{HO!hqLFozc=vc{hROKJv)03?fgkY&-ed+`VP8{dGHsnzkM@1`|$(LaY|D -x0fR8JgLzeST{e4$6f$Zqe-`t8d`x2@E5LtGvyPYqQ&F}m)@#6Ch8XLJ!x(@W9@j4Gn`~k2K<9v4h$Z -lrvM&4CY}AQrDTlM$;O7OjD2uA>1PqduNDTE{Opyx$-z&SUZrT;l$2Mzfku94x6EMHb-}-43IF38T6# -zp1_FwMLxQ58jSE2^42d5vNeGA-A9O&iDD^avVYk7tZvi@4)Z~_qf9+6n~h^(^jvXYmZ!7_WXW71O|a -2(D%{$yb4#bQAdpU{parhmFvWQDV(IBUHeGuva}E5(Xh9+SB8`@nGVwUtY1%gC=A;Lo*L)v-hS`sZ`q -<0C8#zl7}wU4U&^Yq6>N5p}r{zyi-dj;BD%qF&ICV|^BU3W}eKcVZ2a1-L7m0o=~4S-M2eB#ZnD2o&1 -T%0~TmUCxPWL7LIr$J62CF`=F(uYt-Z0CQe#Km?1cs}tJE6u|-4?wx~^k)+^hOtOZABh7p03wxD!TD3k2As5bUViqD@~F*N^?QmjVdL?@A9$DZimr#l5mM=ZAleY%C -C>+{|~QZUrGL@@iOpIdm^mZDnB94a50+<#(oTMK~G^b`)&6u^(@mn5gK!931>5Lqn%p};9%cJmuNBX= -Ud6)XoCS*-zbxy`J)WXoth#I@rdLyb@LXJ$Mg8>q%T3*HU}szS)^ -}q>Fen#bSlN)atE#MT;FMpNpkRPX0w=w}mf5nb1o9Nx7*XGrZBwJhJ+-h66F))zX>9>=96tL=A-62+1 -u3g~y;J^a2AyZveni5<=>+JK{h~~TC>P8zK#Fi4;5gio1{vjf02ldko58&4k7?P>5K>WsVzKWaC?o+B -WI>JEsa}{n7AbOd-=bimJ|TF2y3~9eG_z;jP7S_A9fC~GS^|o&R}>jE@)T5v?yiI#u{G^+d=M~dyApW -73`vJoXt0s?w*(`fhZ>WSPs+ah7=?jbr0iY24#aJ^xHuBi>SUyXQ1)1sRJ>lDRmG#;al!C!VG7BHcF5F^AMXt -5{U7t9Y5wI@02I(Zy1Y%fn2;&O8}%E{4`J?X&Oc84|%iMTZkR*(DZqDK)6Kw#BzT>$2#lvuI|EPX~Bvrc2yMB^LmK_IOsgaY)TM}^f%pq^Bg%hrN -Bswzdc9DtXdvOUPanq*P`R)Sn?1Rw#h#p<_8b0L5E28J)_6%Il;(M|C6dJ9T`z1TzoK-A;kh-!fWTsI -3%tT-z?l`y>)MZsuZbrW(!fd%@u&oHOyA5t72}^8L@k4Jj2GUM#72UvxS~4AT4-me00zXCoLaai -X~2-oJjbHL@=57_)WR7>&@yCxQyB4W4&VPag2(hSv5Ln&;eC1igw}pZV3M6RxStq^)WJ|49+9RNw=~1{x5 -H2HoGdF>r+CHsWFvYaIkR5_DTZFvog;qPrI8a7QF5bgsZIF4sfIf)Dq^H7`Wn=`|CU>7kEFjVT|QY*c -s0+Do$~tXmDRzv^e7IO$p?@0&aUBiO}B24rvemNeSo0V{py&SCyI&VHZSDz%YYXl3thp1y6H+j2p^>W -pr#U?D7o(EZaTWSLyMSu3%mNIGAGDwvuk``eg%p|*gG*ONwpmV#|Dp)wRBIB-4mznmzshL -YUXk6#;9x-X?;LP=yFrfWt(cNsGV9b%NAAz<~cAM*3rmkTQ}LNX$1=_TN2Sak;{tvHE;>BP>$Sdy5Z; -)jEf6&Nq{(mpLjfU-3A0dOyrY6iViRhh*85v@h4Y#Lf(=n_o|vZVY|=%?x#ZCgxrX2cEXd0Q;?o}aPy -%)uD$wcXA9RW&3pT3#iymG`6sa?)B@QAUj -u({pfM>2u51BejGhVUOmv^g2}oVlh-o-Zg{8HYx+R>dvgfOeOF?%b48Q6HUpMj+yI*(sL3Nl5=lpAED -7%=?k+SP>fS&IEl;S`awi=`wQXS5?imDZp!f0ypZb(XSz+^cmbX#igpmrqVB47@nAfXCR8Z_T#;#UF+ -^Ll&s_UOy6z6QRrm)<$3NZIrO1I(|gwU#U#x&!hZFmyMou<2-wkja*y -FwI8Jp`WM_x3T%m-vH~yT{Ro>;Ue-VYr988ZwQWz@3H%?1-qVc|Owbr#J~s_DK#!HuZNfF2jsg!0@aG -ju!w+q7TcRf)-TVUd^e?~4?h5;^7MahY5nY}49=z+~RxmWFPWJL1G@DMR0ncnG&i98u`x0`gi -MNinsb-znBCBCc7DNs4xtcjV7^12Bf3Jc&4ED?}Bx$vcoCK|@6sM#|@TqL+?<1j6;n4x306rIE#iixW -RFBNd0ArUUIM}|F%F$FS{Ao0{-C`H?s^G#K>8h9Yb^b-z*P~UJzI#IOqYklE`5fFZ7mNg(tO-KZbY&8 -$^4v-{d3MGe=QmtcadpIL)6(^D5Z$78fOTzoQ@F*Z7YR^@p@Ip!Jmno?iGz6kZpE!BiSm%3ygB0;{OR -#s?ZDx?$Xv9z|h7N;5GHxC?9407`$c1FZ47Kde!^^CDMH*)%wYn>|r1-5iUC&~PF17WA34A4@$U7op` -6phjOFtye0Ms9t!e-8~!)mhh$K|F3OQkP?#V-U#NQrvB)v;sty50*4U_;L>`M7bMo4UE9V6RKT#9A=G -BLtXP07vLMB`~=2JiV+~_683WlRWljhZ>yd>}WRm7?9jFl^|cLqZ=yh;!(c1&@q)@it5WU -W!Tf6J*}H4ng|$n}SLZjymGHEO%Pnf*tvyfNNmSBZEdyhq3@XM`y}C&Jmzw0m0wsN3qM$G%(s59hTS; -9PUce-}IZ!7%mrHZxZK@rJJh84I)t_zyX<(~|Q^%`FEy(({LinT^j?W)6`NDZRf>nsRD^U_;6xZFBU1 -K@zsK3&yt?_(Af!bBgoE&YQW=_u*<(23HKLI@w_kCM*@)7_37~>pEjqRyD<9wrQ&_h%~V#-*HSUJ^D8lu%t;&3ReI!&0b4WkXYgNLM5-SWe)`4}NW*Gknz;tAnvDepCvF*&A3o3= -$a;s4%fIYhg29k(!Qa*3nF4JT_aVU12T8h%?hnEluO0|Nvt&u)<05{eI%{k#Jb -m&rCaI^*Vc(9Sd38L3pc6@VWU -RLhmSwxVsWj#;~P|9h2mD@ByF~vS -`8l1rxTFZ?>p!13hTAzk2q-4~r;g$DO%JgUx1TzK-PMl7W&5|YTbm=23LPo7 -cuL*)YY8YDEjJY?UsX3I6IDYE^cQ%>s*BYKwM+tJ^C@!0QEOmGA^$SEs+z-u>;l#$Q=zHvtU*_>9vS^ -8&s28AZV{YM#W5~TXUCAzald0dd=1JLJ8JuDg$FZF<6^NfI$x-^z^awnMmesQ3&B=mO~II=SR`Gwl5K)WuQC2DpEsL|qj#vAlCn=)K=-CoIY?w<3`c4?*u -#(1KjHT&$nz37u75R=L=bx^4m?Q8Ere<-V!+o<53F-1uPTh`f*|=hgpmpeadk7uTlHsiQEX%*PnZZd}x5LR~%B7Q`@Y;Y@-m3JSg6}~*_B4q?QMXk_5B2WTHp^vs -j~YyN{-pbe`is}z9H{UzPIt}&W$}O79)?V+M^Cyx4QWUd3DFMsn}Oz_tV*(MQyg0e&9NQ*v1HA+Bh42 -TNrDpaYWg*v0mfz;2L?{F59B2Pjssf)4{hVh{iu<=8#Chp(bGcqFMXv6Sc;LL8f>;EqUu_r!dm!lae= -6VS&SIP%C$v5c{IwK9OVchh3JN6%h5N@h*8E!V$H54tU6hk80u%)UAn9JEl@+l1Bxl%!w2*kkOf|)U` -zI{-WAH|QmO)ZVv$tPFhdn+(85AU22mJGWPVXbg!7}lBIVgj<|>aOL7Qq4bj2zO#&i1_50Fd8Bjs9BY -56jqhy%2J4;aqbTp{jq8%<=+AL{T!7uOGUBc0YeyhzeyW?vHTUs_5#Rwtv7?zMH6FP-vBOs6UJOjA?~ -ADQmxIrwVn14N8CA%%9vBJZKRI*~U3&{&6LV%#RimFPI}i!kuST9>*3XXl7W+1ES+*d(5SrXr01?5mL -M=co=$z+^nqrDw>~(K9&^iVcz))Sl!)kTK;`O?=v0={lj3NsAPvE}z2c-MbupIwu*EGL!xg9e;-|>1) -YJnkBo&xic$`&czM2Dypld1$l(wyh}4Xb>|XZ>KH#N(H8r74$UC&h86@Md71-O>f0(TwLVcb@6$AaghYZO}_ZzD{LgcefdZJ2LHilUw@;Xt -rlO)c;#VmUw{4uM)Y!=m||x@m)12GA3A=NK40bzUY^q(A(HAaq~NKHVimt)rnbO{lvP=?<#s7FiYM8t -=U?xD$GO1_jz~}uT%dGxOb&{8dr4wbjscR>!4oU)!El+EJ!X;*rlPYV3e;-oiq7loPhbHJJF -iHH*tM!z`u`(FG_#B<)zonxRA~iUHQdQ%r3wAO4L|RXT&YW(-w(&J-P(c0~|rm@}irUWhAWnN%y293x -Vf4k>eQcCa&2eS-q#`aE$?rd=8O|A0{NGk%QXd%pMSer7t5JQ1M@|!OurV3viWhy5RUMnH`4(RxVK0K -)s+>H+X!^K}o<^vO~qNFAKfQ9MfkzF!aMSWIeNoHf{k+7dQ%s-MOk6AD`APrD}W74k%@J;eil7tiOEb -{^Ei_KY+WIG?ary-N9c5?U{X_tDY`H(J2X;H$!`8Q`yjBUQUOGJL<|1mT8Jw9Z2HBQN+SENmVq!QfAm -D&?yfn=DjmOm{B%*N2fz$S1?2e?sZZi2OAhp_vXk8pk()cmR0eIkck=8fCCz#&^25|)%PO-V4W1r%+S -aj9MSLtkK}x@=B;_gQb~r_AhvDdE>kJ+>T#1Aeb$IG|->heqxD^%FmCGF-pqh(&Kt?xOdp-}^v1Tm*6 -u@aObK0F{Bz%ZB1*cgoMn>jQ#XK)@+2fb&2}k#C1!AKcjd!~YwZAR8m*{zZf^nbl2fN~n~IAw<~XI96mw8}Luhoqo0jD}2eX5q8nGLqq(VKVF3K}bIgcn{-W9i6FIB;El!m -IL?MT&YGt~I&7!vi?mGws4M6x03#G5^9>3Mi&9+TTDqj(nSwB@h_RKjq-tWqtDGSu=PqZ&HX-ObroiM -JL(@7=e!>_`)Fo`?N$>)Ymzf(eILiJw*3kx37`^;%VdE2(c$Ux`r#`~`rGC)tWc&TvU9F-K^?%1*{eQBBzh1S~6Lfc!rO7TTaLT<(^HqLJ&;!+gm4`0aRo=NMWoW{Xes -J#asq~SwE==XY2a!n`atvN{zk41|=Q2 -7p-&CXfB6?b*T#$GXUKJlDBx^f@QaMsS5r60~Ji^NJ?s7q^C(NPw=q7tfjVD@6jDU$n4ZUot$`^ncNz -c7i2~>h$Lske~7JdMMS7fk4bw5CS9HyD=)2(eYE#dSx3lHX1XPv=0Tb1<_N2-1hgT*_))1nRuKw}TiM -uUKQ1mt#^#dxo3xv#qy!qgp|I`TY7!xc=wx}PNrseeydYbgp1#YuF>yH(LlNt(z^Js7y4}v&O+8rNmV -`{_V-A6;OFnbW+hN54TsC?wwf}dToz}*LBrgSa^Pth46xRL0YZlxrTt_cz3%BwfKwb1;>1%aU#9nFm< -&I^d%`8$;(&|>hb+<^=*mW5xuwXCKjLd`$`GYlh{A~)G0V0Bmp7DbubZu_9bC4FADpng8YoT>4kqwUu_0rE_r3U4brmczAWH5#vpqdT6Om=i?$E~+>EOP_ -@Hp${|kF!NX*5i89v&o5voe!dRQXGBxhi{_2DVTfn~0cI^ -Mte!F#=U%RtaBtrN4%eD0CMQeA`q%0Ce@*|XwLV_N+aE^a`;FA7RMLS*N8^nOVRGH9G2^YYGHsybsyO -G6g85`Q_cDhpL5HT-4HIfVUh&ZPq2~SPi0iI%JfPGf|rC3j6D+*TQw9j*53lU>NH?j&Iqu1df!txopD -cF;!ujD)JXzY|yr)%+AJlrni%TZ*~+n}0b(IpuB7JZEGw6Gn?LZ;hHtk(T@tZW7YtL$V@+B7>ufvYRl -(v_ur%H7m;yOiN{)ydLEgOe+e*kqSE;uP1HBgyBu=sYXth)`Lu0;9>nHd@rV3}n~fMg&3BtbY>1$;sP -d8G;^V5(1WgBINNzWjRyz4h@Kpt*Sm(%qen~evC^mgBI6+y1%UYM2VtUczD3H&JxwpymRsppI*u#7xT -lZQtzbY>ILo@hwV*DU8uxoWf7+9>@!rw#qe3X@(W5ha*lAF7>8+LCM5}24x^|Ti86LA@N6KzV^NlL^C -nhBCuAb&*@71tnO0}WBWaW5Cpi+$u9o<50#CTH%qIXKlkD>#tH8`!v2|uPd&Fz7-NBh2;x -dT9pX{FFQM``{ITqC;DYT!ffiEYIu?^jC4XmLCQ@H3)9>)%Ca|^`Wa>TbD90y5<_OAKQY=hmR(>s$6OHWN#&M9;{=Q~O;xTcg<*WTVDC-=1tlokYyRGNfx=o4cS -WS`l9-SdTZo0~iMMKj;%KxT&<9b0r}Cs96g-v+O>oK*?Gv#0aJ-M`AoSU(UJ25zHkBbb}i{g10 -7~$9p_0l^38Y`ylyH-sXNOC-@)nb_T+R+HkC*DPqi#O->`^?Y2ry^YbApgs0H6hqb7opE+x+^wyuxsNYi?KYS8ODK2Un!47ez3TnF -oW@=0MIP>R!OF}6`GaKSO)C>z(A2CH3!xmdf24QQzJoAp4ngw;v8(Y67ABCSI;9KJ_U;fmQC+jpEm8)Q_zt`i{A97s0U*rQ1zILJWf?+I{{H&5O3aNm2efQS-(%Z)$U -9ylZougWjY@!;flbi7xz9e;Yyo9d>1uQ3F_t23fm$4UX!A&;$8|fpNR{e-U48`MhF}YadGkXKfXJ{w| ->BKl$J!79ZD~X)`FyJ4YZl`MpjN32ZIm`2>_h5K2b4Ty5T@qgt@mPAOVGOy{5)*yct1FyOTPqTkEAZ$HkH`LGmN3tDRUr& -xL@PZs$mp(W~})bwyS=$6@T@4oV^x00T?~Mmtu)E@o^!3r1-Cyvd>@l%?YAqx|lV&L3(18trTHJ56D& -0c_$iMQ7cGQ5DV@ysEVOo<*J(z6tB!Fel-8H-x{qdr=lVub9@alf9*@KMISdAa -owu*$_|E*3Fp-lRO|%P*dv1wYFD5@sUVt@dzq(qrY~qzvGwb5lOoVmLpK)tV=OUa76`YjyccK#nt5eN -eS9>*fVj_22KkhzKZtj}$M-9JJsDGPg$<$YqBNWRKJ#-)TF;@FBFSCNFCo&t)QZOAZ%f(uue -&S=;T!n|*4zCHJncloA4eN#X3jDTME; -kT>!Gj|FmDg^2_X~;t;WM|4C~&{C8CJ|9?@@@re8&U_=we5pB-SQY&`KOFYqKlF -i_M4~mSILn#OPU=%ZchAig&qnSOTdRRnrB3AUb%GZl*%ilYB&se8BQuiD~c(z)69YZjx&wd2)ZqrhF& -MX1q0xfHdxr*eAQi`e=MXh(MZr?}_FrHqEdtE5cU66?{k48Y@#dBZu+yefj_6NW1vD1;5)_H!&QH)sV -c0$cVA}Ve7VM6Nf<37ZXZ46`CS<+Bb7$SFBs$$8b>C;F2o?3pD7P;Kyi;ONsf5&Bfo%%xwNCnjqvto> -$ioWq)2_`e}Mz_#oSRYAw(aZaHipWcpXpw`iADjuOK1qJ55kYsBcc@q|`JHZUv$^7UnI#y>``|%ZIRj -IXQLmcOP9*mLQ7BK{IPkwYJsdLT!iIE7wzJSgD#Bo*!&n9bTz?&h*5L<6m$kwW9u|IJvH9e!>|W6aCV -)#}fC69-)i;Wty0}2$hec><#=6j}obCY4^=(mi82?K%=ZY$@kjf1knr3e_-3)uzGK6KY-EfMR3klcstAk-8#_m65C`>Kxh|$kDB<3U@#dFY93%6NhedYzM`c0bOwZ-~6{tni -6DrDAu_hxp5U9?u>yq7`pV=DA+I&knGJiad1oC{{k4i;!>7mJ^ZIX0c4SOcy{X8NjFGrMwM{h*6kAx{ -I_KyGJMT#5?q0$FB}{o~Mpp392pWhVm#wR=OErDAbKb}oTYp+grJ^c8^8t5<^;F{%3BExEM14M-F$HL -l`^#?$Py>Y54hq7}wHh~=`Jm-Hg8wm?WlHPLcZzzZO0isX4ayr*q(f7>2zlWnmWz -O4=4tKjyKHn(1gf2@U?k^Mq4Bu7cM5za8N*Z0K$VJ6Ss$xul_661s??_c`z_3H=$~k`TxmZ>tPQAaA^gJNeu -tkhg6~@qPUMd9bocbl8Y$Ha(VK0{+OcpJu)^Q#WJMR?74dh;Vc794t!n&SPr;R_dLWXp=E;a~l|%4-y -_CO}$`nB#}U8-08pz)B#oaQ=T0%y0YI>(GFUvS44etsWBsuu|m2+f&}n+(2?FhuKKH7?4Rm9IxlhG(< -_;w+xh3mAH&y|=-f%}V9`O}%odHJEBGIpmxx&~MQggNIYCR@B?wtEuE?Ih`Jnb*GYGN(q>iC=D{}4}E -^StU{+D1da>QUTFaR+A7`jG6Po1NcX-K5g6|B-}C9}^+gOBNRtZTy1YxjLr#2l8#=t9TwWjG+r180A6 -aa5Hz0^R&=;f~K8gS8-ULGMd~x!6rD%>q9v<@M2`WOO>3^WT1|AG!H2Otx@@y2N>!0<=}EL=RT=#(rb -8Uq2&@NBJ1swZx8(eyYv|9Rc?aBXZtxyy%S>j;s%E{LtFi$sggB@X?<#`uChQFZ}!CPneW8C`4=qY2N -u5ig)!%dmy5-`Kk7~uo+|=gariHrhUMzv40$AfAaGa_1KChyF~T3fL-VgjOLT<(Q~WAqjdYrt9es3oN -c_IX2CWSKP~HQm~0$$A(AwXB0*+W=67OV|K&XLezthr8fJf0&at8q9Of7XAn8?e2ta&2%#dFxx7_l~BeB&5}9EY@}qi1cNH_=XsP5_n{@e%d}rmEZ2oc$?h0)Nem4g0A1 -O_3qEdD-$S|6G%v-T|9Q^v$>PYFDXt+wRI<6*m~e&|Q=IoXwl{R-s&TGq+NC5!%mGl3|kFK8O(#obww -hC30?M=mJJTY@rnL(+1J6cgL;-L?Ena)!YhQ45!_=c}P9h9-tOM(6K#XoW`iI=Q)PyBzfg+A}&b{rr7 -dL+{L>94xm=<%LR&7QgC8{r$6xf*DJB4L(e-gD#_{7WB$&VY2Bj&^$D{U@U`%SAzj|b-E<@cVxnCVeM -NeQ@nh}s`3Q`&cFcThg0JeS5d@qMWtZv2ds!%sMequ=8@|3`isp;WwBR{+BJmeWK{v0^rTMtSN3RH_z -NcFyt^_AGMPae6(Rhy+_rOVg7kQ(%WPEY8-_`IxQ6F)YY)@hK)a$fjdw^?O0WX=+e5qq+VlJf#mK%rm -D=OI)abs&rWTQ9c&9-h9X&gJSBve+7bqo#t+W8M~ltfFkKGY~S_4#>jvh;#g$ick`@Lu?-=z#H>(VHG -;{AI-d@MbY8Pl9WOvFm(ACQjZIbmmf789Y{qBMQG8>EJ=41L??Dw=6R7>~d393(E+h`}`84K!Hf_m|; -2jyPn!&XY{a;4_vOS;X1^=R=uYZwx`_kAvZ~+9Jw51GkVbwE~6n&ZlfoUd}bp5pHJKoB!(Pds-NwG;7 -*q7!9%gW!m2?Q<8jhgA>A(ASz+3&0)sfzq1xI1VPsLFhWRlrp|BzCYS4WlZf9YvG1piB#m)osqG)4rS -RB(vuMQx>oys538xu{-`M%bvVt-+zCF1M3z5t~b_ecEN%cWAdC}G>bHHwgcj-#1V-9Ft_QPh(*JCcRk -`NhX|wcVXm7%UAxGKU(TNrywU=w=dKti=b!4-I7t%Em^l_}SNPClyBKlb&qfM&Y9|FcWz{Ix4)NqpNp{N_N~+6z1L(aKU|L;?nnIaULYXBzIkUS%?M9LclI2FC7K -e_=-%H$^mdD=RGA638E(OKjsPXw-xoE@P30W1DUzj4Lm6Eu!W{}cUede1;7zuS4=a=K4LI5- -2a;bP7jq!7h42;n4c3E2|ND9TpKi~kVq3~M$zt7Kabbe5`L2HZ2Ln3&`(TUxx{Dz0e)(}u}h@YRejO& -O`DLg^cUTzRT9X>(WSR+WKLEDBPT@-m*^5NkPXjWI97ua-)F?g`kB_+3DRn=y}F*?N!y&;WhhpFLb%) -cvGAwgq90-rh%yUl8au_`AS4@hXs<*yjj{1#tbdEHd1prU%j7+fvvBtkuF`AnC+fq&FbR=pZavxEm`C -h9Lt{7DUOJ>0|XQR000O8`*~JVGU-Un))W8$15p3~8~^|SaA|NaUv_0~WN&gWWNCAB -Y-wUIX>Md?crI{x?Ob_}+%}f~zdr@nUPG#+T0XK9%=TlFk&o}Qm54D^p;fkObctUZCFM>Qnq -a+=QoMNn?CRy4t4M#;A}&(#?1z_cUqAojySG2QenSt@?kw9?rP5*>*V{Z>xu1Et*?~{Obp{{4#CUQ@| -$!ZM(t{5OVWM3!cu8Os56n^*i?W;e=b(TDB^!7XOB$sL~Ud#V&WUc3dUcZX#`nFW*_Zu~5Tt-);@+wx -foQqYH<>{iVWIlv|3Twmt94ahJbYN~t1K$jhHrwl-RrBTFTSUC=J8pQgDmkXPOjsPd=BOL7? -!IaWuj+>;rH&{BSJf -YKh4JLTVisiHy$L1{aWOydy9d;DZ$JUcV6Kn#p1$BJq4JiV|vH$s}86GBq?)yEWz?aY1DDq|!8)*3l$ -dgnNWB!3z2xaC)-tweDi$dzWOy-c8-qQads-6p|xZ2M#v*L{zigh@2!*^ -h^we3T=a4)*ye)ww!VK!ui2w4wW*B6}4Fr*rij{Q++qJ(C59&ZX5My-sNo6PHVr -%9sC?HAq8`X8bz`P>KOgGq6n?uhR+Y(d_Ub1e}><-I1O65(_4sDgoB^AQ0TkHO9XJJ(M{4D0?<*F)sf`E#j3huXhXSChwZy|qB_Oyn;SnU8 -TrZM1&%^gn7|cs>vxdNe4`kfZveB1B^Z~PY_(ET)6!)D?_Jzt3GMD1fG^Pxsli>6eqicl;ay@!$z041 -9JP$Lxh|8X3fe~Ji=~M}(+inM~_)t{&b*>SXIWdR*YS-!UFvVp`nc31@z=5uyQZdi5{+w23ty}x -Jx|`9gnd!2B<2h>A7+Rqj3#Z_NkkRL{HHZtN~VZc;N)x1El$X}22bQr(7mTt#x1MH103PhGipp0e@uR -#Z06ER%7R!m=l8~C#+^%$&TW#Fgxj6w0^arY5L_c)#xA?3boRw*j=BX$sI?$i!Yh`qr-2pe=^%pk#IW -cBlSTXQcqCHoqBzB@v+%-hMs%=xRL}dg-W^0tsiJPxPqr9#)KxuNWtQRmeI6E#7q{Cb(%zne< -nt5YNOPWocv9e!a6#T<4DxKxe!mq1qbC9%kS;8WgEc~nuvn|7W$UU7_x~4apSO?CTUvVE{bNi0>b2|o -C|BwMjpC40><&i4{clbMG-Wy7p>|F9!}yWk=YF><{8OWq`?~sL3UZq$e3_@G@>#I+9w)-mcbl!!s&SL -$OyOZHS+dE`)}U|7v#7p1fpb$qsx8ZIa9`qLJ~qk7RdbN!afbtC__37Oy=VKhnd&O^27E*ij}r~?BBy -EFthY$qrgGHk>R^Yh(d!_{p}>vc!)WOfN<3uGJ+;e7pf@=t6naThh2xAhp1T(VlM=xP^rWdKwV2Uo)9 -#=(JUm(8C(QEaY>p%ON5kb@qjj?#_Y@YvuFs+G&kUER}9Wb6Jj<{xy|xiAf>0#@;zn3S#@wqz#A6Gsk -;geI8-&-fd`&GwIhrSKP`cnSp+4nkbjtHi%~Po)8+JBxZP&R$=|AGVBc%f4@QDhNCFb}L-EX}w&Un{9 -8)tiBt)cY@DWty3TBZa!%+>u8j>PIMOgP=n$iaVt(r|3Jg8j~7+N)(c?R4dpX6mN{lJw1-a6Y9y(k@7 -F4$m!;3XGlsFCvkM>3dex#u|GSS8`LnLZ@TOvh9O@xy%@iLEhXF;-%Sn~^gm>8!+7W9Y51y9V`ithY> -&>7B8;w5)Jg)OEF0F|3kv1O`xWIM*^)rLr5CRN|XxC!S@&Xk^FSj&xRWI34%~ic? -8}lPy;@WB4~lggPBYg2zM}Y!d&(#-Wpin!`YxD-CMIXRaFHhouxX*^%Jz;zRzFj}9W{te!D|sl>ofiVPm~Jh*XOL@T*RlPh`o -U;2yQn?$EFe}n2F^;lY?%GkzMJqyr8MyoA#7G+I$6p#a>HWr*}q%mN};6U@u0|+GE&SXQq4PO<04cLN -^sV1}^e(6k-i@JFf%ec3=m2(Ydn|<$8>aartdvGJ>*Pn&KQ)O4#arCMc=Y@_@CuzKzv}-2eaeUob{-BYz$%#x4{!@9;?XQn7Qe(4zzjoGjd#Hmrsm3PYN#Ipv!Nul>t+&NGrHSRv!u(q}V-7t6h^sH&}auV}qS2CPT4i`WCAY^~yP(3_t5v0vbd -xl3P;@?K=Ooz&!1h&iVVz4JQHd)d_k_b-0G9X-RVylvI4aN$kvnq%iXsyAsE3QB_wlHi+24XVLCQ+mA -v_0W6sHx6JhI?=8fOu3k3QB&Om3qfWV)`jbCCAS8bgSjRh;pW95)EG`jN$>K>d(PDAp8rPRi_eWP`c| -8zLF$~mT*%O%j%=)^mrB2SY||2X_HX=Ln|W$elBF>p)|Y@jS?(aR1mOSuIR@C=1}H-d};}4HQ7+&^Z~ -@!ZBzycQ%Ey9i@&`k$nmAiNPLUIc@7VEy-*%wVB0+7wrukB2sEe=NaATmv3f}x18&LkAPeBW)PgvIW? -w~5CW!nMJHcPZT7fNty6hhbTCM9Y_dm^T5QH`DaB>CBwtw6G)7KjenP%2V`0K>NbBMoxIV64of}{rTn7C(T;{bLLm$Q*UC8`11{>o*quaUD4HKx9*|@Xrqe(4HCUPmv -4l94{DE-|c%{FF*#cq;}@Gxw9&7!URXqxy^+Qv_Q_w~4|d45;#G-@hVib0y~R)Q`>zz~dG9u$%Sh)tJ -C$(x~S+x+pck+b`F>28N$_Fez>e$b|44;pnphH1ag49fof-(K$Tt=R)5Dl@zf12Scpr@^t3+`JRw?89 -J2u(tIj-f4$>e^@(Mm}qrKL0W9x8VB#kTC}116y9#s*%2joW6t_RnfB@H@6TelS|Jte6frk9qRL5tomb4btkZizxbFDfl`Fs -24XZmxlFQy`$|`lT7$ma^@MLNfnM2kq0{B%DwOk4qcH$cR)IWRSXvqY!g_Pj*G#afkyEOAqHcFo+kwt -#uqyM_5uy_qU&l}wEcMVy0L+FAA8G5DkNZ}#vcZOW1djFi?e6 -oC%uHB!=dZ7K5hvO!k=RXshnaqhzu4|T3@i}f)T>4`*|$Z`TPZGgVeDl=`n^!3XI*l$?-+i?rOo0O`* -?*pKDALo(i+4YB1oheFeIc^>1s?C$B*d+S*-SCrsR*Z7sI5BG!VmL;nmm(BG8 -7myaMG1GH>d{<0J~0L~RDJsT#2qUW_C!u+n$q&N7%ojTDhKZn2$?L}r1W-Gf{q_3y)Db@W0uENl=&>{ -jVv-rX`9*c&o;;n+XXOo<7h!A?>en~S*jiF(R9UF+PKM^KDsV#I!SHHW`k+lD22E&-**9WX*T;4PCMj -pHt)f&FpFg5;3XJ-C0pcda??VIi#@N~%{<#l2E@_@Xg|`XlsvmO`Ps#~DUu61*aGgWOx{Q&TzF(Q{ue -!4KodWW73~FGshAG+*#k~oSEmg&d0yVuRv28iZYERKlnw}_4ey3=gmC)H3^2AcwkMl9I%%eKsRr%Bk} -221<;Tiwe&uA~w1bu2>WVi+i0A`3W_l#W?rg?Q7LzV;%K4|Ow=T{fZ}@d;I&d4^`l>HX!i&~9j_3Gvf -@UJYXU5;L6)B{WAeB9tThr&_aPc;xxkb`+vFk4o^QKgo?K@-V!((eH%O%z7g5`2%PoeOr@D~==P<8`~ -x~Xh8I`Wy2n&-xp>JBt^igN=cKImPn7EgwU=H2+TCtdx3>}BZywG#2%#kh7&TU2W2V?$f16d}dYb0d@ -4#8FEp(zfhtZceC^TghH>Ni{e`Bzzh9_s~|8=eFByP><}B{>^}HwuT~Chy9{JNXMD_vY9-My -X9?Fysds+MTB%XHVCV{Os?zf@01Mv{_%f4nJ829yYt5rWwe;$p-0m&i)J_Z5_905t*e|v{2RT!M-$s3 -JAI7S;*b?OwC&#CduCz0FfcxhNuv;`voMo@yZ50(dfu^Q&I6ADA36`wqu;Wf3;Qj^g9nvKkGR$0qsr5 -#kPQ_O#Fv)>>@-774+CYkyWWm8YHj0i$*%{=wr~qdN8q)(0Q|-(WJkP9OoqsxDk?j*CRSd+h|$Kr`!+ -yxW@S^F80B_b=F;vf0IJ5|_^yPN^S`0$=Au72-rUao##2=)u%-V-Eyvk22lH;y9FvJZOW>T -~;%092wAwo;I*JDV<}sKB(u+M0*&hb*j$jp9*MrIVKwFW+(Vemj>H1j`NspM%1(UCLzMFLmN9K@68?t -K5enh`iv#{9CO5)m6UU_On3DJx2!S_uram?&s?b+kO;+-7HPV*nQ#J0YWvzYolVDT_Ec@%-%affCJzt -a_La-^Z4Zh~b(xKT`d|Dw0ifT!17<$oLtmotzUC8t;UJ9iV$&evK!l{-sl#AN!EFlm7Z#>flT?fRSgTVYo<7?f -9h)!bm$cuRU8GaWRv6%p -E2@UmvSvYmGW0Df*@se1M8|sfP8(ktJ$C4sEv$;Dx*j$z+7y!U+Qo(gP|){1Z@10|XQR000O8`*~JV-AQjCA~OI0{mK9U -9{>OVaA|NaUv_0~WN&gWWNCABY-wUIY;R*>bZ>HVE^vA6eQS5yMwZ}r{R%`HFTjL?E#)!Y>?j$x<8(Y -Hop@}gCwmk(1ri{I76~u_D4A*Jzwdojp{h^-DLcKBJ!f$eiv+7~y>8ui-8u -gVM<+)o!IN^ctMcV_6GTr&!TF=J^Hcce)8KD;k?ew($v^T<48^|9=UGu_Y0#8GbDagxcg=NK1TV`)bC -*j -)oyYWz$34k>ncldo{q9N^K8=u&*=Trsw%5V5S#>?Dp{_RNl=t9z}u`cE2rD_dN9pKrMOO7x{pQuH=P$A}ud;db4J_XZ7rYMsoYa}~dK@&_n`YYV0J0y}iwrtaH) -8@y^E?iIc=0WL8p%umW+gKKZcZ4X7EIRI`t}^gJr35%O*T#8wbWl{O@c5~{lCihvHtX|xJ_1hO07yIf -Va|)ap&(Ev!&CUy|D62K}{}-;SrcJ7=L13q97zTFm2k8C=-)8CGxRg2cyX!2|WB+gbG7j> -3$_SayR@u=J?lM^Q3$@*1fc`DLi>5q)>FDU_o5wG|dHQ0Cf1qfU5l+J_MOFCs=%>4nMi$C-%gQ=JpJy~^ws}<{?vvMA{ -sutZq}=}$p#i{PPFRbo9Mb3eYsvi(_cOP`tc9nzM4M!?yILiLgnzfX*Tub;ls^tlgDMXqz~cIQJO7iD -wEZUa7>{djk?BddhuOZWFw|4$H&J} -F`0h+=sc>k)dKhE&*NaR8KV#a)Ms5U$1=!z)Gj!P1~_juVOAYPc`+|5T-4QWq!4JmT{Zb;mAR8u8&IO -U1U_BPk{JxM2AmI;C2TSr$`3D|!E6UVz=#)l1)GIy$GUcr-7)IpcF=FJ>P@!8^^U@ov<$Q-+*WyLj6i -mj6qTM$Iuu(sA|)=N!s||{8Hf%y`YnK!YHF|ciX--(Gq*I*lct7tl?V|r$dTEKDi#YOT>vc%?J?;7i% -b&1`XU3EF1A1v;bk-$2QFv367|))zBH@hxy`T_r<@vFU}Q(q|H%QQ8_~qq(cmeUU5mp>=l=;gHzb*z{`=2Bn`jL -EDqw3!@As7upGczK^(%Lp+iv^H%S#Qf5GiRA7{UutIzZo#CiU%%FKQVdHrU<0;*jHUbb9P!R$>&fA6k -WQUQeY)iwpe%&oFX7vWfD^~7L|O#-6>-bn%gi0F6M`TRN{d8ZC`JIY6sswQv24zop7Y{zdMZk4Zi-4?QI -?W3Zlc%zSn7&+uGQZ}=R|tmBGpLh;a0@6)(h(1|rPv}QF|Luvs06zU=DG@!x2qOTK>0Y;so+@%})hL&}8KHnuvzmX@j1`RLrb5ZLJV)J((Y_F4U&2SKIZ*%p5z-YH~$CbAA0znQx-0X$vtxyB$@Ofm -LAQxf-l9ztR?1pu>85zY^L -~4M_APwHmNVK!60HretTcT-^Qu@X=J-N$QD};?y4MhqKNEJ|ep-JhG4}r{6FF-6O0p>1e-BdwC8r4H8 -5-j>%**t3pLl&thd`^mZJ4&T=t8s=7N%Pob4j^yPK?K@sn<2Y*UMsQUUp|W8G3Fs6)z>)Fq{`s -pWX<&~o(XLY5&#wQQq?)l+lY0>E~q}?h0z98W>f?2O2Qk|Gy&0Oz$^mvA_A_Zc?x`q?GmYvETR4i_*J)-NkkH*n4#_ -4CgKs6%ibhNgxWa}8%1zi$H6c8W@OR0R!!5Si%pG-*0)yKW9NG!usdjOF`O!V@q4=Y&SwtLyrKSS~_3jo{TnopZjlaOg%EXtM~95g&q -gK@{Ary*zYKq!E)5(QoRA`(65s9}$ohIWnqb|8Ioa&M42RL`qqqlMlgKrbOELXIqW}6WP<+ -i3_*Qb{+n!92gf*^<8g2-Q+wRflw&n@$Fu~;Fbl_jY6amjTMP}b{$(M|K?kZS5uC9Mcs_E46Iw#s{mT -_b$P+w2oR5yWbipZUi9-!&loA -E(C(fAGWBZ<%01k&aSZOlGHSzsX|cf9TC8pdEasagMt>@qN#wLR_0YFUl}SC*hS0f2S9tjg`i6!N+R# -;JodKDOt*tnww!HA*}frDC*CuMw&6!{zDOYhw5h!~N4}0KA+)MHgyE7P5QowYMmFiscns5HrA|k;fKr -M7nUrnr;jD*a$#v5y%!EEIi6P{toy_yc)TI5?$YSr$Rwq23N8?tTh{ewXtamVjr>X>9)Y>Z%A+GfA(( -&ctUIikjLo2aCzB!x{3-+8Lp-1p{}NR%WyrQB~YaeZi*t8B&n@(Zvj?_F_OHtFU`iP$vnY^lGmwd3s&}g$j&-l9AutQU!a1V8}=`!?>%65YQ@ -X>|>M^qEWdNcH2F|A#ULHlYu4M4CA|U|+zv3}rk|V&CYnSXgKkXPYBPOD2K7A$ -l1_XS`P@&+sB}v@4kDXW#~_`D5lyP~FC?7!+?#mfa}r9N7sh}h`K(brAClJsm*Tk(ZZkxZ`{A>Cc%d& -{!j4uog6aj(iNz6gL75CCJD<()5~S8C3w&YlfQn%w{AQo-IAv2d78Rum^{YPXotS$`wiE-up~=j4uv!XihIAG%X3vPbtu>C)H!p4Ez -6d4MVx_de^HUq$X=|eDOF|Am@$80yk-Y=qOx~x<7^DVDL4L>tqvJe-xkBs(z$BW18Vdtn#;pH={b6Atc;m-ZCz>m$w#(>vEOW)J0IjEgR- -?l_f>%i_`K61t^N0qtJFeW#}}`n*J>zLJl?=Z^})?jH+D%r9qoT6#cXGT^%`IcrguoqMZy$ -`c-RR)^0o_>O>9GCC5eXzx$cRT79oiVZ)mpdy(E443#u6s!S{&WT^Z}XbYlrF>RO1qU_mFio8ln_M`H9iFLO3zTd`S|`CbJ%aF`tyCGV2aN8PN!m?S35_z|PyO<%Re-WFvUL}&4%QHWb`lU3Kr -rZ9f{`siHy${HLL -F7JSGpmdmxKd($$*W8_U)B; -Q7Hdrn6|(lG%SZRn3usH)gjFkxO%YgJW7!56_f%r8tZQXvwIm83=TBqADMCn(*vVs}Epfp~bN9=9F9( -qf&kO4lo96_+-*xt*`-cIK3MYD#F^k&$kYt*Ms0qTCIOP()70tvuPIL3)G1@bMZ?r3u_~s=I7-z8O$Z -F@_MQ$~B6^6sIjHc2McnzIhjAoQ$KIT;a|}@HoHF!8os{?XSc7G_8~hwCm{#J|}xR#AWP(*EczbvzX* -A90cLOQ(_P95?}>nzqrwJ7yE4~uI24;{nItPQBVlCMh?c>a4_y$Kf;BR!TFWFNkwpDo^_{JZKRsc0CL -eW?$iq*I`FaU&Bapt{H6?%V$j7f#Qo@0Yo$>taA|rc)T7D2+OJ({Q!C@0V>nfuy=kh%v&SV2lz&D^Rz -7;9DsBCn5?yP1LSd$gM*Q>F5q)>$EnCX9qpN8Z(hfjHYzStDcG3Bt!rL`3C6n@XdN{78N}{d#lXp(E4 -&9RHp?G9R!;@1g7!>j1`4e?mMgBUcRbClUcNm3K2S5Jx+f%64MpQdTRDx#|;0oe%S#cNUNr6;Mg)z}y -t0}f4SmzuD8_|&$GC}Yw(#nXoBeS*7y?72YkYN7dhc~drEsxeACmCFM(K_$RXAEfZW#FBo58UV^c&;P -OIkFH0S)OPZ1x(O(-W#|`Wg~k%*iVOIf(%rT&R%jfq)+v6F#ERyPmAhF{hyBCR1wH*bBq0_Rqquf+?t -h{R7_x78zKdMZApSFe@Xk&^i*+WQIAoHXxBnJmHd*85HAy~fFt6eG;W7t;hA}VB{If%E9t}b7y5L4v{ -!+bg0WaI@be{GFJAi#w#yzF3sJ^Xkh-UH;j2%Cs-|3>#*j;zR8uR;3;*f_juSLE91B0*JvhoV@56EI#{3z#Y2Z -0E`a$tSJ5V6CZ$mwjqZbw#@K9Uw;4HeYqjl3+n)@!mKn|wMc-c01t$huqk#Rh^=!u8nDYaIB%T0^{y#qTjb8PUe|85vXf1p2E3UrjcoCs^-h5tJ4VIRr1CUcB-{)d<)2dx -#9@`VG<+sW@JtI+mFZ6H2OM}NBLFT@_p$C&LV8i6W+r=p_~$easOA@4epFm@(J;kAys?x}T|}hq9COK8z&2KMP6UmwBY!GDCD(q=HltJ&&w^+0Kraa+_3J!|9UlLm7{$(ezVWgGtp|T -{(=>_fz?Hp!>;vFR4GE7bieC?&ydP2Y@TFt~0H=HwdGa309?{??BeqAdh+#KaJ2A26_+Muefi?3it3X -NKO4sJ>~s2rc_ZIP(FE!v>Q-9bxZjWzH~8OmpoJ)X;Hh}E0xeXypLE?-UrEz3h#vJ#wg##GvotvD7v5 -Y%^=HlCoX)~0cOr)r|>}%s=tezQQjoc4l{V*^i*>KT%pX%**!Wh|t43mWtcM$75ug17kRCI9 -)6aeYcjb?n4-P~mfY4c(woaqhay@c8wY5j1xE#p{noF3|pbDV(ve{|u+!yx69n`Ju>BDyPlU;=3FrV_ -Tz{3uTT>8}*lgtRYJBHX|-vEsnl-?SpGgys#_~M+|p~G}NKv7j6r3{vtY)xtrq2+hSU|%Mo7wJvsg5( -di%m?SFkVemMEl#h3pPpFEiU^wZnZ*E*3PLHG|R-=kg$LjuUrZ-<`a?`DE#ZZXj2)1)J8|T8<65a5p9J^C3NtL+l47X2mxGdE9w$aTWtey2dNT3urg> -b&he`pq{rXgpZM9##b(U92*kW)d`)k6vTH5NdW|TlD>s(@XMOy_EeqY%P4OO@+MQ -S%#`x8LBp&XqrIC(aTP_A&F+ywy4`IjGW)NmIM$u3(p>Y|0fKgB5H>G2hVjjC~ee8kAh`cvP1-*H~A( -+S&-Zt7z>7OZ%N5kRyU;bk{FhN=vi)|XGwjnyMv=_Wk2M4Rhy?E&5{)p*4w1YvF;P*c&ASb&V((rs8* ->fkvCGhr1k&+rY2j#+=LU#+e~JyH3>y6O$jkQH58CFolPiTb^I6j=WFCT=tcxV_d+sOM>CN1*-B=>D1 -Z`MFlPMYuf4*6cE~mB --SxkTN!==`=I}zzrv392X-03RxM__?u3a|CKQ-l))kHTNnO$s_q|@t?BMUrWFz$yAU{yd7unfhP+GUJ -2h62VnrzxO%H#km}pgn#8vw9Pm?7_&=_Q|Ic%u%P(?ahc^GGstPIwnnxIgu}sCfE -v+X(C>1f!G5vLY@R_Uf9qcYrARy1Hwt?)oBOLOW=cf!PzV^eIEIMFtlBv@+tL=M7=o*g>J_%q6-i-lf -?A~8j5XUvv;aEURm<`O#wToJS~nhk7sA+21#>T9+InH$!I7|2oHm!HE8zDTtqsDAEzK<0U63`P%{b)! -8w%F%A{@Y2sFiwfaadBfolMCSZmz80FDSH(19SzV}nW(R+IRh&6uT*2J|I0Qagf?tWFfxlN3vd5j7Ck -J(6uL1txsPi*}5zuTgs4M@r1sUq{!;AiZp9X{q!yYvUAAdWuO8nHOLSd^x6c -`YzOTXd~E`L0(=G$m~*`0I%-(hrn1_sg*?|~U&g!dKnvh(wr#e|I}~}!4O>Y-K*3*l%hkz_#Sc@sqwa -DFGql-f2?|ouL2C)y1EXqAJZh_^gk)=@MDg6tJArKYba98;npoB@*Kg}?|wlz^9W+->zzHTgPDto -6cCdK3xP~X?mY#NEtZJ%QCn81d3Z;Y~M_y{HOE}pS^wahtH;;eeyP`)_C>9+rt8OtMCrS_wo7LX|hf~ -`(*TXoy^Pn&GgHk(vL2T45&UT!Bp-G9HgEU9oJ*jDsYa-V8?u0lZ`XD5gvq`im$0#}&dZ>CD(Voat^1d}(HNr -7WgX(g_6X+1n!d1uHh%^lVwNeqyRZwxlLhn-5X)*-C~(X9dvaO14k?WG7yBkk7ZPq+??>a;#DZ(;l7I -67#$U&ME(A8a1(_){=P=Yxwf2GY{+}%KxaJn6iVfn<>AOOGr4Hm+K4>fR6LXHgeDN3{C)_--eI)i88h^@%(Z?%fyQ)|7)|IEiwQP59ebA^EpMLLCkS9&BMmePD -c4@^ePGt|0F_74M}UWG^PXT6hqSKr?^OC{PrLlX1iyV`G}kx8Y#I_QuzJ{$Ka>3ihiIy~g2SChj)y*s -?}E9H1)GN>oFB@79u!mUFR<*g_ -YD8K$pju%d5^YZe3;f%!|`?wk|JiBqQOxBIfg#mm8h;D4*-D$sH$Z6MPL7d&NOpEVr>{!F&VgfHMAnb?JM@iYHK9?V1L3?s`nTcPgL&V -4a{%cWGlK>W?SUV4t4Q|UuCyXFhIR}_%x|^&#()t)rp)bI;DFAPl3hUUxM8_DL;)=)adjIiLI42W$KR -eI?XG}5v;BzW#|0J$ApxvL5nXw#G4#L!$)r%b##n6gW25MV%?*TZZ=>PBusk7&V2iPrjLrOvgdl1#vaKES?-y@+d&I=nz&Ew;m9rsMT=4GS}by<-ys&^5*f0JSL%H-`Z9JxZ{O+ -fkw-j0&X1(Fdq3qsDI$%2kLQnDU5rZxOL2{7vL;_~lby#DZu=<@HMzy4_S`S0mYB!Bdz*D7F}`b^=DG -Y>CaX|5+*(+>rCLZE*rn!{oS4}u=po+wUZvfE%zvJ-fMIgJ=YBc>CmZtsF}7~fZPD7`gbnlhBqBjIfC<3fyFMatC<2SdU>)+xnn}AV -h0OL%MqGsWJJKLG+c%D(LNk5ZfaCEM^Dd`qhvWt#6Dun_a%FXEtHv6ibjec-+gCx#SmX5v4A+8lQh6| -ZFPU5KFhi+O34j*%|?SRNJNl>lyw`5l1Ung6(>`Yt9qayE{YE4g>O0i})-0o&57|4YQ8r{X1CF%0J7q -283a@FTrWuK3cm2*a?C2G)L@U%_hy{2T~rlK!{$8~T=5ip#!LeUN*#NKZlGROQt+qpe$yiQiZ>r|C -O|$#TMv6r-rbc_TqeWpLKi_dqJzNxiIY%2V^oayEf}r+xXtHf(897H4%-wj<@B!Jj`3bpM%FykXLGR_ -L-g75KUru0r`d!6-FR=jC#%)d5x=kFEL-_Y8{QQ4}=jZo|@D!H!ZU}G4gM2=i^G*Er7|znsyJU=^5S? -VjcV=MphtAkUPNg<^iQF2_*UjbZ{lGl|h?&#LHhs*a36%SKzQZ1ttF4M9N}0h+mRfx+`wcdf#A`H&$n_9`K*MVq+8Y|^cZ!Me#3$g>>-(j%UMYEN>IN}ObKcx4xo=R(2H(5nRmGZBz(R -~<{4(Tmyzq-G>3HMfOH`PsodKlhi5Z8VYeUODB!~J5B&haKN*|-FCwGNe-O65JtixX8N8LOJ9)Azcg@%}#tq^pLxy -K4Z(gP;{Y_BX{Y*&GbT7@pk!A|&DWi)@hTvpkVPR~duuJ)!q!=~TMg5ugh6u`bz>o`?yx^S$ARZx)0R>t?UD4}^4sMuxC9v+0%yqEXgi|2K{n*&!tnaDq(Z(X{l+q4Vg -PT{@RQm@yeUp@7WetJOQaG{=6?bp8jCL6Ac=KJl9bTMv -mpDBWsccqERxpJJzTD+7(ccYrfP2jF-vY~v-p1D3@Xh1lhFPcCfDfeH|z>!S7mtP{GRkY`W_s@t1XW6Ovr>OHclF>Ij5icTpUIUdXeqy|@+NbHu(AH`zp~7p --I$GTktaVMb$8(#Ua!%ECxkBbSPX=d;O!b?PD4#K`co7-@ae{lA<%8VFK=E4Zvk5ei|)Tuq6O9mD@}V -Avv-yh4$wS#Ft3HISBGnOoZP(wu+LNffrvaZ0u&XPVpC`fA9cw5*wSo8u>w=|tYRUA(U%x -wdZGfsL1Fz!IvFG?O3zp1-7;4v^VDb?FqjUjJE({f4CH!yurZr#TOE%v~=qn%D;Q_dK3h?D>YOVAaFn -5m8QmcCJP2mzf`zzenPQLD3EW!L`Z=@-lZ8A-$!8C!?-a=D}t7P-MFH#~da1Bi$#-~HuJ=%J~w+!(R< -7H-DscfNyLRqvi&u}jJQKm)ti)B6Li@Hu!G=3`rcoemJBeU|9JyrN?m{Qq+uhNx|bF_=KTETd276I$zi8`*W>gnX -DuqIPn2TNnlZaZ?-h5pa$zV3Nt)Vboz|w-GY5~;)QPar3D;uzKYZFyzB&&(S(NVq6T4R&5F_Os(s%RCao_^xPQvH?=vep3ZhejV|$@j>WbucPSULm`0;fr#xN -Yt~_(Gngg4DS@*97>aIl!iUYAyIr5{8H$SipAJ?tXlT3k?WBETEyO+M(Q=*5ZDx3Cau4&kvBO8i=Y*n -@fW$K>z_JS(8D5np9?nS!RL}?^dcz>khu^fVTRPuc=PbUHKU|&xmKkEHo$2nO$TC8_QJY|Yj0GxqSnV -6TOi+g4%<>v3o272?KZqa3A%LaN}qv_Q5O_D0TnbxwH=kHa=-i3lk_wy1&xX)1VB)%&qU-??nN&?NdW!CQh>3#4lf@*IR -chOT!2{?r)>x1I~1&3-g}O_-x{X42e0Hae)gC`56?o9%Ip=Shu+A!`hFZ1P+4>Oz0~x@=sRShUoC!A) -_10aGGt?X4R=)cTplnI?3JNAIy6w!K_9_sS}2+%+)`Z;ZhvSc~?VR;h%)vJo4l#_Knw9i*4SG+_8vm2 -EI2K}EI;+%cOw=*0{awLUk~Wk5z1HN2bS-hU;GWxf(k+CVirdSN+K2n3>+t{zJ)OfD0pbs^Mx**Hw?ycGw3vR=! -D;haelg(In>41XGw4j|i&c_R@Bwlwlzc{5GRz%HoAcQ^u-4h71#1kEaZ#u!oY-1ovO=7D_^y=3*6+E; -Q-jarIPG^e+5^=p6i0V&*W{c~soOF1U1k@5FcQB?9s9YnUhg4v5{l>N5Gx6M%QLqH)3(%W5284bs(VNA0B@+h5} -Qpx)tIzg+of*0Uqsw$D@;NEW>-6roRgaDv{wPB2Cec6D_P~SQKv)p2Fxiyh!x -73^$7ACl!9O%;g(l6EITN`>g61<@Nd^My>r@aT$=G5VNhw-b -z_HG6LaW?J0C+}Q3-Gb~T3n-hIj9(iZzs^E@V7G(LBOet -IKA@!5ZBT>K_{@YLw=km-@@NPZY+qR8qg_giEg=da%Pu1wpMX(=x$aMZ+RCN)#Fov2Ld6k^R0p?Fsbf -1n6FdsvQDnBEQ8M`X&WmYE>W+R#1XSx3m~T4ViA{S(-0cyZ4ro&5>_K@o2X8*A>SGQ)@E(hE6#u_zp}n6EOTtK9y6E9X`#v-lK$$u(rAg}NU(Q6ra+KrH2@g52LB-IY -9}zXuF{Uy(HQUFe#PLgSw;twLpw^#=0zD=uoJmdY%b0TczYFDh07A!}p@h{idRONyb1&_NMTW1$@F@0 -~We=g9|4;HdTI8z{7g}io;P*1x9CH=m8*sZ>nlb#q@|fwFXczC{ -dyq2=idpo)mhtJhyd}L}Q`#W!6XBwl!}Wc7QV}V?>K-~GJbe=n5a0W9aP{;US}Y4o&nyn(>b?24F&NF -iH^b|W_pW4Z1&QPoc22o9Hp4arj}WjKk^oGBtsPj*7yL#e+n}is*@M7_0N|*vA|0ruw9wEeDEp!8%O& -|hQczRnQ%ZGebOED8<9#?!jrZfY-5v$DVy;?dc^)BKtSU6lcIuD*4^T@31QY-O00;p4c~(<+rZs155C -8z%IRF430001RX>c!Jc4cm4Z*nhWX>)XJX<{#JVQy(=Wpi{caCyxeX>;2)_Pc%s5^sj2D>02Tv%90l$ -z&a8>uG$9?H)U}%TOdFv8D)>AT4XAKYrhP07&qVqr`2d%1lfVz{9&QKs#)-J7STDlWaC-WicK78{Z6e -20QFPEZ2E5n-|PI@YvqBFZM?8-%ED3F6JU*7h+nh!kn}BqRis3NJQqtsteB9hoghTlZ!*YxGz|k#q8k -pFmTUxuHm!Vz0RB=zCQMF!SV6o$>rg@;KR|$VQc!1zx&30vwxj=e_Utq@DEQfWPjkF3&aZWdRKB -36^b`brc<7y>_F^gTA&6LWu7U7wNU&b4>OsD1s}2%XBp=)KtW*(CR5bPbZtQb`2vL!HE1ewqoRBMzex -XH`g_dxeb_yb1j+!La5l+Bi8>ByX03eb|C5Of)H{}_tijp;F26rL34S^}zc@NYj)5llAU6pT50Y#uuE -u*HrHB;T$jud#-*{Ab;{k&YDBjzaYEQ}}Es$u~k>4*nJRia2Bn0d^-~+spMVLhZ -x0*0shIzOkIAeAR6yOsGC(=AfXd}C~nw@ef(%}`uNdU4zNO`Wl^Jtd_d!uwN6dZ8~1aUoDWE@a!VJ%(Z%`Ue~0w&?&w@Sd^ -q`Gu#r2~*g4*Kw1zc1yy8UQ8dSzM#Cn#0Cwtmdb+I{|knUhS7++7^^y42#w$XQFlH4&`dyt_Mu-l2_uCavjWi-$6i{BsRRaFW@<1(;ab{bUD%;U?_P8J)SK0Ek~(Op_0Ctr?THVxMgCPg -)FwOz!hJh$cu(0TWd48~jpE2#8mlsYsRJ#JJIpRhXfL%l-FZDmTTkbILUI!aBjM`+L7Fc>DmuXV8_r?z-C6Hq3 -HA+`A<~p#k^W5o~-XKz~vizIM+H^duwbR3~9Z;NG2vm^PF4s?N3o+v9a;%E1_5fbs5k^(y#! -&p+dUTmV44T|FKUyTBMLVTs16l7axP7T&Cc_5RdvhAk=lvN(q}nz{h%VO^0_=Yj2eDFz?9^4)8rC2oW -UmpqEBd{Z6XX%uE_X%|KmksuSfvX*?LGww~(y2E#d1>!=jdGch1XbH)?QF;O16*3x+hCL66Jrh@kIqk -cKW1TNsehVR3V`pQH>M08cm}1=5j8|G)8wsg(Qds>Wkh0#8l0eK%vOcf!B2HSKqBDMSK!X11q%2<1;LO4v(|oE$Wwj?7OGbGj5%S1)vSX{!gLj`Z7`rnfJr< -CBpj*hm0YUDxUInz;QUNFqTdPJ+=5fga}Le|kk=65FJ>DO3nEH*35oA;A20`yp;9&rgeiQ6%q5=+WSL -hUDHp!hZ@fDAD8NH&IlzUPWC;4DNob)o2LV_XQ`^eG!sPS^$OOd-E3aGpTVNSh6M>Apg9<7p9AVNtyd -$AmAbQ1Tc!3Nhk3xVoP{<@DRD3oN48CLnz)n)*1yp*AFl9W?MLvq=Ji5gOg6*6&wEcbNBw3V}F?Yrtc -Wl@Kc4XMrb;~*zQjF8E0zKW;e^L9Qxybh~^Kw%*s6!@H;29w6I+?KR8%8|>EP$WoP9;T49F&S$R6!2@ -7^>8!Mjte=hso-e1DZ7itGmU{2C5-}yu%VdU5_+7P2S@4!)Se+FJss&6_YR>eNAm>=WX_9dv)kxOCMJ -tL0+LJRM}tMT>Kp7d{w1V-1*yRmCbQQh|SS0fqXXO!S9y21kd|*6jK#%`zkbiEzMN@pt>Tl4c3IItyu -JR)U(N>zp;My@pUU$+w*>whts0P?*}yvdyN$ftwg2Es2&~}Qj-3t_$g(xw%bh6GB2~X1~n*Z>&2re>X -Ukx%1@e+(+qYtp*5Y@r=|ol4`+*T3?xDRe`n>EjE3zRBI7U)c^h`bz%NV8!RzuO3iUZc3xZA`3%*$5a -Ylf7T(dR}v8~=dM{+w`qG_jcDwrTC)^o9S_NR8LT*u&-Dv>UB2|Bk -*;}THYcYK|FE>ioUnr?$dEY`$mEyI~L2*-_xiry3^?sppar}Pu?M>YnF~Mb6Cyt*qr_QIHLVY=eN8r# -6n2!ST7;cbn92%F>atJ$Ue_F3+uoI|2%CWdCb?FPL8)65il1*c|s#=o_+g$Fq!+MtG+KgAL)f2IP34 -A0xaZ@;pP<@H7;H7#lbrswAnuWw;e|JR|8Pz8sQA5aH{BWOm*e$0~lku28K*i7Rn1pRW+@py( -5tHXL`6cqb%7!rRklR`ORiDzY)_dqJc7d$J}l?@zn^u3MRFE2z~rjQ9ZyAFj}=Z#~K%S1MkphQWSvS< -#eLj*g5EL8pYR1aAPY?5K_guUC5>JY--V|THTmgO2zrN?m -XPE4;`t=Z@uiPwRB&u`rl>$eHW7tsuu5)$ZPB5$lNQQy37HX(HTEL2^^4I7CMM6+JT&VnSZ-3F>Va07 -q?(urGO^ZFx<}u`Y+X<~p5U=yvEsaz24Jul+E_PfOlFzDdoM|*F7HHigxDa=7*r*qVGV)w&QOl}tl^P -*)J!$_QnOov$-$25c91#T&;mraRfGu^EbxxD#U68Lxfyyz_kOTB3t5f1$fSF~F%?Lv=ZIaAl;XvL|Q~EF98F_PZ0M2S#_WYh+9X-^)h|*iM|TMf~uyGClA~h*EKY4jxz^JwnUwT5o=Z5{9T-|H}ZMB%kV8jV>Yt6h=*wIS=A&hEAgSyr@C#@@9+ -OJ7gP@~E)n7#>Jh?I?u8aM}SHZy$nw+QsRmxl-DQndG74WSS`N0`Vo{{zfBdYP5wV^#`ELrWmCzgW+8 -WynxBLKl2%h1Fs39PF@@wgZG`NGiw@#ke6A85#5G(yOXIS!bWGhzv}&KEv()}pT;8}?KA%^^nUuV(yP -N?nDr>7?^5@8wSQRgFU^h^9OgVtM`?0v{ -yyLgPH1ufGt*V2ptu9To4x>;F{=GivYw;L8-c!OEHMKDmY5B%#L>6#=B7l-Hxs#(p)61brCS>(HAXs> -)vsvqetBA6b;tmMslw)Xy5C7&D@tmw%2J+#0lnUlo^?9$4XqAp#vukSumtY#`-YX4hjK`aGk*M{3YgC -s(uOVCPi&QO%!?z+5*|jjIGJTPjy46wqENZ8$NpW**Z?Zb9x$vhgM^CGwP00)2i?JA}AS!OYUyE59c^ -uqlu?)_2OM_NS3?N+sF$AAS#B|0{ax3UhQw>6JIQgwN=J;rQ36#O5wXncItxQ)p&%3dIP?9vxRnV1AL ->n&Du@UuEzU?-G@WDY!Pr!AA(Vsja^{>> -H-9jRSn8nKdDw-v-5xp9Ceeg&htCJ?tz3|757jRV5cY+T)B1K8tJx)>fFLr7Z9;7sM

s?{~Lz)_oP4U#H&MpXvvyVj?Y2ud$6H}F!OiZ|e`u?NQ3g}`~)jMGh2?AWO&9fODpSy -Nc!2um$M)h6`&hh?`_pfF%=ROWcWI9-qRk0Qv=Tk`7-3ZDoFJ8Z=r{H>9+TQ!7gmMlwc1oU>OZqTWpm -^ESGkCupNeSO_@6N^<6HT^*yeC+UJI7TBcu6}(cZ_o?abTnNB1mbey=8o&WdG+M_+H(<}x9eT|P}XW_ -w%_pDp2Bih{|8V@0|XQR000O8`*~JV^s9OJ*9HIpeG>ox9smFUaA|NaUv_0~WN&gWWNCABY-wUIZDDe -2WpZ;aaCx0rdvDt|5dUAFf=y9KY89c~0~?IGK-algfWi%$G#HQsE+f%4TZz<2Dv9^{?7QPbmMooN}ILL#Qapo -A=A}4=2BTJb(Y;3>N_x9UUD-r&MX+wgw(}EEBg}Nr9Y5P1P5sG^u%^6jh -&wtJ>CpdU14V#e@xAFZCKW3Ka0sS4-LAyfM4d`@Jv5aV^H`N9h)v|0W2K#MQk{d=^ZAhnD&6y@G5L`vAP96WKP?abA!i$${ -J(9Lzi#{t@xj;yzm>K>hdCrRx4Uns}-4(%NQ)fRHK-QuVLe2@!{;`{c?5s`uu$HVF?JutbDv;iHeits -(hAQo4euUjO7?q -$^-cj2J7v;DC`u)|(9M9+Rj8wF~DJi+n$ZkgNA?(R6>qEYc`S$?`y6K+08ektyU>^V23ij`Z~KI7)rxf1FRQ)kW^y~@ajXj7(*c1`VgB16EL?Pp#L}@V`PVbNHnX~8O8%tJ*@rg- -t=NML=}D(A%H#=K#XX{oJvrn$rDE3FomAy$mb?gveZyt~jDRjdMzUIKfi;+)C0V&^0 -cPIBCaB!Y=Y*oY~t%f^TD>)5JM&37l;WvS}ozeKB=3^x|@ -|=+rVYN5%iT!dSL9W?gWD81BsaGZL85XA6^taE~!nu@uuaGKBUaRWn4fC@X~b{ifbuQ3HOJpBq)0Z8f -8{YDCGmLPBCl6F8v%uNW1}DO~KWJaL_zSobl!)ZfyGz?yeY1inH8!&l+Z=J~#|qo&fo8YrS5}LGJ2r;Kg;ocZG6?Z(ZgE&rpJC~BQb4ugv>MsK6Gfq2+fq#p&_G)YPV35nHD}*>!=2 -UM08`ciX8DjHw6y6%3_M846eBXf(Qh_%I6 ->&k1Be^ -%ED_rcpV2vRQ`{M`kUQL>4Q9cUnB?krwM$s0B9ZP7v_D=ItClqKal5>VOSmW_JcpQKA&{p6)Ss0u(VY -@<)mDEtYpM%LKPQ?@a!NJsX+n*)}!P*Howsa>}7#iB9w2jgsaxhkRG2QE|1`|;9A5cpJ1QY-O00;p4c -~(=#)IC_^Bme-#m;eAD0001RX>c!Jc4cm4Z*nhWX>)XJX<{#JWprU=VRT_GaCz-L{de2Ok-zJ&K&AQt -bVyoaob;+zZ55kNbg^YWNlu)~3Jeh|2^9!104P~a{J-DK>^F7+QcCW%*LQr0MPhenXJ=>UYiAc*!RYZ -Qn3ZW(TiV=H)8F7B%A@~R1;(8ew?udQf3Iq-LR^LuW>Bu&dT{BX=IDzrUH-$uZKZR-{R(O!Rlequ#XL -W7p>+=bq#=Iv#fxTEh^TC;$@@90xhd#*2L#`@in^}2A8af>6sY#SYRaUV|hKhg83ZcEIoa?9e+1HIzD -)P7#~eHHgMFe3ijmi$58(zJ&WSFNajHI0T5!ACv~k@;8j-FP_|7F+}c%Nk}8S3ZgYXf?u -C0j{Nb!+{Kaha|E>LbYNStU@jj1R4k5lnZvT%=0@~_aG_GVScWX;*zCM9p)6ILr@koh722g{`3nRe$7 -P-Kw$vp?)Hko3hhQ8Q2OCNaRCUM)=ow^d?Ul6pP_-Bd|Z=@p2C0;0|j&XZ5Vgh&MbHzh!r~WtG-g^P6(cMhlQ}_ -+x(w!nxAlMkEY95ME|8R@4lJu@Qg1^YphKhnTRDuJkV%9n^e=i!cM3K<%Exs?EzJ9lbqziN9Z;*JaKc -R(pWFsmhxyWrW5Nmu9u1*Q@=bZ-L#2Uhv#;Tq?pk3Z*56{>6(59~o#7A$FEQtNn$(R4nbN{ -D!jsRyjNyGFcIgli(>F6sW`Y~&ki;=^L2(;(`p|GE#xv7}NX7v^xPOD)X6_-m_qBhtN{tOtBU>j}iY= -5~6ZV#AD8Cd#;#3j!{3*(@i&6a2>mc=EQ9$A6b#+5c2$H74{%aDYwy2&sWGit6AjE~BwR@+Wd&#Yj96#(H+4%>F$xIk&@IHAUdTEvq6hC__ -aocUtXuT4{Q9wVL=^DJr_HfTJU5_Bk>HINsqvH{S}9n=(s-Bqej7h8v*&SK|UWg*5f1{pW6BLhdA5LqWWJp;CRd-(0)>+cVNgAU7r4QzzK$!@a3zy$jpCkvz+(qX` -{S(Knt&vPc}PpHw-8S=F-f=T&*lia -za0xXb-9>%e=NA>>xLx|AywSRWboEAHm+0KoK1F2CiQN!%0%UDvUNL2Ebs((0c{OAWLwgiq8ymf??{Q -V7GD>NNREB#Tx48#QZt2OJO2R8~qkHGM0H-gbg4I)so^as26MoVr3>)izzI7;^V8uq>wk=Fcf>ypd3N -P*2bmtG?cAlIZO-SXy;{2I#LMLgA`mu@zphy;ZGGo!^Xm -c2B#+!p(Dkme8zM$IDzU#)P4gPI{fDt5d@@|kcO+}6H%TU_0*j6DRt@Tgfp~KWXKX|r(GBrbYP6Wk`A -=&{caI3}49(&=)Ae63WR#Pz_+C(`%F^bH{O#evv)9i}5IR6=M?gD5uL$JT!SONQzbntN`v@PtICwcd-ha^ -wrG%$H?vMVSjQ+Vj`s4WQGaWI(4QJXo1~H~QYYaCZ|n&GxN$YI)5#pO -eSU2Y?CnQ(Anfg|w_-tbTgizJXmBN%P6;QN?!<*#RUdsE!LfCYG7LXQL(#j>>NQ0`Vs(849k^ -stq&RGR=7+Qtn(10_$N^s8}cldEHS_p|$sHp&V!$HA>qi`Gq`)QitcoWDQm^#WSeGA=1m;h6=&I -fI#DR>y?(UZ5s$TrbZf=w(PH+Hg}b7hhFLY$@5o02yZ-4hEJ)epHXZC=4D2_EhWKftTzuncYbPBI5j| -XB`t*a;lRR!#}2>(;CCZUHk?~>xejyZY#{B@9)Ga4Wdhx2YIw*W~gLOvMSN|t$ps& -qp~y5g+JnmCSXmR}5s1bY}68U|vx;+Fu*f4MPij^AD=`Uve9)G2~h5EZa95JpQdSSb<}{5hDk>=!awF -^nzDR0|3XO%4@TCHpwbmo-d{;Q`{H#<7UDH0(g3{5JgOi@QI%uY}TqIIU01<6~-uq7O!YQ)8{(Z2SycTq -FJ)B=pGWkO3?-iHklBNobn(+va<5R&Wh$W|6jb!RgHK)0ty!_%vQQI1mpCJU`a-$IpTE;VETWd%7SX< -%EcwyYd!(>2C+TN4SZwigTZ@LUVu>ryE(j2}UGawC+IMa>(=;XpbT&Mt`I2VAPDZ&OZ4)0kLdHP(jJZ -n`U$<>?)dA@-y_AomFjY$RHSd0;8wWTOpa|#_Bn{RrG$?U ->RO>N47#blznuBbJnMLTX_;y8yTp7mk+(EhYM(EX9cp=Y0LkJh{=sg+g1S4$mGb~2wuGHtAxyI25%Jb -!x(>{&z-IIMJZTA9d0-4l7GJ^H9^+AMV4i;UbVyZ*&uJj=_PnI?&DZrTVB7$`T*t)b5AOBkz@k}hm8z -wOy!@&KsQP~P|AEGtb}r@ZMFS3`=>826n+Qd~u&0y&C7b)wjXLM}ukcp!Q=o -eAOL1UeB;X7mGyqJNTb=)BCVA!G+KwW2xSZu=i)a>_k?7+G^%SVEvh(O7|+0w)b+sc2&is05roL^`u?fK^1HIks7}(gH2VIEnF$))5u5Ks -HXGg+e~6SM;|IpfhbdxC%#I!{V2bP*DNH^({tc;7ABNohy$cbW2_Q+-hJ~6!?b!~Hv#de!Obf -mhG!s_*Fo;l}4|z88VBoT>+rm8I>!F)8$4_s@SaT5xxvC_l;W^lrx?g`NcxN_4M!s-fd(8iWQRA0;j? -0{7VT5P(v_Nr|z9_3m9$Ef=|LE}G@atCh3%-MH2DWAR$5viy3q~K6j*Pt-$0}0(uK!+Vk!=4 -RF(#&91e#B$OE{5{fQ$nLF4mM;wS1Q`MLRJgcG`$zy*C2dCRULs5PNxj_~bK;fvf9)31MaC&xe3pkYH -=J79X)l$J=v;W|VIawe-+B?*oA}l4yBQL4+8q>$k(V#OD{=&8;VG;>EDp~!jNF@lnP`dUt$AWLkJpEx)866f$`Hb8x- -Hw31toE;u|_M5j^6_a}-<=Ra=a2rP*8e>_88LF8cTm#=F-0uO)`CNpXCNB{6M2LVhS1<3VT65h5dR+t -Mdp;ZfRYu#dA6G5Bh7*u_wvt4wz*4gp;6m~FM!P+4W6|0Z^}bxhM!KxaCw3Fi`JNWlqkrgMHUG>RNgW -auC3U@gljYTL+bwOVBw-?Q&u)wN7M^~|aYi25uU{8-xKQg3ZhlA(_g{5xp)Lr+t$H7wa$uDZ?d@qz*Y -7jfq1@uPYZbIz|L|onC#LY>NdmspwHQ$R_7I<8 -6R#t38*-Wzpx7?aLVd?(_wy-EHWlEa2j@7@A$#^D`D%PQ~KfW -cu=99xE9`XG0knI;&$Ke6O7w51%O3H;1i8VEzLh9pg6p`|IcTdWK7k(-j{&%CzM-G2iB0> -v={LhBv*y@t09;uAl^#&6>~rt9PY -oulS1L*4%7a|c*AD6@%=Z`>C4z#PhGylYrdR*z5nb7e^vP~bgor&j+4;e6VrFH4wDEaVc&BK`l_b$Bh -h~!zdq!;UNxut2Gxm4a=z$p3GrYEvl`r1$pRInebj~ENIw(cWV0)f2t}GReq<3Qbi{Ri>%!Er8Vr3=B -#{=ec?y4;{UZf8H5Rwx9mtlfP-yGF|M;U>;>e)yWK}FI>h;fD -ZHX(B7wP$*@ES;4BLDJ$7d=~cJErxw|Ku9>>kLd -DS$Ipcowj*y2KB-wo_!(`OR$u^ZA?_zT3h1TO3m0e9J_%rc3mY@dIy$Z!5`wk7Yj^%`(EntdLJ -MiFAV?4R+I2!WUiG>I6(A7asMQglJ#*$^e2VM;Dx4tu7#ORh=fC-3Ix{IXu4vDHt%Kvbw0LXj!~5$9m -6ic9%0d5W8ksK({GhOfE4zxvv^1XtP*($&S|=oDc^(|~Jqinr=Q4BY8L{3V*=0su2iAY#~0rYT6~Z~F -#~Rzx?n4Mb|pKnHr2y#WF=Z>R1+LM;&hF5^m0g9Bs7tUHV;+DCkL-F?4oCrmHMPk{hKs|F -8@?i?v-($XYk1v=Ob#Wi?|w0EZX3+BX!F`PCv*+b;H9@ocpFBn`^k7^%kFa|n_SDJruQGcvWTtOkG3!J>vfJ2$z6{JAZ6dl7isPu>UA*GD{5#hpOs?QS`D2WY1ZJ$s<^aMj_*OrEq+! -h$!YgNBsNUn1W&*CV~2>xGYzmjj7xJ?V96W!EhBdrD7g4#L3PIo1GGuIbNR#sVVGJ=o?f~uR8x+C*f% -j|wu7qjLq@jh)Rbqv{iaTrDXi}-?;7`dKb7-~Z$SF#2sxnN^02mMC(c+lOJtG4QnwmX-556_%f9>arAi&bfGCQR ->`-kJ7q1(4cJ?YnQV;XCx!7W|Mh1qWSS%3Zj2SNFG&PFpvtp7n5_& -r-4$kiO9M)Hvyg#tJB8wYA$dW;?A^t|H#44-uJLSNED}f0I}ts*O1N>CmwI!YVH*dnxD<3~fpp9Hyz} -KPJ+V(A3mJ{@T!5?D(26jUnqc_{tWs(T!Lcm^c{lY9< -P50GPi3gu|;q&zVg0W_4p8f0j^+8vxNUc;n?S4Chh2rq?$c>#b9w+B)m8B^(7w_Kf$)M&cQucq(qn8u -LfCP{uK&eA(fQ`BV#!xrTCmru;Px1|f7&S1@K-0Heq&as3KLytHf2ehW@FEqV~_RxY%&!^t($Of>?w% -J0mn)rH*wnSO-(k+uP(Dnf*BWaAE+wHRCD6)jf}ld7e7t8jvOylRyXMSj0A;!aqC!8Mk~ymyBz)Ju>0 -V-}hQ>TSspZ%6XYYrgi5UMLDOgVP91ShbyS=JLJpd?LQ_fu2ChZtxFoR`|?&L^RxeRPUlz!@NQddQ8u -sA4Og!-&WEQnb`{Pz00#@g$Wbj66E=iUS*Iqavu=LHO~sX^cXC^852l%0K?lY&KX@I#@~ISHvf7!jU< -96ZuO|vn>3sbw)*d$51Ec{t2kS^2@ZX*jJt4T93Cy1S=|Wu6toYv6b-_p(n-#ZD9qjd -(vKR=Uug_hZ^GBDXiu@p~)NOxmX=s%sZr+M6aNr0ArJ|$Btel{&Z?^*U@HscfiyDt+6C^4bt9DYmsNn4Bg^)I$LyDNi(K-qo=wnX;n&0t>0pj_PszlutKk%Q?b+=pzRfC*R7~zM^Li34qCy>d$ -=Lg_}VGr(|B4?v>Na`z19-_`j$WA>cx0PwrNb!WlxCzX&AOnD^q*x)^6%c2MV#z>s#F)&k%|tWfaG$c -$JQoD&$F{iozNbfnuj&D`jY4%iR*hXL0R+?syoTkxTHa2(YGqyBV^-`vA^YW61m= -^WA)2y7KZpqBjMaEpqN&h&7Jr=^@}*zajM`d>+x42Ymu40wo?l+qj -T<5y)%-7$`JdOijnZ^OGp4pjFU8ts)uYfsL)2(d4PpH`hFyANP@t7!RDiS6U|t9&j2(DIZk|i>}*Zerk-nAuOwjzfsAunfO|At)^DGudu;WKFTI8pV%V4xN%N2{WL7E@l^vpVsmvmKF4Wy}@ -Veq=gx`(>Aw)nj4F^YJ73)|g>%P^9c5PiF6iDbaN=-7r7r6-!)$j9wH6&DWr-rc;G)gl!9FntbcH-Dm -omV)f9715LVsv<~J8FlJJgB#8sC9(Zj -WEoD&JZ_J3)OmR{Kiz&f{-f4)8lLT33vd%hGe)%|7M!JL_%Dgcb!F^zn%(oPGo|Bfpccm!nsFp;$6s* -u_}N()LL=&%r9pknuRTE4RD(^Vo6J){p{jQ*D$ufd~ItE(JooQOk49z5fdNHL$YoqL(KU*8uHJp6Dl{ -rhq?m+l_695)r+2o=l^oHtBM{h*IOYC0Lisip5Di%Q9luA&6VteA=7zg=cW7Q9jq68#3}^P3PFmw+0w -*43I~+dQRlo*gqI=AMR?kKj=n})qD112d)LNFIHqq_|8lS%Yk;cLz4V9xpUqfh#xZ9K^f3alnJF{E7r -5?%A4~|S!*Md3j9;45TZKXDz5&-+sAmj&Wt_<_vgU$(@kQx&Lp;JJO&d9Ouh(QI1X$b#;!1lfqO)}k+ -ty`{6Z!E8MiSv+Zoynnp+8={>wVdOF0X1Szj0xCBLy$`xyFFt!6e1)rQJ4M$p!a`VJ6Etm(f9WTgR*A -7!nW0Y!EFx6IM`A5cpJ1QY-O00;p4c~(=Mz14uQ3jhE_DgXc=0001RX>c!Jc4cm4Z*nhWX>)XJX<{#O -Wpi(Ja${w4E^v9RT6>S%xDo%~pMs5XuzdBZyO-;=FuH9oX;K78lS4Ks&>Dt9TeQurED55#yUyXhduN6 -Zk$QN&cQt~nEzS&QhV%H5)RV!H*)?axBlZ{_XwP|M8zR7W>bA4}nWe3hmCHqt7^R+E27RcCVb>R~NPG~EoNOiQxn ->zzoGvMNeZ)yEmziaMc($%-p6V=wXhXoNa~*RHLyG0?rGXHk;md=K_dN23vO0@b@B7YsgQwyp%PVO{Q -bB4@b}sYsD7<4Um)LKTN0gVdd#Gv8Jht;_a_+2VbS -jdQ?S(e7NdjT6*2`5br6a{ID8jB%$X7{WX2F)-(SXoD4OINRwgcay^FsTGLJ$>MbjD15lt#%(*=!9rc -)iQGx~+sN_EO -J4E48=13@Ks~2Lnt!$$e`YIzKGY!-x0z8T&4}Mvv7srgNG9_}EswVnkUTl?RR@8~*{BX{O4`T0R*rRG -g4jErNt4;Y!r6{jDT8=kWn$0f7mfe*R5VlLVOYhMa|hh|PXUF%H^&qjQg(L2l|S}4Qs!V~{~Vl8L9ky!QOCTL~8DI#aHpHW&we@+3zA*(?!%6B6oQ$ut?%AliTT~_F;o++t|B$Z -X9e@#tr2jXCu{L#oWPa-{0NDVgRMeRtXLg|KK8tRE3pxg6X0R+lXosuUWpsXr8b61|uxoaJ0{y^7FJ~ -?^BC44K@+xieuSJs%_SI~7RNLI%h3tTIL{HTRmzNWs!7RUPV@i^$z{R85AQ9dO+oZWQHt~ilku(r{5(q0qJ6z`Eq}PB-L=!VFz!(5YsR9l55N>iM0lOTVBFQl-p^xurB~BE`Ak@ -einlEnZqj3qjgENaeFfK6(nS?xB-O1vB=I|PS>(D`Axwtn1o6-Y;%xbwGD2FEOycnSp=v$*z`+ExrIo -oj|6U@Q3?c1za@l0ouUp`&;>Q+ySjzZkiNfKS&ZD)E+K}xi?aszgIw2w$t@~b#gXhYfsb~fnx(ySGfg -aFP6$U&8D;GOG?7JWx~on&EIvaw}pOuZq5g^_|7y%tUTS4|#{Ond-ETxPrmmVFFWpsE#?+i3EI-|>lG -*luN6f}!G}hOG^hX$4-2CUJROPoN;f--7;<))O5?VT(COf`>L+^>$yRhpba+^;3|ezl4KX!F3r1|MD8 -(uqJ}Jl0=Us3Cv6=KICj6Y}?*@J$}hIBP(sNhO23#$Wuames4>%NKb>niYF8Yx6s -CdJvg5GkIcv?O9bJNpSCcXCb-2{@5ioHA!!o7>I2;Y1Ep`HPVxcn|KBWq7No+2@E@2uT+9}SytSOG}?4Ri)M^A?045#d7*7ksw5LS5`)+r*b0a|)jf -?OuG@vCJ*i$ti;ZrhxXT4AGssumxP=B{95?zd!?Tk0K$H$(fiM;N92rnx;zBo)>%D+do<_m`(pLnJ{e -a<-THtWb#Q`>h$N=?``TZRhe#aF-V^2fG$0TwxZG$=uA#k?PCjIav9`sqAig5y8o7$*B)WX=k3|)jif53UsG! -2oA#9xEoL(p{s0D8b-(ORnc()T10%G@_~0AmG5dc7EnWZ~M;v=>)P?48gl04R4;$ipHd -1m)|2cnB)*}WcFL98Uk;XqZsM;XIBvrp(Jn5FGW1ttSIko=*XHyz-JjQv>aw`Bz -VHd#~x;)h_-$L}~P%&wJ)EF?8L;c1uU>gY)^r5UD6cZ;WayjZmNCqMsU<}_28+qllof+E_xwD-0coEa -v)sc`Qw&@+7t@87_O#P+Vfd$n&b=Ip7hd-Wm^;I8F1_Bq1Oz{=p9K<8^PzA%isOof=p6N~j7k?m2LgZ -mb;O-ww)9>_Y;jk}Y6k2{>!RlMEOTY3>ASV}kP$<3ezL`bB!! -?~jQK8hS%5{ullHh_=()QHdc-=C9^HJRkwR -2KZfAVGT7fFNE^v9RJneGZMw0*e6k}>t0Amug>?D^}MajyxRLfmiQdx4=d -$x}SfWrM&>Pfve$Pa_9x^v#Gx>0BhsV^*le=m&h# -JLny-S82A*#d4*X|7yUVKRbFcg1^3FzlbE-Xc%eOr% -V#vou%ie#)PzEbnq&-bs}gk*a=eWlzHlqI8{wiq$Uw*M(RP*_5809f&!%v-8t;7q`4z7FMREuuV -?AQ~ee^l>QH!TnL6t;0ktxKzK`i~ZDN6{yJ^Xd~3tK?nczdo0USAiGTS(>z)UWKxj>Ht&D<9$w-uPx^ --vZq+t}+l@$GJnxTx>H+j8qY=}P7h4kDFw0^QX<3y(-yE0B`9BJglZeWIjYfzUNRL#LWRX5{k1uF9B} -lwSsIX%`zs|)y2*i?0=kXcw7ZADN_dI4X{(aI9@Vn2>|fF0XQk1OnfS ->uw#KLO7JPz#ayAQT4-HWS@3u_*OfNEU^040;YfkXQ?mOVBluaJCU@1-hUiC3BJh)^5*9c&qjX?K7HeVU?0_|;G4nEfA9vqn^$iq*SFK#vp3flr?&(EFg(~ -{ui6R@Dowg~=&#dx5%WQ%nTNiaV0JqI51;bjtfsNPrgNTN?k_gTBoK?KRv12UZ*^pTj1?JgcK+}~E1Rb)Y -fithBvppF-QIabAnS)Bdxjdm@FYX;mhL*@ZSV~)(Q0@Q9c}60 -W#&fcsEX>h?Gauz6NjHNb3nh*KK?8*cUm5cd3<0)$BZByZ0a8)ULlAbYj*^kqYiC?N!%<+os;$e6M`B@2y$qZc^qi^UYxJpzZo -?GydMy%P;_i(O$rYc4$x`ZJsNK`4NQ9bBeaYj+(BL)&r5G|`BzkYU59jk~9D;QK+RBsLyx<+)f1Yord -6Ar)qtPFN5srD9o#6VG7~ncVy$m2&|8NJ@>%*Y>hdanY48}RX-Jw8$2nj;yxv!!)oy{m5abF&=iPDw| -EtrJC5KSU~C07h=H)k_}EKJz-)y?F6(AXs)OoL9CpUQwF7L9n8O7Y+gkn_&-&*6ROp*D8~R{JjiGi}0 -mk+~0Z5ki=N1n0{4(G_^_gTH%&#=xoO8;mnIfTkT5`i9MeUSpn+(?r2EdrOJ(Xq&wTvh6*)TYz~@{B? -8%;tG+?2{|qMktNY6Ct<93o`kO3(jsn0r)fq?W{D>lH1iN6Qr}w^0tiJ?F(5Rpy&j{0WO=w;hsR)kK< -NP^XQ%T7)F9C`!I0UQ_Ac9s7Y$t`5U_)KN8I;x6oQ;G@rdV6a1^~<<8vw!U$0z`?l7okyGY40LU*h;v -1YJ;lSFVJ1{jt&ZV|X){YxuWUaui-$=4X90UBu6jp>6p&JGH|kQ>A2%=R%;gJ1|>A3UL8Z?JbcNMPFD -h+Kb~@x*2ddXYue9&8bEkpY*&x&M4X@_O*ZYcM9_8RSz}H)nZ{snq0(ZaVy-&V&NUM-%W&zIPsqSZAz -=7CV%lL(%7jW&ka`$`p--vAI{Y3$e8v%oUIJ}W5$mKE?kjMF -g4!tkHyM47Q^eA-jXh2p*4dflHZEk;y$Nsv7FBvW8a(ZM-m -+#OemSU4_XVg@%6)^AKozndb9l&S_r^(^HXTg$iof$9fa!bb8EIN@d66@n*9LL^3Mkbuj1S|Acg&^58 -Lg_Oy^=vx)IR%#)>Bbx1z~k^pJ74A$wd0)oyU&6i_yKZ>x(6UJlo`(q`t@j8rFo#KIB(m3%aRlwnDgX -Ul!hBf9X4C;HHhpTz4|D|hrJp@dIi06jYZ3n&ZFP%3_HK*QM#en1|BRhAs9EEa -Q2B3~7i1MvPj=G5NAZ&omD)ZC)4KXBoiqObpjP_hNpiDzrXsG=OAD>fOlH|VTHSC@&MwZC-X=-Vk+P~ -SUye`e=GB(MJh!Y!0`ul#ay`FirUV;dFb8h3v;h+od$-b}78zeI|?XlMeo3Qi~%<<5je-|(e?5KXx8j -wCy7-NOzTfTm_(4N{Vu?f@H>6?WqX7z(j2u*|V?fwe^p0J17V+h2RyBVUi=PU~_6Z4bmIq3fLUOX|Nn -x9!SmU90}DqCP|gWW}naK)|yOV=SRk;D|zE_%Gtru^KS!o8pB45<%%H=7_L?5qZ} -@I0t>|PmUcISh?XZ(D2OtL7CZUiU6vTF$5O~7Q>)Q4ZWE`QFQ86MOvj6*^|6+a>K!+B%i__c5C3 -|&xJ-I!-U>9e%w`XtJ&7V)-Ub7n@>bs{$XMa2!=&)OpjPlC={qMc-Zr+?;G@*F}?%A`~=L1b!wMl8;X -dM7sDIg>U=(DW_uA+}*cusX6LbyRO4DAu4M;X`>GSO84(BYiT#T<#GD*`~X#0lhuf-q)_{w(2m&=lrd -*q8+>0`k)RAX`I)1e#<}j%&HYCt_71f?eRp)s<|*zE?bSk3KmpqVNsZ`;BqsD -#YgcscrBLH8aX$a-jzmMq%$5Z3v(1?ZE -7il!~>|Zlx6rtN7W#KO2jtt6^2w)*fPh74-XHppY%>}t$rnvXvP3E26ijka9IbSz`>A8xkD0~Hbh`_g -5;dp05-W$3e4k-1M6VXo754xdfi&`(8*g6Xlu0ywyJMs{!RcoWYCn*-X#Lxzot=CL;EtH^AO$4FJWBh -8SP-qvx(>*=7X{};ciFk5yg#q?4WI~3_tOJnY4*aY{;TQDgaRC3e2a%hBdv-h{5n{X>6@uEu_*kaLt! -_ftkAAkIRV8x@dztC6H$M`8TuZacp`dtGw=s#KA5SAru{>X|Q!lH)vJN&5&k7SRTYZs46K0I$GgyIh_ -uI8pn2r;NBu-4*ySRW}wR*Uk);@%W4%cKum$e93ThY;KQ>|Uj-5y0328*ge$vXz$&(Sa%$2bWy+Ct*^ -ycqYQoIWt<=FB{=^Y}IkJ<2)%u4t-{J-uDsK2;)}+W{$SUR<^3SjnOCL*`5x_kQF;LLz=&-c@?(t&d{ -HL0Uc2@FC@5=nx>08T8+bCF7SvKMI!i%!k&Uar^z-Kz*d_8u^5C{gQzb@L#|GEfBs$i^Nb=(B7U00OcRelxObRMb -@ai%pe?Yl?Y)u#z{JnnjN(RcP~lnef`cmlyn9I2EjaR4es=7!~yG=TXT9k6A9!H^sL;)K4#-(oZlJ!1Bqq#S_ -jOW=ixe(&)Jy_WL22TP$TP%mrh~aYiwK|#wOtEV-7Bg<5D8>+B?Q;zOWcj& -e+EVy!3Ga$qIiEQb}Kf*&zXV!T6l7LHf#}6+JIgrB7(|?iWA@h8;iM!h~SRy;NMS;2mIAynE{JBPDnl -*3J2>0LGmnizpqSLw=iaIwyDp%e==iYo`WOq_HX7Ir4E*j#q}QPYlT_6&9PdFr$PofzGf`4DlXe{O$A -SL{-gghThgmY@p8{)FTqVYR``SFon8`lL)H|ordg|R<1(LZXxQXPg8ks?nIU$uL&gHpiJqr{+&h?FDw -j+q-aa*?<1`x?aR)kN7zs4)c$Ff|G_NxsnOn2MqU9Pk0q&ah?94b06)1XcvT)4LAgGBYK1@erY+|No@d%DJTSQw -QsMKq-njVUn4RH&v%F*9p2?WaXtgBi)S%4JAI5AUYLHXM-(WFxcqUyquPg!t2pN0C9kPX!{b+Og0iE4cJR -3sNdsM1i@xb)Vw(Shnz*<(_%H7bi7i6<|Cb -6G7Ut&eT>1D!w!U%q}_Tk_Eihp^C8S#rXgypr?L~Xw?8a^FHPuvHz8NN!n-AG_k{Ymul;DTa=e;tO-Q -0%+n(CkEkab-Az-sG{jb;WJmFUB0jJHw8KmFpB-XLmalsBXrg`WSGu@inAhD@a>Lk-GfOf7X7p&rf$Y0Ch&wv#N0$&D+@4bIngv8t{NvG^}cu@A|soksmbdXZisfJ2MX^py-y;rnkJZStK0T#kP -6+#OvWBu`EM52lAb=vA0a%^`NcD<`Qn!C-jP1mAm#1i=))8hjPZ0r$u -AsESBGBg^Jx%}XVit>tonBAaBBjRfI$OXDw#*Xjukv>0t#*$zekfQS_gj4~$C#6^U4X~>P~TmnYSA%m -=OJ1mf#1eBZ5G`ZWo<4*<^w<0HdfkrgkJzvew{eb{r#nx?*M0Y|Lg!|;s(8L*%2708o@!(BvP5`vr{@ -Tg?sGuN$c#BTKR!(2-F~)~ESk~WLGzNG!F(lGoQX`oMS9o564o{Q&>fE*$x49Lx)H=3VL!_zLm)tvf&P)h>@6 -aWAK2mt$eR#UKm)&iHg0001b0RS5S003}la4%nWWo~3|axY|Qb98KJVlQ+vGA?C!W$e9wd=%ByI6V6+ -nIxOc0ttrS5rU#Yj4mi~32sa_L?yTwvrB{otq|8OwHRgqD}khwwV4d7*lJs!r?gVE53PNkhqm&=AK)& -F*`TNa5z|67wp8yrJQ_+!77{Y=Id^6^3EDn=zn{SR>|2D?NFpL@gRFz -@&Gul5V^S}S=fxoP2uVpc>q`iCfe#7E-ufEUs$hy3>4c~pZ;lc0ZJ@nwCkA9cW`_}5b2I0}XM;^^9yQ -eDeJKtTkdQN6$dcI!9*54gn-_iNYnTfyUKfi6}5qN%3nKm;64xLJ9vh7-8A!E3cGXWk7>MBh -o7%MZ>P_DA9=`!hJOq{hhM#shv&o --dV{d9=Fu>D(5g8Ns~+SZgmIm%_k9lh{b@8D)a#t1fiZ_~!Hfp2OxAEETxd?ix`u}!O#2FbX6}V&?$_ -X)a~f8!`7Qu{mZjl~Be~L9vl#1Xs{ybE|?i7#QqVRi -6qx!iV(YCJzHyiifkc1h;QNdI>}P9K?m3ZgL<46Rm!On#1%s8M$)M{x10rOsyjkttRt7t7%|#O~3RcY -8t?rHeyXoqNYPR-%yh&QIlNBdhKeke9+Auw1noAFz%(K*+z2&Lj0sxF-lHzu!2#J2P+J+i!BHT%L)gT -_vPryS}Z|4JKN*+{S@j9x(4m4?*RuCA~g<5jXmTMsK$Q85#^o@`(6NurDU3_s(zza6$Z(IWeO7YC9nnUU)EEcII07BkfPH2IQhOD7Kbv8y^52!s2VshsJ!C~D!$_`P8R~)gI#BcVDjw9T@BpTSUh-%*zV$*CC!tw#k3Hn*=bj7s-hggM!d -W+|#%R~s0#~o!0@c<4@bkm<5??wZakgA}##GtuXRfOxssR5FiXStieBGvXN_sKe{9yi)Ue#VVW|CLrCEo;v>hLPjY+~}kbE$FwGDO!VN(^Ms^ -Ubel8CDuE}@TnD!$ht2TljQ2bLZ(fDjIN%Vm32q7akYB>`Kn-q^KS{$PMey^KuCg6N!ks}^-2J8vAdwQ3yb(pZM;0k$BcRtq>arV+__pezgw`ZZd9ARhzfww<3EfMC{U>yq41nIeqgfoC34jUsmUV#JOHKxw)32QR4Gw$@3Nf -n7>aw>4wBrq!-8{YR+Fqv%Lb@pv((eqK|x}CGs@!Ct6~!mbiwzrX_BpiQm%_4ZaV@Ihkl#{t8^f)toY -@3xIPm$85LP3*_eYsysAG&9On6BPtYYDE7XJL%B5gY(9a$-MiEcWMRW1^0A1P{B8yZn$OSm?M50Pzqo -`XYQq@0EI+?$Ck}_2lLycc2k2dl-Bh)3uGS2jTYH^_h%#HT-G?q=yCK@9@w%fheoJAP5RPUiJl08 -|Aup?-wqz%LAzv%p)qeM~(J(KInWouBAu-DLY1jnV}x01s-w!UvEcQ?6yjc2*vUy$kJ!7r)gv7t16Mv -N(SOA>eaWKDQMa-7V0T;1jvrzEtsWE>Dn^Zv1HrQpd~BjApVy1GGjcya -f!mjY2^Vd80yM~2Uz8$zt|xTs2|Q~L)k`Um>LoVqVCU}k6sF+3r^?UzVwZ7V&80lf9LC$E3Jsc9y7eN0fio9rNp2`9cmQAIXwYq}msv4nt -PdO!E)*O(7MFWu+Uj5hq}XW6aYzXti%Wq{Bne7AUr&`gWJ;~H#9W$kN-NRSVG3eSzcI+)#Ck{>HabaI -y%b-EFMupWX;`IHs_wxyczv@tAZ1S#xhV~?QCLN;fo30#{RU9($i&$s7|%yy*IfmImDOjn0aBe)fi_j -C@r*qLT+Zr?81hGepOXhcq^pU@Di -s3+7TEPTaxEnK$Ja@PG($g9Nk2j{AQRAHV0o{XtE~!K?|7RZ3 -nRhpV{v$U>WgeVu!svflHD&B>mlAj97Pk-)X)<=u?y=L4D$mFpoBuy;M$;_N0zCSlD -fiOwz+R}V5Cl-HnkTIy_sM%LaZ -DfFmsu#;X~sm5;trV^4FP9uu%3F0TsE%dsJ1}E^qEMeOiB($2uN~2AaYCWWdvxn22}nN5Mr}~l|7_TM -S;FY+?dS>B?X^w`^*k8Ueb^$#_!(%(>ssb*8^<12Um26&cNst8XbZ~QLVJ`W%VE=++OeK;Umg?Bxr7* -$xUv!jbR|G_MQs-|29g`+FA?&Ny;sI5-#& -4}Fvb-`M2qUwKY=bgzIsY}x)Jo_YOzU#`K=DAyt|H$>0CMvLM1YmSDY?8hvY*K-VIoJ59^Dc0;BuZ_2 -Sudr$L%R4#nIlFD!1>%6u%h+Cnq*)=Zod22MAOBW{)%v(@1nwg2g{^SzW|_{8cPkvFyNT$!ZkCdEy~+ -^Tj6MV4J@157hyL4IsSop>W(Hn*&WC(8`y@&!>eyPm#?)K`PHW+Ice+X_Rh)>~}O@TniaCy|>aHs*8| -^N*Si9coU+Kw~!m*q0Ym)hz;2G2wq3K=;&#f-aPaG)V*P%{9Zvv_@YG&+m@0YWen52#L58Ut5MR$_B> -Fme1PB)TuzKQfjaan;F+*+359JJ?-aKp4>T48SU16Fm5suqNyqqLxVOKEm3@0 -t;LD_Jb^Vs_&=Jr8Zkh#dtzz*+?y=U(}~eAa%dZAHGd{}67#^5fgm#Y2Xh-{BiXV&HGqVude6V -s04gl?TFqWlf*p!)E$KnY0oFLOv^z!wETrzb}sc`#W!{LqnTQ7sYd`{e2w3PZ@)uneUeG8U*MW -H!J*vaLfilD+G;7$I9Rx>hehNj-i$KdztTlmyVQOLB2cXb)1J -)wQ+o6rMenIvDv6=;rcMig@osDu??x3r2HYEPiACiHV9DH$?YJj@snU(5-;N1y@+!+oL4VVNu(%O_?t -Zx`wL*Es>-i=-b4)!bjP3KAa{U9B2~s6?*Pj{ZV_d!$7y*X2Uoz@e?hpYl3h23R!TmBSpW1`E$|Y@JD -W@W-S*?l`p=1RW5cb%vNX0B6}=7P$aXCFup`1xjxtAq`zmAHB!+4vM5RR;#D^GSft$%)z?3sdYffUOD -o@gGEyZSf)wS8Aw-gyzaI^k6h4|i!4gW9%g+1Ms=Uu+GJHwK5#!PD4UHoK(16 -f2y4qku`!$Bvq5mUiu^b`$()ODyC+2T8d=B%)^7y~!G65IzsQxn{%UbR<*ikI&><@-H3h4W4-W%u*u( -vIV|IVJ)e1u@HTKA#@T^>(E4g}=)TpZ$^=y!$TW~8{2ja_Mkp*-XbFF{J)ndTW)nJt4j>bXm2d_hfSG -@%lT_H|ZEM}PNHk%C#4UO@FPk^ys*ElFQ_QV1J<*4m#SRpp{GW@hxabd9^cl>dEnjG#IdgZW^Dn~F7D -LABL&~?YrwZSMy9In9)%K_Wa4- -pnG?#Aal5SLEa5qL)Yo-*c-U^G+I?2#{`FmC08w`g_G;YA+D!Gt7A_i)Dml+94!F0-7nZ;En19CISVq -J8)Z26IMl@?;Y^WW<8Leyvia+agk0eUS~`a!85aK9-CE)Aj!h2a+Oas0qm`wk*uvVuzC~82@Lew{yGuv;Br!e&B3Nhmk_8yVr_;iprkJeH;j*PzUfl@7TVF~37U4Pso -q(X(GF@i6r-i#yOy-~lxSs+#^PBi=@~bh)7Tt+0syw3>)t(V%c*zW8G(9Np!u+;+N!Hy|-1U0MOF7>t -?p_$9i@O^yCK0uCKc2$X7r9sg1o$Jc)*H0fbs$l>K#)9p189u<%q7Yclx+Qmpo?|mCiq%SC^lV6ztx6lJQ9J`)=D>lDp{D%Q)HIT>d-LYLliLf6oc~K7%z%WbGUsAAzMALg0X -V6-f?K;hC>f%%nU??Az}>bfFIV2MT}HA$S(kn$nBdC3JB)R;r88xncD&O5WpJs%(pQDX2wjy35D+W7~ -mbc0Nl|vSP<^-niQJh8eCbIWQf)(U{A1`ItGAbEW=IT#c3q78gS_Gk{c^aK+z -gE);0Lzyn!fhSE!STYPjE7kQTd>F+n+YK~RlbLny9)j&)%NxsGC^NXB1vUaQW8I#7XB=yuk2=X6VTc* -N9Q`5+|zVhx?m{1!*R0V0_4G&FHDjnu`VdT<@m<0NG+FWXY2>?1C(pvT3(^Gn*nR=)z!VRm#DlSmxXv -i*mNj7V1b%~lec4?pn}RKT5BQH`ncbStRhavYn=fGCFIGAU}a)W9ZFi${B*&jx>zR&IOqEpS@Eb(abg -b2A3wcMXI_t6P^X(uMMc;R&rptEKPfyh18$g2h@+c8opeQYu`60#YeLAxw5z~IakE9c~xHfDL=>&HNnO+Qf>G7m -y2q%fd&l2RlS(0(ZCy2GqNGMI|lg4Afn`wKV>Gt8g;N|;U(%ii|34zKqR5N8Cs#9VW4HCJ_YD`NoCT}2ufpx}d?W;&*)()2{)1Ia`SECPx -RIoz3gsciEQw>=~hW8JW-1Zyu~bLw#0!&ch%+6b)F*g@6{vWN{-CtHzV6~@BkMd1e~c`8*61snO)nxs -7$OE_jl;cT+j9ml}>>*e{SRUY5WCMQFlp}J5mFh}7|vJ0!4KV7ILKZlSsU%5}t_3D7WA>1v*ZDDEg<{ -^ZiAT|Qezlt>IxvnKX%$2K51yY-0KT46soW}LdC2=9w;4cEUu|P*Jmj8vKo8~CnwHSxtk^vWFv|E{27c@=me2Y%61$KZEziY? -pjGT4J*9M|e`P6disR04T;-k%2TRmCx{mEeqW^Qo6-2)wBRNbHwVo-r%Q!fEx;SLuE#i<-_!D-@a9*D -U9=(^-^u3&~&jmbWrE|e~et0evHNaLU&snSoSX=twxS3v)tq-7<5F}Pn1*Y#RVISg)v?{f+BC#C1f5>Y&Xj(P}Fgh|6pKJf@ -K?xN-viwy*)Tg)6z77hHm`Pjblo~e_)d}dsvNzGoflHpGk`VL=`~Bh=&ySBxsl{P) -GA|6bzLUGU`2TJtL_0ED)rTw~&Q5Q1y`fpdhiG@{?J}5utI}KHC^*-1qS?s;_9?3qa>WhR5SKfZD$iq -@Mki_oIkp!3!e11&QaD9Sb1bydPum!jJI9s~J>`YR!ch;k3A)hiBIM59~*Zb`uD~JeDfYkXgBOKCu4D -1Qo*?^6pGM!bTR;i0aiyo`Xc4O{Wm+pG2MkEwe_=;h`lve}-Eb|F#My+8LZ^A3mf*vE^dv6a+*|m+P# -##fU_0wyPC+Z(*Q0uo?#Gv55j+V9WHNlNMQLP>p&5GJEkirw4zhoW -}d-NAhL_uId~&J%{!SwJWgkKS}{J*J*cWtO -9iAE;J+Q1xN{IN7`61kZH^O6Z*{y4xTHx**GrRvJ7FK{clp2fztM08*b@g~(;poEAJqedC-H -f=zfvUW@=B)&b082;;-DwF7pynzIQW!&>%^Tt^9GKVF3*0|piso8QLUi!)EwABKr@# -@eWwHTn#-Ef$s9_6^=GBAhn=sU_Ud*rDL}O>BRSza&rs;XcG{1w}bqcqiD*j!( -*?vSZW^^Uh3@&h*>d8F}*YW}$wdAeT#-(T>ANQL)zQZu+@l>RC5ZB#q2i^STf^XwQ!j0FUd3RARza019P=u=ZHKJW836@_LuLvbsIwa~fT)JR`@U0m?)E`D8pYHaU}SDw}@?O8XMu3!GjQWYzXaw^9X`#tGw~ -0&@9pP>{A0J$bh>DG7-Cwp!K&yP*bYB?|Q8n;)hdyr9%cD_Lopv~sd|k4;T1KN(2TQ6j=rjK~x%;tk1 -_98{Q{*}!9Ufl_0DBQ`bVT3Ce#R6ezeEO-F-6<$Jq#rPKo17{%Mht*cN4JIySV;W})1`020{F-tnDbN -kCQt&kXKG%i6+fU%{o=*Hda1_t4w>}rAkKkGQ*xo@Ofy4CiQiwkO^9^|X?CC(yWn|#yV5M0K;;DC()` -!ef3J^@Yb5IJN!2nQ75ZfhJ+8~Cun-R0zkzi2y=u ->Kyr3Ye$IZzOL~f!yjD#YpRP@qJ!4Jpp-EyE0^|RlBd;M*44P&QW{FjsHkg+ZsxajQxni{O&dw3w~UQ -c=3jYbHR%L5igF}kmb)@1_Ne(_u1(MAV=bWz}U -l@)wovr7o3N@_i~^_K-K=DHYaf`}T={zHLQLW|cY*FE`0$`8H`yzAXrU1>IF1FQu8AT!Z$v+|p_Snl& -&{odP9SJm~6KwG;D2_+Nf`<&)_jT6J*2djxxH7GBKrHn|GXT^aber51ZnE%V=#ZP7j%bo=NwFgDyI -0&ZM@<#-#f=Ug6&_?H$MY1a{ccHTOUQ?sW@cDAe=q5ikpibTKAo9 -;X1MSr>L>;c|LrS4^|_?!~I-Nu7>sbUNr~f#ltL+FvO5&4fG(VXqlBC>(h|(f!slUtVPR}apa3vwCem -Pqg(aYPv}{wT){#IuZNs?KTIvEvT`l=KtM0{9Z~{Gu?a|)c+Kj*d>agPw!An$xB6c9(xtxLjwB5vuFs -X_v#X?;I#m?P%UFP(ZJ!{owTeSSyaURZD_$Dn&5pmXpA($BKDgo!HaP=YEIwgogrhzK_M#NDu6l-f6~ -`D~UV#^7_o}B+cE(+KKxPDrHTRd8Qy^wP!tr*fkA)7|JBS5(S1tQ;)wNc2tqso{7V7)`6b8B>8TaFpSIXMiBQ(cWx4Wu4{bXENf&qqC8 -KXb3ov%twLB?te5VW{?AEw9OE?_H|$`PpR8kX}B^BsCik`T1PbR61Ad_h>ee>3;>VP@bcQDzF?GOW&R -F{(wE?)K)-!_g`32LN6~b_o^QPkH4=!&7`jj{?F(}Un`SXEO5Ge{nLF5Wr~+R -7so}W!ekt$*sLF^i||EClSv~L00=K$?(fMOwly^VdIhB9&(1Y`a#k(l^fM8f -hj3nD0pnz%p*>a;o>EuwJ})y)avhnzezDw+u=Luq^?Vll -;Ra_R~R$EkoU(w)NYgbZ*~~U>Vz~wLMmhz%OCXnAlB;g0C4}aIaqQ7+NsJLd-&o?7gB$e2;y>YMqbY$ -$Q1Pk1rSFYxrEyM7Y5FY@$;Pgkm8UD_ponxXN^1j|S4##w@mzO?1N+`$;mK7n5o6z*KJ$rqzp^jdO)u -vE8g^Z%Srgg<0Zu4d!MI<|Tx|({LTeNnuuTEw|FZBV5ZI8u%{PGLr@bP#2+*4*;Zfmck(BT@Ac`9JV+$f&;1l4gVmL2KNem -0pC|m2fQ=|jI1h=E<0o@Llw)aq)!>Pape+V&AIt0jxV`t&DpAuyuVl2?X4C4@0uHiHxRgahH>;(x+u; -8ie79fK@3Y9m48n8;PrQeKapF+lt?i*0u#Ypj`vlJ(L@Fj4Hd&&mgX-jDVAw2~`8vEe(hL*aCYn9Gk4 -dvx3ZGI7*&zm}9r|^~`0W@EL_5)I=fL{Q5Xdkd^MABS_2&eDD+~j -ZkayX14poh>h9m7#6r{Ex+Mqs{^UDNXfW0$ZA0;B}!QxZF -#RbK|)Tgigc6;XheS)=N{McAKJN;*DOZa*ax^Kj;|LFAne0Jtwx+fns|6}^XJHf`6>Do6teXqtJHb|7 -o~)`b^jcBXdda5{!0L$6lj#1RYEiDO2v(j`v2DuPDW)JIp*3rXav&JMf(qKp_2wZLr3^@-Y>QtBM3ySIH_b2P?^EgYl_|w( -T&kTZREjCvJCmHVTKe$(R?_dT7w0p4su(f`P6>Z6py_xY8A}ik;A{e{@4|^f+px>Y7yKGA#+&^&C|SD -<@GMYLb{Y9>e_lME3E!3}Ll2zV{Gd#ij8 -Tj@!GV!ed);>vORaYCIIxwD?B;xR&x;hbw0X2TG7IW59YBDZz^N^}l{sRNR;7hWnn1Y8u6KyPUhmM?Q1)i;l_(PrAo~GrU)_TN(*FUjb~LU=)+Ee*K(tf}1^T&Be(nX@Ai8FD#J&wJ -C&r$np!?ESFl7(CLKlEB7n0$TQm@8*aet!@H#purh8rB)e&cj9&n|Q_?eI6_b|MS*HlX@kmAq6Ulrwd95uVU6Oai^R-dC6Y|uTEUy_p~!x;z> -Mr4OMlVsGqQgShGi8L-5#okqTS?Vubb?+7E9*#b;x09vl$hq&DQXzwv2Fav+d=VUw)OzFQH}#<&z8Ed -lNIZWgA*;&U=LZ;FD9}^&mszScqF9^slT-s6LY|qjuN4KsuLjVOVcgb6$Wh(@TB0Tn^H%wYE^RK&K2UL5vt%Lc&QHuJveX3Ynl -jH`I-wZnUc-6IbH4X%)mn>}8xRiDWNisv_^wiBqNn42}fw(Q{%1+C_o#5pP>vSWREpz-KirPMKnB+iJ -mLl`(x`BcFp&c^Cx+wQfy8gej@5KcfNc>zm5G8X94}fnt84;^F#rG%P=dzrmI)Og41}@6QtlCTvJ=TA -Ve-7F?RAWQvz2Y_Q^MUT|r5t4%SrGfau&{f73$Cf=#{kjC+z815I(`X87x&!dtN-$C!GFIo%B@0K+Tf -0ew!2gJ$x^T=<|wn%O#P5_ShGs)x$xD&d1kO2@$IOCW{FT39o=;J4P)SO)!>2l3ONF{u#huqkSySWG? -E%M0}8nOZCivT+6l<+B8I0F^@<*H#DfU~wL7#Y0t_$??#nv#!N|Im;yHEO{ivT8MFHxx9#c93`Jmmie -{;?QMb>`0K`0*G=-92ykH;x>@eJe;T;#=NPrD^7Vrd-A%)*}PF(!B%=GOIE4WvZqhNV{9Qyj*!;|(XJ -C85%5|>~RxAKG5s&J8+Zv3A41$T2xKACvna+HGT<2et=wG_^K_dWY0mviB`ctG*Yp~Q7l;%P~_t(oe)k}q97*E9S63)rz$ww$My@FR+7n`P-osRJ -OJ}iDSuH$IR5GJWC4Wu12l7v)kj8H4L`sa2|C6_d1%)p?f2s-8errTjamU4)E70;PqO2 -`A}UlM0fyG-&5wNjH@-FOa#10)Nl)%CvQEEyx0`--)B;wGu@;=y^HuSzb)nW28vSH9NB1-?K5)Pl3qz -yS@Uvu$zi!D -lDw2!2$WoG6uobhox|_kX>5m{TA{KAOO=4lJ|1$C5)v%}qBMfw9(2X-Z@;t;8UIFx~C*>dPW~fb%$&D -}jN4~r=@+CC<5?twyx>(fYo1cj11CIGL*`%k5PYj~8P-;%RM{H^(D?kD0NyCCgLhSH$C2R&>R2Crtn} -*2x|EyVc%c(nq1fB#|eiFAM@QQwf|8DVo(3W_g{3JFFBI=5Ah5O|v_iJxg!P_`D+3+?D<6g`~Cagf=P -JZ&0Q2?G{fTx!ufVvmUBMXP)?Qi1*ke>``8OtD}Q-1O=yxo9P*-gd*fczvLS-*+OUqRGO)FT+xslS~& -f`wsz^4Sq;nQ*0Bq<~c8L+r5}X4jiGzp_`LP+@c|IjGPLM$bd!w)&3NLidGKtW93E -W;pk?J7NZoWQ4eXr*HL4PCRPs3G-9qtE_C$sv%J!L;>W2DYv3dg`4YDc7BV%s2yVMxTLjn9oBh!`+z4 -Gs@7-)ezVjb{gPD>`4K}p}xO^>0Zku5No=glFy>^Yh3g@%hD*W2ta22+aACJ*lfI1H?(Zu#O9#W%f?B ->jZtTH?z#q8AUcCWGEHMmo67qd}_kDn{T3t;GF|hs1 -}dVSi31;%13il~R;653xic*4_s-9ff%G1c4QkEhusHP&%-Y$tjc$>LPeC0nxbXuaHVFZMDR4g(=TR<= -Rd5wDr9Z76xk(Wm1UPj4nWdykO3A``VdV#WR-zI$SbmG9B^sS{YW!j66zR)2(DHbC -HBhSNWu@k2OKVB>@qb6>dVsQlxpg~;z4xCxEh#fc^q83_om6MSub?nrp5KhRQ`pn^4!YDn`X$b5bg+R -(%U&7lW^Dw(S59hIZQiIVOt5KWYSZ!+Rjiq&v&oby5^N?JO^ymv!JN44kkJQeciCJBipy!{lNqR0EP0 -t@uTAqZoJbUm;T1Hn8sZ^e`hw4^poReBNh-b6WqJeAq0bL`}_fO?n(s~#v_^iAn+kVRNj*tV)HCXXxy -?AlL26Me;zNy?z%^~~-Y7j9MWvl3W$Z$hw8&Sj0mY+IG1zod2$qQBw-s?96tJ!!8E4C=J|mqFUW74TCd#~tnKI5`!HF#;QpnjJ$8=f -vV?v7VK6{3IWS$Sl1ktDm7}2RRr^jmPtR%vb>U=wlG{S#mL5X9qwDMV1y6z+2G#VP_Kr+g!F^q8+H%X -a%|a1xn_vc&UeOEQ2J}CDnEYPVqiuHO6C6tUKXmiVm2}j -qNG&y1FbxaN)%C4ds?s*xw`l)ET^_pa&^&C<=Rf=<*4h-Ccr^+$f?y_Eu -VZ_@#~hXJ}NfX0vV@bstA#t7F%X|Iv10XpzV4~c~wK*<2odAt}pfGc{0#DFXk#(J93*MARD -v)Tl3BcJEp}8fx1C!SWjPyFsG~ZDupvprYO623MiTgzNp2GQd -qL5*n(5eONulJBByadfIWRG%3!Fztwv5UULP(Z;StK8%dEA)$Z!U5kK5P1-r<$+~ug?^P#cuC!ZU52= -^$+%~HwmbvlTMHSyc>m>nz4QHw~ -mJ7{ChL!2qVYTWbf%TR7zs5gs+COLKXqDY2Ocs1j=;hgWE*=X2m?-wF-w({!{8<9GII#MtYK&p6+ -edW!0n!&yaT7s!!dlgWK?&Z -YQ-|n#aShh`YDk(Az*>Nwv0k2Hag>(g8O7g;RwWZ;1U}a -yJ6*2&2-5PR><$m$AX>v3iw}d}SG0oMC>M^sFBD)6LnAUPLy)PLoweV5y3RbYkd&TPMx9h)1~~doA=P+h(7Mlp-w@< -3<5XaVlwI+$5MG$bPL&{A_5Cl8#_eJZ4i%#Q_ty)rS^Tbj;P9)6@mo%H?%OB4|d?r!96~dmpl?Z3mI3 -)PPKOZINX^(BJxLKv%TH>;!^?oc+YZ{bj`&Uampqpyge0jLU@*ja%g$A=ApFLyJZut65E2YM6M=@UUZM;wJrU9)2 -Al;siuoY!TD?4|Kp3~a@H#^EC8W<^K7r%Lw=D(4XRc@kTAyKAYV)9aZc>MjP_WmXy7cqqppOEV!aurL -!BI1KEmt^lp)ZU@E%@`a-)ZAQmHJ+QzOP{czMeS)nF=tU6w+^d`Llga#O17$fJ?kB -Ej6Bo&fHj7k=_TzU*)q@FK$`#hWu*lz7RLu%w+~h7*NBUGix-y} -tkrlG>6adEKMmU=1(`yh(PQCcpkr8wot?tHA)8k7* -jKz3tUv8+^HA4p;D@Iv1VIF~lKXew`(qsn*ux#ZtbQ3SDeK;Ve-!SLCe -;`XPLWsEj)ilzz$6c^qE9)zpBq;VfAlzWqzV6rJ0(n1B)&u{G;p?)SN!!;*x2-3-QYSO+5T?hrWo{0< -T{?rVOMnNSVL%e9ps@^i%dC5OEYMuQ-@>KKv<8&K4Mh!KRMy(SZ=Yfhpgnt_vjB8Ap7B32> -P-UezsKRUumvh#Sa#q$TS-M`Y7CSgP(djIvbasHJM_iiPSwFjN(eC)<;&wdbE0s{V!mM*-z~<@Z+UA^DdjFzspNlv3p3$0)`ERp~Y>75yAig6YJOXrtWNV*FODTFK; -Ca_a*N1W}?@U<3*a7n(869`H?}FTXD7UaxG1CAB&Y21}h9wmb@glAbzK`#OxT@JjF{Lp3J^KVYshUiX -C}SD-!$Ta-~(gh@KxVqUVRO*gZ`?7QSSm4jrBw<KRCKrmif!{|Y7;HwU^hmaO531sS?Urqx7S -c2WhvQIyzI7#AzK=Z46NUPtzj-VS+1rmz(2H1a-Ef`&j3M8BFjNK5%U=0d@o6kK#GkPhN(gykegTs6( -k#`k(=JbDN3$N0+QF>)tKTE3tHbo*SzR0LF=8I3UYDV;zBbB;6gU(eV1nb)4Peximw^31Z>%`M5^s|e -4yQ;gcX_#T!#J?)mr&^ZDob|#!zRx)iuN&3|08eHT;AMAxl2g--%N~{L7G3ZIf%Q=*;+gTC6aEv}rZ1 -eK$!6;~=QqA|E&07CupIROQ=Cc`zMEKgNI4C%Ix-$L;|^G2m*6L>p|hM+mNg6 -c_I+8iWzO_oUx~Rwj_}`WyVKHiF`aLqn+b{S2TiTuvzr>#bpF+LQ>L8SR;jiwPO&I69ko56D`CP5FixuG@#d?C=&dUnZ3g&KXb?KO`+hlItICm -6wZ1#ClSakTW`M@akpfo_4ghqL3v@Ettr$EAxw|c+<;cdA(%6$!m~nD@k0_iNH*XmSl{aMF&F#%_Hoe -+C8@C5Z+c<9T+uT73Ujs*kO!C*ak)F^wmh!z4M~7fGbtz7n>npnnx-_%2^}d@dpzm!;Ya1tXt@mZkEJ -c+s@pesVL6&)YyvWfnw!Zw8epC4UZ6P| -Algz>nw05E+A9QzBeXnPx&hQ5l4i3E^G=xLDJ&!g-ScnL`J>%&xwK>)c}ssml%-6A8`U*%;d+^OBv{< -Fl2<0kL{t9(<_FDK>MGH&P&CjbRm=OCD{Dw)b9-vQ48jF*^D}s7}10GKcbT5luD`-R5GhxY>cP#zxQV -@tCvhjDRHC}3|xf3OgXi^EajSX<$9!?R1G*lNoR;&A9#==Sc^~73W4?~2xuY_&;*#%e3rQ{1qmq2sHD -})j0OgS0xT`|LljkM`}~$VM?0+ef9^&^Dues5YQ@Mj>g=Vqaz_)8Iw_LVKA -t$V+3-w}VbR=&J?5iRN9!eZKrJ`3F1Fs8VRpf^k6CN~_tH_tpWZivHS`KV172`5r)N9wDYeNN!guD90QR(r&{T4{at((r?#x3!_P -QU$Yn*Lt$SB*mYT?5KGd1L;T)9ep%8gQNAzViZ)?^)ZnE_Ps%4sPn-Wn|5tU7q;YnD~vq-9l&6xa$m9O0y8RrO{kBL) -#|fZJAzVT!js6rX!2CihSb??Ft)VrW-&@X!@@RTUMwUDeyvKnda5*uiAj9Q%DTY>54e+Erl~$uahmWR -fu^L%8Wu!mf&1Dx<**w#6~T1=kFh*iQ|iWA{@dC=8+z)WNqkBPg15JL)ofuxGKkda>7xE|%!U=Fnm(4 -q|DvKypQaw3IJdEagtVy6KXSFKfEAhRMO -EQH%k3vLmnHa%fLYpRG*&)byDyB1&;*eS%#8pKNop`tC72C)PoR&g!m_QxSEb_Fn}}6&FwwpCf5{F{i`j2bXUVqmxDO(f|TA!?jTU9sZ^^Ljf-~Ub-0lbeRy -5(V5&wcXnnPpTG1>^SYN#c1J5s})>l9}`S8b9RB(ZpLA-7{lot)OUe<2fYjhv;P(v#0u4YK}b7mCApJ -qlT3+cm2G96XW`vl9Yx;d2wXv4k_Qv%xIb1g#&iz>~upqtt|bW=O}!+L{aM-#?T(?L9ic5f*Nb1m3AY -`=m=Nwm~&sE98_OAhT)uN|TtWJg0NtXGI%NpFK=~_@4_eFM6K{(teql`0rH6%wv?w}G-Xa=CqZxcQQ@ -%nCsfuM*iD~!!uQ-3>&7HN~_VR0AgRDAedvcD -HV2glqnVFqNY@|xKXB5O$V@@NlU5(z}GCP=y+nL;IKE{(HK(%mca1x -V)>Mfe>T_y@HPvYHNElO5s?&|BM$(0DOhuQ}pm1JWWK_zaMp{x~w>3+u{_Ay1DvdOfmQ;uc&63J>o|; -i5=9EM;OMB>a{}wf)datRdkoT&T1IYBZ-B9RMZoji0;;DVq5#f9RCBb8HW!4CL(qZnWq~6&yAEZSm{o -;d0uCylc0M0`|IQksWrW@jL9*hH??4)U9b%Qby&o}kA0>z3)9AmV%JZ%K&1uwd47tn$WbP4VSTB22$D -aVy5BNv$6dH0YXQp+cNQ>l&6Cn`m)d;ldpc{>HBl>nQii&AJ&g1XSGYK|5(L*WOo9%X`7wEWKQ!h6WP -D+>RscmNqCqm2(Q1VWo1llN$-_~~V8eWV64;P$7`??DB&;fvUAB)?m(Gk4*ZA{xujQTJ@SvZXW+ZRP} -Mrg#9$VrpuHUnw(0Dh{g3L@fhM@NU{S3aylDrd9aDY`R(R6n~xGsjp#eMeh|@6AvIKqLx({AlrYAmQ^ -&aMy4x#o2iwG0WBeZoE+RrTaNRktuxke;Tr{KjZr6WB8Weuwo_jTPUg#XPG&0aWH!NH2mE~ie-j}7&h -^fbE+Bqft!+;VHnRA&5lW;|>4i9YpzBt1;)(NV+$TpR7Uf+$t#XqEctnXACeb}V-6y@KN(Rw4YTo7K$ ->}asU~hYEgaWI)s;QKnuZ~n;pS@CnJ*Ti`-VoSr}qq(?0xkgs;7}x6Y6Pw -by7WD+E;3IdgLBC6|^ZnQ+xt+&s5oEHKZt|;gZKsn^D{Ry8HRBvPh1{v= -6Lzoa9nNji#{p#P@fe0MtHG!d4v~>UySx* -u?=(3*I0l$n*g<_KsJdHb7FA1r~xP^)sJ*yK~0wLqq<<12ht9ASnrcYyh3pB2=o9NDntO{!!?l0GZw(96pQH8Xpz_hWe@yyj<^I35~_gQf|p_YAuJ}W>adk))8Qn?oVN+6<1b6SA}InrrG=LoJe+G)irnyoZG{+ZK?cc+b -TT7fh9e|1_hJSYAorxgkM(4AIv4t%52ir1%(a$4~>s&2pgHK!GyU)G#fJc{~S{*_KEKyt)-zcs>Pg(u -lq7)7#Rs-~fBq#D|l9xF^qj}@jUWooq9{UbeAVEO4uD$+Ew=h0mCW4SagR}IEK#M8~Oz&#!oyd!)R-0 -kSt%<4V~I+`zPUMhmW)6}Z>{)on}STv|ry9U0&OGSK?mkN`vRz(tCDoT@HD(Ijmyi^?0Jyg){kMdCA9 -jR7rPk5;KYeKC`dZ?hqd9UzLfel5y>eNVx5qj0_qdZhB(&no-Hem?6(@S{)z1tLV#wd5(^?6m?B^DlX=s2lcLQT5GUD;7}3_jRuoi3zTx-9x=rq?p2>hMm9CX$4 -NG{}ZPbrw0;FD{!GsIIS4=AeL}ifs4|x2ZrK5eBEgU=1DlMI5BXg(~2KlNDS|lPAjmM(M~Hq`Tf_OR# -3+FO-?IDFybqnR=6kr1E&?Vd79ITb-(|=JYs#X{vcRYj39 -gVAzshBC#Spm|By-x~gvS>SyQD^M23n+u4ya1v*^?i}b1q>IyQQW=4Q$^0xsBH!BWLAhyW<|4;c?$kw -@Hg-KPNoF@egJ=)MQ8Fma(L|uXfxFwilrFn1wuxcNKX7VDQ0hgp!OlT&lq$mj-Lhrh~E!GR3{uJAz+q>)_ -ww_H}am!UczvTdO>1UhOm3@OJ0Y#kz%$Fqs&Ct9=PcYtOJze^jxFp&?8e{sLXym7k#F0(f6Q$Ibqilf -7F0a-!!g^vD>$KKIE%j3^Rp>$6F47_{(ZdfK-Vw4?t=K2JA$^ylg5R~?7H)ahc6#Ve8@=RzA~bSaLPg -$LynH2ihT#ds)>U(j+_RjD80Rrv^C;y1dqk|Aps0wknUsZ2G7+`wW2SMp`6K1g{n{CXYPX8Sd -VXywtCwim1sCJO=!|zxxdzWf_!=6`!SzY`OS;G%rP3 -nE&x4RU6N8L6gNa(2l6K`a%U9M#RQgQ>vXMezknT#HNtl%k?4ouw9yNzKN54^f#BmPKe(NIjMOi%k{A -m#@#Pw%SRY7}jnGu$sjAMyA?wOEx5cf5rE0@Uv=-$R5l{r;AF2i@2hG6-pXO-O~U8Tl>`X@FfKhlwi5_kY`HuyHca}O={Z93e8MF<*iFQBbHtN+BV8ZrB`gDSgXi~`nO^fj@X7 -P{)TOnt-BK2CO4Li`hEkwi^osh#qtx?^(>KPRc)@f_7C8#hZ-pborjj2M@%!BX`WBL*IqAfw3&o_*_m -COXZg_tKBxF0<9yyASjflIsh^)iX2y5TGfv$feZp~phx|68ne=ikH{)WzWCid>IqWU6!B -P;i24BVNy^d(ZSIuD>3a>M3pJ}BMh?@-@r>y(}_WF`Vu!;|`co`^mhDI1PV>@6}RdU?}(vk;20TC~?{-*zt8F}-n;-?rUB&0>lSK*y^WpnYi#CtQoKU(}u;vL6VsVn|b;vGMy -rYtEACf@NHqO!2~>BKu;R#N5^|1k01hVN60_Zao}Aiif6zk=_n@Qzn+)b*?Uta`jg+Pq3h#SSjTm%0a -*)9bgjNe2-Ya_oSLtmaIT)#M@#A}?Q{F`8zyl@IbM2Glu)FDRD@ze}fMjZ}<05-9#1t%NGY?jdf-(1Z -+T88R3{75UM-l)<2RTB%fXD=qq^p -?t%wkmtbBc;lJm#eK&ywrr>X5!Ou_PJQIG!EEx)57Tjely55_=oA-v!Qal6T3i7g>{5&OKCOUAyr%3r -gD2D?s78jPR)9AoSKSTeIQ>{TH8`om$lVU4i!LkEvNXK>G!IB-87KLTbc&agkL~O=Jq>O2B#o&lWU1F -sJt)8{ON*O?ol$ -)wJ>x8Istt+7K250?47i7xp2SaWEoEh81E5ei}rY$HPd&(uzB^^K%80Bn-;cgLsv!sw&vnOVaTp94@pDDu!BbUw0SYOuMMAy -F-qLKQ{)=oFm+Di<`8gyNsS1$9eubJ-cbh>%jt!Tuu~Fu@gTVBGFHX*h7j*yRJT>>4~MQ%iibsSo>g5 -eUV*Xx(fBBt5E3i%Ec^x+rZ-~h4L+Kyzz`bg5b(|7ymG{jhpE+ND9axGAD`R -FL!l(6j?!+&MD62h)Lp3KC@gWS_g}9oN4PkqaX3vgK8-1tW+RU*coU7OF>aq7Y`i0bk_dv;tEpT42%I -%qB7BNh?+m_KIQQz(TC#88<3pI=oXRgHyS&1gj1p?}G*_u3Zk5qfQwRT9VjYnNaHEWYjK254U(e6@hN -_y0Fg5Hv-RfDe6Zql_#`yxZ&;}nM3kGm@YmreM;*n1a%sH*LMd=CsTDms{2_)1h%3ZJ0{j^-esK#(X3 -nh!)71VW%;2DK}K4wU1F>8)GZk!_=G{r>se|`2jXE-2`b-Um1@Av= -R18ep^`?=QMYwx}G)wBy -p@b?Py-`RgFKR(&PTJYJ~oO1M!+gJc;cdq!5{Zm#xpPQKuZ>X_v>HMqidC&BFm-3k00lG*-z*yIbM`J -So1byw6GoC&V(L>8fj#Fs2$4(#ciK_jZ)Ta+;Cz5PubBTp8Z-%J%nA+{iDB!N^-w~WI4+cs$wqh#y@c -16j`OeT>CcH4ug%5=6qODq>KGZE1#7R9|j;+?MH&25=oQ%~xC^;diY_1a&78MXJSbUhF`p?)7~kN -nj%p!s0UUxk{46N(gi(f$u9<*%R{cOX=+lRqwv=kAY7aK98kP0{%W!1YV@*rZZEo6QEV*PNZdmO=A}z;YZ*)>VP__=#uUzJkrJ9!L@7cMEHChE`P^ZY1=MN=MR@Uek;O~%chG_obnH%{NG0 -teN)`8k -x{sComJ3<)l1O>Q35dirHb<29DNtb=Mt!o%z=JY_6>T0UDXI)>gAqtLEpyc5X_%-aYRow&y03ON7R1{~2iUpiD+im-jg -~L}vl`n2gmBx~lSf%tVi#mmJJhopcHDgJEIFamO%ObuomSRb0N#UupZ@OraoTIEQi!l -oAkyA7Im?5>+Fc5|sjD -Eq##OW~UvY+1zLB;pQWv<~a7X;L;*&*#4b6qXhY)@hRet3seTu&+YW-nx;Z?TGVDBk<^R9>`pHNjI%C -YpthJXe=$zsuyEO0klwJ*u?L{~zr(+52aj{EaY=&GnkaUbx1;R(*0&4+r1;gUZt(yLmJ<*ebZk2F+>9 ->l3{M^a4HX;#6>bc+v<$GP+Q|E8GgsTB}**i#Wo=|)Ff36h^FjJ}TC!x5|AVS!_ -3M=epqXyTpPZ1Wpk8~|J+dgy_)@i@n0}r}(O4DnVn~hIswL`h6-`qt)AdM(tPY69xlz&*^DO*rj;R(5 -9-IoU|^wd+lrT*$bp~^#h3y##2Yrhs0%0u)*$pQp&0~Uok8Mo=6@V0Hb$-{1d#>DL&)!&h$aa-%)iWv ->=kD(~arFc4q~~tqw(yRLNycsWdxw0STo!4-o! -(ttg}$z6U(;o4E7uf*=NG0c*4ASj@`3cjv69f@BGfGlu#Lb816ANO0ed< -FrUr=CEbp{yyn;Q8dtVH;M2XAnx>&eG-I^L2Ry>11qE+muMi+sn^?S?%0tpp%J&_g?zjOHwBgp%5h1a -5**r48b<*B^6e&*(V=IAt~ZPG`NzjFHbYIc@qH|Jh^u0v>q2%*_J!g;7C%=)&_q{4;6$k-`lua->#k$ -LB{PwMev=cZpv>zV-z`I3MW9|T8wwSKyJc{z47ZJNEDG$;ZQdqZVE=*vWkU;GzMGwKfxfs_X3=IE8g7 -yAm5JPJkYbo(Nx_i24QMOWDi_Oyl&87G`;vuv;9&Q2P4IP1^bMO}uzLy8DibHXT{D2h&?NL^ql8@7C3MlOFSgAPUDJguFSL^cY1}aN7_pD -gs8u~9gohQ@EFR47(J#2k<-I&^)P-dRS3BiX%yv|Sqb?*RNWWAnUu4*Jk24k4EIOD@byOYT7^}awpsl -MN7LuJhhqp&{Tjg549vlUtl!ZTOrCn})`jc|^Xf*EVB -@0|t_n0L(OxRR^|rv}I!6T*%-$nA*w2cKj>?`3)EF+^Tm0I!#hwQpH9Tayqk?2@oo* -#ciAk(AjwK&>sec~tK&3+KosxT3Nw{F>W#)orU&Z0=nV`Q{y(O?L{L;b-#;y%pMl4J5UB;fi8INI7N; -%3j7d^Tl39*p!a;TjfCB#@E|XD6RHjXg=GEiZZ_LoghKar8vw`h)s}XkYXH{b&LCi#rrJ^2>Cc{1`SW -!CsGAZNXlx(^Yrp3bEV}Jni^repGicq287PkQSV!f7s||7k}i5Vv5Qf4FNUZ#kEq}EX_3Jym`U_-BBy -eEv`0y2=^Ex{EFFQpvbPj%Q}vq-pURsJ^{r3nYpaot6yUT5jTG@UE+;-p#I5vwFc)pG!Lp~)ouWZ#jl -!L-_2WYly301@_8_Vh+_L2x^6{l}t|RCn$*gfzUP>eP-L9Q5Jr7lnBl~o6!Dmkt=1|q4V~2h?bfnE5` --MyAT{RS{BZZ*O^wM{phpg>P6mGu3YBW*Uf>!qglSkJ{fgMARkCLQG6=tDy%hKSv*FA{BR&|s+fPXl<+=bUT3;u%h=@u%pJQ7)m;*#w -FQllmv<*9;An2Ir7UVRxllXmm6#YdBfzdtOsw|i_5h(tp*A@Yly$;V6;Y5ZnHP#V=LPA`gby^NM1offC+1Dj&+2@^D95*S5rK{qWIbgSor0B}J^!3BQ -xT+j_%KrDczxT3`)m+%^VkUmsf_wC-hzV7d}UsrrbN{`vzD$|cn$YP1o(OqH!g7N_+;DBzT7~4btM52 -)2^Tap0wTV*e1!`fPaTnGrc}*17Q7Z~7P!v|9VyYG$Mz^MlDM?XLI>!?$`+BJhQYkP^dXsb$Twlk2isU1#&>}9?<-xiF)^oy&T^i{-VZ{+;>6oytf%TxUJ_YLs!nzLDw}kaMSpO-k8)1D -;ScPWhwZd8s>q=q8U?n+(6@9w2Kvs`W%UPcNLR%}a2LBa -~*O6o4GX9WGiDzrBH2y2a?KNr>uf_`DG74!?MP-S;aSg|@L9TZk9zDpknE0l;xZ{;c|Lzjm1vheVOb- -l25g!M6D^@r6ZtT=rs6$@(staf1ygf&Z8gJ4Y&*8Z?g64t@6#t7?BSnm?nP*_8RbtJ4o!fJ-KyRb&Wd -Xun5!|J1=9EZ2dIq*n;^+#cy1nalLItA7v!a5z+{lYp6*6lfBIytj}t}NSBm$%7F==}CckeRn@fO36xe@Y<;}ZqNtj|LCf4>*dJ}FR-%Rnz}7b5sbm&XX5eIOy#cSNer-lz2`TlWgdZ$!9< -y)!wsACc;5_aiCXv0dN#l>K8DNtKnWx|Sb0;V9QtUfM2Z!2ZiUh*nDGdm@k`9lx|4i}tqX(p^}zAdip -8btlsQfYN{0QQ=X&OjCZzkjF{xbrl+16|O^`J5ARL45nJ+(sh*llFR5Yt=-fw+dCvDIlebKzB8Jx%}l -X-Rb>oj?9whlOUH^T563s2j>`t?ZRelofq!AI;k3nD@N`t^t-g*Eo}RmTU}?u!X#=11PE$b6JuR| -V2f0)dU7_o3z9%Y2rP)h!#RTRzrGt&+n~r31lTI -`}Trrg4e#JVkJv;1hy<1Um@cA=pCTid=ch8Ts%jG9@x4GS85ChRhl=YskDn<^?iq$*d)_j?B6;J$Bfc -XN_mAS{fWg`x68bgu*4tFf(OOdF>{)hTrlSYHQiubzLtj`DqKGxEL -k1(kH%WiTk!kKicgeIVq*HgD-UO=+t%|a*t!D^b$m^G_x(!K#SI$fo(l^NzOW@YtYLy#$))vKZHPj4Fr<7X&- -LQQV1v=FA-#x+_%TqGu|-MRFUrmZx5_B@nJ(vsvcXYr;Y}$p(Sp -8k4MB2rZz=)+d8=_vple$*-QT1!bWy`gwzTltmkNwPWI2Lo>*YxSG&0jKr&slNF=5V{^e)vezHsq68xE9#UyDG-P|qfUgBI|@(ej3 -u9m3D^m{PnM^2>Uqw?qE3my>z}wX04F9KR}8k+M3K$QaYb+M>KZ@G8UKlM+$?gp-{l&g=8QkMYg`&+O -1SiQwKY?5I^&PK+D4MaQ@n=7A200eia$;T>PnvOjBIbNrj`cw+PhG?~SMX5M>yP^&{$EAgoOW!Y5HDH4Y%nfRNLuCD{IMMj1xK%|_@GYzNhQjb -VnnKl>&KsBFsFLei@RhQ0dPl9^YEP6(gURIxy-zhA$&Y?RC=))C@8PQStTa_xjR`oWz+Y7Kqu*Q>;YY -4_3D+vc)i$VtdnA+%eM8||kvX-EQ9=X%=T2|OPJI&pe1kwH${-(@laDrJ>9Yc)$s!%xqfsAU1TMX3_s -a60GPBarm*E5!l8%M`FudT6~`fC#&d%Uc|mj!cbquiXSTymFct% -N6BIlT}Ki>PcDWk7HCcl{N94WG~Fv(h7|yKpYdn4M&ijU8BS$2^(99dw#HzAhgpzj -&+%R&-s9Zw8R9)becv#JZ1|2NBSB6PZj;au@Z*Y?rhg3V -4#UG!Lg$1e!S-J#%7?DFY~Ny;fwg}0lEbJS4JyIo?NUVQ@U74l3&Wl! -qc4|)dw|Lc~pPm+KxMl{Ykt$CnM8NL8c!eQ{BsTF8C+oHec^_r*Z(<<~`o#dAGe9Cw9g1X(>0BE)?sJ -NXX9>@0*p7?u?ARHl+%es8J@;#Kmq(sApR2!L5$V9*cb)mp$zr*Pj`PPCyvWw(^T~%VKxu`c|Q -;lS#eB-QEU!Yhjel)ESk^Y4)~g9M7hT{<1ngxq7g;d@+e!vzU7EV&FODZjhN_UbuSEW=@h$KPxL2?(V -pDqF`^5^eNt@HHGymWl^KjLxnh+rS5~?S%?PfUHy%e+rYht?C#oC{h>lDj*^-77SvxB>6dSmMrcy64m -bWY-Y+THbuMg%!PW*;yw+D-JqFw*1D6jn65Dh%eUJ --+M2Q3k~$xbMUhB=|Ds00lHYT9M%LT`cSdxG{9Xkbq7oHYHA|lf$TiPFm66I%nu(uaJ4lY@N?}TkdMB|E!_$2XN(2oxlnI=t~si{w -NXVitZ4HAA;L_|oPam6OA7?vV^E>f?;F`bb*n|`E+y{B{H3A>fx8JjLIobchGyVlB0uBO%U20419VxoTi!tqvuu)asH ->eb(imEGmbfWo4_Gr4tht_Ei{Ay$FlU*ZG}h*1P78}C-nt4--Wzf;STI6aX*B(s;{EdbF7FT#?tB`RR -{TqA>f!_*J6T)v*kyj^u}iM;4m3gj)Kw!#39dnT2g!9PT$--N2xKS1=>+PgoyHX=xK##=4#5OngV{uO -RUbF%9>&uB4UvwsaL?iL~Cw%%VQ$u3l$;8h_^L)K-5ER*Xmg4ws-q!V24`4LEKCll -2yIUA`%vzCWkOiId_+!h}|*Hlx3EGt8@!4*P@Q-Z4^AxTlBDO%d -$!|U5ymzk%WGzsXoB{R(u@N4&cCGq`X{SLE6p<#8$#7XR9euPn`yQua4Zh(A~r|@rS2krH^lE#AZyIn -O$fLIBEh0jOq1`sohel(iyKn>GM0(?UxDV+l`6YD+I(bhQ3J?!CD;q@U{b98#y(x?*1kCnAUU-f4IuI -LDNnXHs{y3Ku71jr)?R4(_siG_n$qlUnmR}x4QN^4kd~XIZ*A_J9wck^(PnAz7Bqa6C{6Lx(BMrp3?a -$FD1TWr_?$Qj+D55*B1Z49oMK`c*jaMguuFZ)X^^sQEq$XS*;K+@emA1p(Dx0I|L61s)zbyB|8TIm1MYNT{F)JWM- -Yf2dsk+r56v|3Z(46P|=aqiP+szytt@sO-HMb=X(bXck-x?5AmHSRL5`o3OIWxd{%=x9Z6${wLN1;PJ -dEtNE_asFm4m5%3yBDSiK=G9ap-HsYHm4@ckRQ{_OHI<(>R8zULc{P7k0mGO -$2$|yxmtvZBhuFNI{>w4%z~7aJ+6{7Yj+mGKm!>T -Qjp%56e(#^hg7RJl=AR0&ShD5^~SpHWnKl>4|}(NY=u -g-~Nu68q~~D%AqIaV?c!LKn(4*SK0&a$Qxc##$;Bg^f)ptAw_=KTJ*K;zu=VD%V|YsHPG*-ME@c(N4M -0F?+q5$~9MitC~uYTuW*y)d0V)rgDd%M&MJ1fm=~gX;V~GPE%E>yr)r7iEz!Rs2nC#R3bq0Dk|G+R8- -#b`&Cpnr8}j09hFMy{YE8~r8lgka^evp1d3B+#&9c6K~em=lFB*tN-C#Zr=-#xBlMvtS}K3~{aPx~4* -!E%D%V_XK}+SCXPePd3CXW1EtOqocjHj$IxNo^>rd%-k#;EIUUY|(;>cQkNPO^8Hd0J1Ts6wfktPChL-dZ5H2Y1EZuMw1`|Z}?nyk;|) -5XrHL>T?(`XDXxIaJqQhHTi_}I{8;3jeF~FALL#qXh6t76-evKa9Stc;0(tnB5fTpzJ^`l -;+mrB_Vl7J3-3r?e~(>X}nlzv-BiOl2@tE`$6D@~2ic;z}WzTqDuy7}<{7hbcF#V(w_a)gdt -wAxV0)cSy{-P;E%e`nlROG3z4lG!zRsKF)65A?4GmZHSPgB-E(lkp84Z -h{*BK=hf94CGv -&^W&d*|yeBDH%uXPn|=4Jy+N#;*B{Q?~9RZFxIFX*Ok%p@^3uzDhqO%}&E-UB?Iw%4F8V+Kc>}{F)7M -l^YBFGYlPxieq!A5aLgX0|v*l+DLaitX%jfX7yOYZzG+6o3pSES6OqjvCM?=YUsO277OqACevv@;nE8 -iFR@i!V{|+Qfn=;+XP@R+Y+&|K!tik<6ml$rN_?i89?t)7C -XnCd9GRT3Y2_--c!bF|~ZGM*LJys0oxczM=1?WY{LQgGq8s{T~i;)dO<<+2)~6V -&ih%X~!)r%t(T*VTT>c0tqj{9AUK^~_=5JI!9Lq1?k!Zl*}pUsinYz}9N2Ooz;gIr)uEUOR;HL|Zwf|C4jX0E&{%(8iy;YM@l&yKEP-?9kaMVrJ8dLwlO0uia&vf -42-gKp)t^6s9htr?h^q=GxPb$|X98=7K4pPl5b;I}3Sz^OCiQD~gj{Bx4^?Md$DUKJXyP`V2iQDYbO% -(;hP3LV!O1@2UVv_XdwPLYSdVZGrm5KBO(o#M)(Wuj+Sba@b<_Aa*AlF{v{u{C(T&eO=intVenYZ!+}Mvzi9P2Fp5ba&$;Vo>VX;9{S}ZE<0bG-IWFh{F}tO}Ws~Uy2cKr59~ -oaPkRGNt$aNM-r~pH@iLx-fj2F@|aTHCZxP{k8y2zStQfdpPLohrm6#z#g$swo7Hd3{aoJ0ZTbb!m31 -pN-8&1NTjcSXYiF<*;#AanTsneu>ffttWYHtpRU1<7I#9}OabZ5TR*nk2*%8LUQoNxPO{z=S%DPQlj_q}^C2z@ZkG{!X;De>wU>a|aHQsho&W64&HjFzuvraLshDYz~|$?+2sJB -)a^Qse>|oWwT_BN|f@$<(+ENHQQv<*R}~E<>_JbJ%qxD+V!Bo!Bnk% -`I(S=+D1@p5!rX>m$X|GXUo|OsGh*Ec%+)Ezt>!tW5ODhumc``+3o8j5a#%K -_8?~Ry3gvE>+Pe8KzcDRWR8oSvz4~=yB=mvT+`!UB(rql;S8kjR$aHeU@HH<4lSCnfy*;Nh)%kTvz7% -hO@o%em#n3h@9pZli8qT+(R^L;7nV=%STaf1oy0KQo^?}$kJEZ -b`6+tiJwwXPO@5`O7vHI6QP;*Be)Z;`n1ow-b@+0};1qYNNnL4f;f#`m76a$xGgx$=it9wtg!y?WuEABE9LnRqv{-_HgY?5L{%Ce7{l;0N94FMeYl{iw;wBKj6YgRka9$s}pe@Z5@%Wq -A&{|DjgHI3rf51r6fYV>}{l%?p1YmI6mUk=&Ro~T`9VM5oMcyUt)dlsVCCayHt%Okzgir{Rb`sqok)$ -((<{q=(WzMn%>);lP^I3|7}v7J9%l+z4fPMoZf_MZ~FzuHA)RS^Am+86A6i~{YjhE7vi=$!9Vg<XK^V6Y`c%p+cTpQh0%wVGZOU7OmpT@Tsapy)! -n5Yn}Q*H3I%98-9+M)L44S+xzaabHP}(T(zmlzH${7;ulP>5YLrg7)c5Nt?n`}7Zq(Us!Tqeb(if+Y8 -(ry(n_HnkDa*rg;_S3GmF~M~`%Bi*+u9Yx+0ur)Jexqs`=uE-!|@ -ItM+kK3Z)?OO^6=S(+@zhOm^)QLZz`3A4|FpIZhT;{NQ7=RC|{_DNB~Q-URoMWSN^pl4b5pNR~bX;~{ -J&L)c6qr`&WBHgj`G*vu^;VKaAe9(l}#0GbN{bUp;oqP#NksaVHx1DBWX=A@YB~&40IO^DFWxEA -T5T@{6^fb(}QX0%_7%tKeY;H1f(ZF7K5%K>t^X`=nlQ;wT=d54_o^zT}CIy%vqk&3zI~tlI@O#s#ZI3Ip^}=$x^2{RSd&z^1iYr5|7E^N=#MHh4@}2(G~C4W{)i%eNC -b7w(!mOz8-*c^nF{OTJB@(*Sg%++p$yU*rglsz14ffxAQ$bz8kTNY?Wl&IpT!94enq(6tZW;30o^{3U -RB)r7v6+(;#YzBU_&IpFu>MGy1niu|VI-1{6T0S`0Q{Z1?&oiazU4`3qGOae;nqc0*f(y%nx*ymWHAA -4|M+Qu)j}EwH1fY&+?Bcp7=}K5HnA|33dOJUd8)_?X}n!A}IfH|yA~1a}Zb6QmIo6a1NABf)zFM+s^O -^tb3(M}k0tkp%Y;{M1#)P7oXUt(oNnn6 -HbYeTw>BoemT0l<@>>j6~~Y#%iePM-K{TbLcOh+$Xb1Y%CkVLN)dwY&eT!Q6inm>>d`wBG`BqDO{rIZ -v>l1epRROQm@AE;7CtNE&Vs`((DR7%fIerZY!gaNko?~@2ygvi&pB~vlXM805-G}wIBVDA4w2%(cp -m+&Ut -+!IADE`;5|1`+(cr?H1;XXnmQ+q}-hQ}5;NV`$a7jc?m_M!)tqb?Dg -X=3D$bcj?-#`>j0!diLra*r#v5pxbT_?muAQput1#2pKwT_?@9)BTOSl-8I@AJ|-eEYV5e^nAp4H;_n -%sFkxciq{&IiQ>NZKZTgIvDYH^%&q=eS&&|lpx-WZPPHx`))_j}2VE%%I5BzCS;o>3|k!y`wkZDV>=F -YL8_U93=%^}{f5|>zrvNUP|s2{7~6C3f=JaP{Zt<@^R1qeFYvepV;$5DGqq(5yK**B4TrF+=8CVbE)# -}iM6ix5ruPo(~BFqL+Qa2Y^h!U-F>??4|CAbOSn(Q{ZtpN8Dx$OdZCgj>2ubDjvBL6qc^Emic0au0-l -27RDiw4EI40|MwbRZv|ZQm9W0{+YrjmEvWx2b6GVRjoytuvEirV4E3JOd~!Mw4p4~r=qV2U_s -R9&!IOfyG@iHIAD$&CvADAHd#_R0!MI;-0xcjF4hJyBLz)WC!?+8j~l2R{Lw~t)HxuVq2mi`lb(U^|GZaP$))N*Tzg -0x2Q?;x2%jc*qMAOe@A}c(zM^1-!ON6d7`F}ZUKe23JCyd -G}Us{wpgE{n%>agfq#lf)xs7C%m*nPCBLEO)LOhA{~;`_#rn>r+$;*la(m8I{Lu>*2ySdT-G+PwZ1RN -EWmEbVm1mmj>$U0J`77znQ+l~f(c-Kk)iie>aNn<{|9kkQVNPkHyi=9b)pYCIA<8sY@I#R3M>E8j6Cf -mBO9O7WzC8}s)X$CMhY&IkqR!@tcUuGe25ai)#?ytg3)x&vG5?bn=nON@Jbk!_EWRQt9Ek82b!@bVTZh||R9pFwTG;goFVrHGmOdzuhn -J0cRv#w<3k%{k10?51-?w8H12bun4Lb};hYsQr=to)*x_>a;d6S89onGYZzGNGqv7Ma*mHIo@H+J;q( -(-?n2TaxJe)m|Zj1=C*um0Ux~DvoTJ!PL)RS}Wx+Q2a4AQp`VcsZ0@So-+2zv60q&qBQ5z?_EmWG|VB -&-6DF5v=bVnHbT%GBxX$D%G*R5K{@v9Y_fXeUq@xiJiVE>ho3$wD#{Dbu)&lL0tSR8Oz|o*1^f0y{Zvj093c-As!9a$A{(KDVzYm^(>;8h=f7kn6{})X))6!o_6MqduEYbYwZtzbVf2 -ror;|BlKxB>s$#s7c6bA6p={7-X$uKw>vp9UPDtN-)3!M}PA_;>$)jeh)fq(AMO*6)T%R_Jc{exvl() -7{ubT>Qg|mn?OZls@>-GUsyFin5gtuUh@*M;`slV~?*{`^1w^J^j~b)~$bb!*kF7?S+jm{{5ww|MAK{ -H@*7W<}F)a-}c6vZ@vA_yYIbUUa`G$#|Jxi?cVd@-hKN&I&kpN$A>@p^s~>8d~x*H@h`vn`ozg^zCCs -NyYHo{AF9v%>&LS-=YBeW;pdB&YA^qCrS9rA{%sH7;4`C|A_L3?)ElHkJz -2;dd2>j#*PcfRJmcL#{RIzzDi?Xt+D@EV}C?r$E9SdeXGU}#bvGJ^u#fZaTayWEIBhLEq8vRE!CEpn* -;aB`4;Q=*;$r3wwRpsT;FnH?6p)$|keWw<(kMJujj8 -YkYQk78sc8Y}*4%j%T102Pdt%QboT!PW@|;2@VV{>N{UhZlNxZiHEF3ilC8;~v#y3Rk}VzX(yS- -vfe$V#*4WoOQjL#RH&FV}8s+LQdKP@0Jdnwyx7&rxQbHsL1E0p^IcqkJZ>e5y08epCb5^xjXYjcl>&Q -(wk&EjB9ROk2R5+%!u-ZhAnTy8b&*%(3M?mO&y-E19!J%7c`dC7!4SdBosyYI3bmTJz?ml8tHxB}B#2 -Zy?oUKiKj!EVEO|F*`HILUo^=-((u|Gcs+K{JhjTmVnfBo5dQ?Z;(CTI%sxg&LB%pK|eKOBYvpo734$ -#Gp)Hf^DH?wcSa5JvRLP3=A?>Br0Stq1Mauy+AIN7$BmL|3h9y8@q3C`JF?PT3!8V7G|9{9&d;=EfDQ -6(saB2ijb*2)GeN5orirps>5|)-HmwF>(=4LOwf+J2e4sUX(%1o^*QeXBcfWvC1$%HxgOr=VqhHT{L| -b-t?tCPQ*p1P-K2G!H&;*}Z=h{)Z`2j(e0_rI;(*rE?@@xxlYdN3U#8L{=q+7dif7_NdAbLsdq)EfyW^X|Nj2JN;<4FGWB&yVf(`l^1 -fRaBwKXYDQwk1XFN2jM{=G&&*Lhcxtmo{6Q$Eq;KHc=;MUf(nhI7kok6U9{D*K{?v=>O+#E&EUD+p_< -Xel7d|pvK-f+~-O4e&1^B<&DBEnAS4A5wlySXa5}8AJ5u3=4R -LKh^Zv88m|5DEhW?GqG5+<2{*CGXa9hjxZ>? -naLYrk$ZgOUtB_bo$%KoCz=nfaoSQ^q!w53H;iD72o;i1gPqA)#SYye73LzW!Q$|M%4i0Kw3>O>Ziot -v+|-5oXYo~Sq)gVVCJ8FMj||3nfe3Iqv>B!nchC|#n>O8+rL%?J^Ks&2NZHkKG2mmHoDBcdPhh_{ -2uARdDKr};IO~}mz?d(IG1k$|w>nGZ>C+Co$na1`DTI4#XS*4&g0d%pmlpBSnLcGtC7Lzl_YMCdvwOZ ->=39IfIh#MY}z}THA!|_q^u(u{Uk_h88!L7u>>;wHI_gS7@u)ELr?8ck>^v_Pbi3;_v=WUsDs8;e(dK -w=5K+_T@WneBFLsP;i4`sa}JT1*CYNU@>GL@WQFqVrnOfvp{lrU-M;*&i(Z-wbU>&Y+=hQ;md#Me0 -Qm(sIvlrynpJ1^HHqDtMlSx@Wf>b7D&bd#ayW;wOjEZlepnqY$%`$INCgqbaN``FgVWpRVLoyrf8JC+ -%ZDY1I)w+;y{8;eRM2lR>Psr^@?j49DUe>%5AyhKSy0O{z{0t;E#-5%|(mP`hp|mu8j-L2P!7SH0)`F -927PVcZF>0Q9saA`-+>;SoQ+`RQ)=W%el3AkclQuqGu9aAi`V}c)O%j0Ix~Ywkq$qQXf^Bls!}fol{~ -tf)W5eC1PN(tkyFM)T+I81EH}LoP19AKB^Z)+`{B+NE=C@z%{C=eRj_-j~Uh-5-tnK5h2Cj(2CxXy@g ->`oyCa=326tk6LwpRSwX#8$g;=(j4>G~;dI3IG80&wrCxDQj_!0{i#ac*^2u- -#>6f*&XMCxiB-YXTM1vR;JidJmn!}a#lJ*JXEgy%Ui?*gU$5ZvoRZ%Qiu*PNzmF9E&lP`vAGs{lGW*X -ye$76ze?YU(e;l>{k*E31OxeHr&9?tty#L7O|8*Wvp8wlfA&}$)553_z>X2tGUAVNT;{3O-Wpn-Wvbp -4$6XNb}^WtJ*Zlv%P8;))WXeQYu*JRVIm|Jf!e7Iu1PT>zcd+OOP?DVtJv(uVSfAM-@n?M7|grhT$2C -$T)Sx4`U&yW7u%pwWQ1S1JT35F63CI})3An+&fC13=#7tHJofkbeM-~_>Of -+GZ<5F8}fN3fe<2SGW(I|N$@HWI8OSfhkrMX-#Zn7~GmO)!gK5K&V|L^nfKfiX)%%jocsXS_YGfA$43BZqkC%y~tdZe>9{g9L98v^wisK -8+2^#IeaLy__frSO5K<^gR-r=+drqsH?|bzj2$-0kK4hZOYxxovd4o*EJ4hz{B{DHjqd>)|9Dgz~7aOc=oQ_VMD2Z;u9qG?%!X3{74TEUimo?+QrWyB*&A7kO -jQj3p+>bQlE;ZqfvALGq4YPGjqQX0+99pYM(=psDtiJoF>)1v^{d;+ij%9e&zX#dKzfb-9!L{`6SO31 -??-X8N|9;{Xo%jw~eUJX9j@_%t4~hW;82Srfsh*^3`WZNIAR9Ms97|44W|^6p%x<^K6;)hJ|HW+0nl< -c&7hYg*zWF9AFE3}GfBreUcI_Hdnr^WOOfR_PnI@tS?<@9Y#$EI09}uqx_}RK*-@5tp7cD}7vwUCO0l -x1NdGDfk1HGK3xb!}MKHjAR`}XY(Yh){6#f}GZdNcRNm(wFw`P`dU~DwUWD-b4y30a* -rmMJ$^N+MrVj6vejWMap)f^q`tp1BB@uq#z5_dDhf5;;uaUp}PWf~0I)F`CxB_%E$n -zOQcIMWliEYZIr(P=LB@N}t}n-r3Xfwx{+TAQIA1K87RzSkmFFEJxrvY{S*kJKD$XJ3t{;ptuZmFFr9 -OR1P5CYxf^`uGh=^L|6d1p9vqyYbm-|10eiA+AwKfZQX(5auNOj=T|4cUt1@<5uVDo>Zw{TyhZsV2}$ --wc%f(Hi0m!W7r4pZ%&<&(WN%j2qrB*ZXYpuafgT$0B+wNzxuaAj#CB1OZ>B*BPzn>mA^d&M=?u)&D -Jm*_e_+n?j_oq!xVMTny`&%a?y*F1P{UXl)b?MS3Cb;-dj4kfbgZugP;LKA2)&i(ud=xOs<%BXai*jm -trKnGzJ}fvmm<=90n2j1WiiL-VvzV9|QFaq1Okhb#No?xWscgoK8EkRdST=X=T$YuU#pcbM$E;SX=sy -Yz3)y=6X!ghYlTLpM3HOJ9_je -`|`^#+1b-a*x{czJ9+Y?;ESrNDt6)Q5A55^oYmCSuuGRN30}eDF^I;81jf9HX$-3vYfXI9h8lZ2YQO> -fCN_!>Vbk~owvgY?*6`JAD}R-J!VjtaDeFk_dr|zs6n_-OkE8fgDgIoFzks)A%PIa-6#o^9UrzBqrue -5c@%vJIGsVA`;@c?xN{as?#otcx4^jLh6#pxV|1HI@qWEVi{zXmvL46rZps7zDO}|&!7~8ax=HxHZm{ -v)R@Z?DTe41Asp#Gi6 -AkMF0F^sprm+>Kaj8Axw@%x`*eD!OLzq*_8Ltks+2T=Us6hDsQ&!+ebDE>-{{~X2NO!42N_`4`R<+=J -e#Xn8)YuxesQwn!e3YnC`LzKevl)?^5;UuL{)1I@RhH!Ra0%sTR=j_sI&Mv>o*_A`w9ltBZA3*U(Q~Z -e(e-6bjp!mxu{#uIvH;TW7;%}$;2Ppn=ieIgXZ%|606UFaJ@q1GIz7)Sd#lMT8eZ$AZ#>7NM#zu@D9Uhr7AgEuzK7r%L^;T1ej3ICOk -0DEBWJ>UD2rzD3fZ`t;9}yKhnv#!-2p>HrJf%MZ^y$;9r{W(IZw`+QkBE$*_`qvm|KQ-eQtFY6jgM8I -v0eRdNf|g$P9d;Y&w$?J5TDYIjg1){o6@~QXMggi06|KCz9|%cbT}eM#HMuZ(4n*JFK`U(6+lSFxYJM -R)UiW{yGMxt!6HD<0K|`ogl{W85qwzjE#zqj30YzmrhCm_}>;ZAdboDM~E!cQcD?$0806Z^o -N+@sfgpFV&er7A_ukn3ICMHA!eWuOKEf&JGMiI4iv!cpEBG##XKmAGPsBQZyh^UpwP|jA7S(|hPE3V6 -&o2(r98HWAfo$Ng*`@(2oJx*)95vLbV__md`y&rWxUE=F%d(@L=4jFTHm91psZpd-77UQDPe>BT6ybq -?V|}ppco%Jni#3xKm5*NgF{+*8g7aq&xkR^%R`4oMAZAIq>Kq4Z))e`HJq#wDM&pgvOfQm2m~2#YU|- -+hJVCpV%Jz!?~n2!1~a#5-)?+LMEqzI@fu^=^yMeouYFj2IF)0J#vAEJ3U6}1GbF+k)6iSyA4)%x7%? -Vl*oa2nn)In?V!}r>@@BCOpZG@JoR=5X8jDAtAVH%1kCEq!nt&!{xnefDkv!M3oTAEe6DLj-^2lP#IJ -fNa!h*Zlvdk#9#u~x4ERABDo>fjmbC&dtEHf-1^RdSsV^2Q$BzyYlr`h`T>)CV9Jtt&^& -6_t1dGM{b-V(CHr{yoOCrRGeM6$w;9Xr@ZAAKZbgHzvp&A$2O8+Pi{DOO!w&CZ-T!+tt%fF|NJw% -di5$>eTlPI>HW|ZO+PybAFSL-10A)?RdZ;dTSx=l!!*$SjkV`n*bx36o4|Lo`}twEnjdGc^3yba*J$F -?fS5ot`aBwJSM{g(!zsR*;>S|_X%zo{ivJMBf12XIM)CJh{Q7b3zjDfd<&^(h<&-W;AJC;s7qNBKS*( -I~@$WNe&>*PD?h??Yi+`7%xA*XC*X~y0v(CMO`t<47XHa`T<85u}y-UyD{rXYZ_I^DE1rG{l-Fn>~G^ -l&)LG39(yS8m_?bfqT(4aQ0gKkwE0s?|=A7n8222q^bZfe)Ii~lXR4{BpDv~K0)<$Iga*hTN%<@WAv4 -6WfX$gf8yADvIHL3p~i@G_7w`vtk?W<>>uAbe}LrXK&z9k92rzz)Q*Xf8zN -)TNS_;M<_b==+Og(3czQOeOt9^MLmm9rc5OzREJPcA&z0FCH_Y}o=UpDHy9q}is2FLAMngG&m_~h^Y+ -=ZXTSUL#~;6^vFDqA{p(++$^81lg$rkCUV7^I@#C-V+O^B2)9HdojvP5ip~+uK2S-v+4!@=RP}dlkZ| -d8(Z*MYDR_$&uuK%=Y(?$=chwNQH|NQfZX&%krfB${{{rBH<8c+FWpM8b}D*pA?Uvrv&bNCYtoW|KNB -}u9xT2Frb@yCCysHi9!GiFQ(lj{jbaBgAp5gFiudTp%$?COyq9KGp|u=D243#qNG{e__$ym9nJZ@$S1KmNuWZwS0kpFYjM`|dk_@Zdpy=+Ge%4sl3Efcq(m^USNSzFN3 -#+qOay*`gc{$N0RwyvUU+S0<2L4cu@%DEO9JZV7QZo%h~x#~mm;|H#P5Af)?S@jrk5JSSOZb?@H2hfx -`>{NaZmczJobyPOXkIKaR7;tPQ%)sMg(_7f*g@MFi0iFeSX0?9nWB%Zr)?%cUr;=3!9ru54%zwn=a`s -w_qpMLu0x8HvIHRW{~;fT&hsJ#s%99AOkZ^fVFDdgn_;I9H`PyuzK0`OPqQ0*!-rbPw9*$q_ZZqT{|e -h>bYm6a=~9(xf_lz@iq+qVn+!5>GD9u;+P^5jYJ4jj=Ys0|DA@ZrM(&^AyXFi~%4JBYJ)?_PmF>X6z$ -M?GA-c8zNvdcQ*Mb?~P)e8+FW|Ki1qoQ5I6|DmCweZj{7;J<(We*X5`Z*!`1PBOD-!zh2$$DTcV1OWH -lyLXGWa^%PnVFwN91Hf-+C#WOf_WF;UTaI!*`XkPV?%{mU2e?O_^MU1@PyCSczn|v(=ZhB)Q(NiwTkw -DHz4ulJ1_llz8Ga@8m#7ax1Mo$eqg+7`>H@gKj=KNqtFHvWjxtAi17Gj~XaU|Rf7J1_XE=}ijC1qHoS -P1Ce&;^ULv|AlJ2=0cXz2Sc=e^$IeC2V@>+0&B`YrfVU+)IsuYy_^D*T>yf_4CYN&brSaYs0h_>{^YG -*BH3-^=+OL_^N!oL{WtJpB;ogWq#+P23Rvmo8lbFRus*3F$?=Py!mL9?M{W~0ia(7RTkgPdDwoLhQZVp`fukv=>6Z0KlR0{`}FBEjOt+}+8O -$mrsx2#H^*NnfB0iuKs@v_+9&B0wNcRUCFe0jLln`l>0kUto^u3GG>8A?%a=KgGYtSVsG!o(RGlv2)Z^}J4 -u7iW6+?#(?M3~03GfG>G^PW5558_phe`{09enilzAgNP`6KxT`zZd`{JR7VcxF^_K2@P1;cL$02&d>T -H0>EQDD8P5`XuU8`%s_MlW6GihTON(v!p5fd-dw&=6@RFG0tCCA85ZA57fFqd%(L&1MsJQkC#85#$Q@ -ElK*Y~DE=(b@HEkYCzblo=|sa6!ei1mG7V~b1`TR^MxQif7nOS@=eL0dD*u2tsr~O3$$Zol{?y+K{-? -IK67mq*oJxmE3uwT2&>U|8fAo`EmxS|I{$%2RCmNn78rIun{PWLpKC7B&kT_31Ew^X%NwLT3+cRiT+c -RiT+cWy4ptnUbmm1@bc82kVSl{oZRDfAkkB|Bo0kqA%(~1+-V --4cvgcN(=ga^!J$8px?w;g|Q9KdrL$4n?%Fb#UuEpMJE0-(a^X(|AqRb)$_vniu=a!WtkD&F*kx2rA2 -a={a*gn=LZE1S{Y-YB4ad%KlS&5{|TR!z#DkM1Wv#k?E!eJb%H)#D<5P01&!eU%4K))cZr5Kh=whNL< -6-2v}e%ptU|+Dg@#q2A!`hOD1&H7kK~Kfq67_i&?k-1$QZ*uXqx}gXJ8D290Az4aU*}}rI$G5A{DfC( -o#FQdi5$l_}5fk;kc8(vxI1%wy>FK_@~mI(I>69-^HJ{-o@ACjpmQ$nEAum;oLr -jG2dyvzQ6(aEnBwCO+#ZkTB7CZ)jEE%;tBr2iU| -I`Bb2|bwCC3}?fE%{h9?yo{!Dzd^1g8H%#>-sBV-I3pW_Ig{$7l~d)f6*XVf{3zpE!qm=K$jlk*{PMS -DaWSiE?#ph3%Dzg8zO(WV;Dv6^~5-|>oD#z1>6Q`&Q*r$zY(_@nNlqoYv|{KXev6tWj&8K=|93knL{G -_*uVOLc+=ebSc2H_)Dmh9>%%=AOqqUw$e2 -9mqmW(E_|NHlVM9EChKJ^8q|)&jYGh&*<WsCw@#z1>UpY+6nc%c8Q^RJO3M>^x<A#N#476Z+_ctw{eoK1unn?Z9@y7L50S&0Kijf+UQ&NKRuhjQ);I1S(Y&{KEG5L!M8m! -mmk~vI=}ey&xfhLZ+QJ-&6+j5q@?8h>eZ|H(xpp(zW3gHd3JWTD0iAy3SI&YXcwRpxPgwwv|tSa<1qL -hV>H?d)*~>Fgd1ce)E#JP;vw=ce}yvot?NICOl@~vUS8hUfBMs(t^v>Fk*03jHzaPF>$1oUq+?qx%;Ef1-QE@ZrOIQ5lw? -J-_?zyMhLk57rH-eFz#-Q&V|dT%6#o=bn2`;E%FKnZpje(GJl6z#XzF_y@8!+5y@C+62-?A56GkqjXC -A{r$~U@6Ybou>&^`V$a9tM)60!TKPvUR{-XVz=!IVlWc)g#iBh>eRJYB!2=khP{wK;bv+UND9RN5|7) -+k#$S2m6)^_>^Pm3|@le)OP7?KPGiZ!lM7VD!w}+U#?{TC1H$r}c{Dc0V+T%*RqrJfdE~sy`S>jV6OU -#=$PxP(S*2VaSz6SiQ@*&y*+9m1)e1bYapRARKU`M*BBh&}RbD|et7^2_wxa)Z+t$l!-3W$S#27M78@DJuh0N@U~o_XdOtTC?h@bLJpxV!OJ<$o&g640Rbi%rpi@{pUxkD{BgnG+HzL -`;lNAab-ZH?0PgDk2+6fi{~Ndq{I&C68hciPFEIvz4t2dB{RjFwj7M5JP`0nX{yKl+i6;aFN3J;O@p>J1%Nv7_Ds6Qk?+Sty?GJgD!*zZNL}2g}xKCsJN4S`nTW7@7LiET& -R5s{-^P~M4f-NM2ouSsqWvQZs3M`Q1|3eXXuNtmw-Je&;r~kkKfbJw1~fU{YRZkwNxjN?;-!-9s6Nb67>_V^qKq-;MA@Sr)b(A|1K!c+ -qpw9jMso_WuR%2b(YOoz!MiQ-d2{@(?Tg^Au3Mm-K_hsb_~;tR2olv5#?>x=Fz$Guj^yV5Fdz?{d-y| -C_wZsGT;88QzmECvYoq7SKf&1O8rdY0A7ttprLW1iSTW202>go_lgp+~F+CO2M=_0x*+V -f$D`t^ma@o|uZ1~+y0Q(&e&iD8z;Kf>y6|vq&u!rDcqxmAJ+Qe4dHIvh -p078^_Kdb9Z7IK1JGJ)^c9(4d>CH$$M;A3&x(p9hIC<|A_O?eiFE}gac^p=H=yeC(Z5dr#4-S{)}YKp -Q*pCqJHTN^-D)dK7W>E+boiS@eRWef?gzVoSl10-Y3IaDE9eG%3AIx=Q%%cj`PtwME*avkbigT!#7Z# -wc7o3^{k1yey+&;*9gv2pL>}4&9{&+_Dr$ghUrAt#Eee}^J-~ib -P^9PKN=+`k8p|6H43poXTu;ZB~)$bQ#j|cmmSkuP-4%Xzc$M$RaV-7%b&>WgCWU2Fe%yW@1-16nf+I<+W3E)z0B8S;^OPUtebdBmd=8til=KDE?g@sw#{ccsBg$ddEmBkTaEfMRvSVz?6kG)syA162BN3*g= -gS|fNF@Yb0-J4}8hFhB$y9lN#~kXl2hdMB&Fq`*b3InkS{ -MUAs0(iwE)spToq1{Wh$rVt)Z^!mmG`_p5t`Km5YEm2eq#NZxBho4|gjHh;(!3l}cTDJm+;((b(i2dt -Z*Ebw4`9eYjxQ1<(<&xG|*@FVt^u(#n*_LmmTk>RnWnF0sFj|c1bz8mM?crUo|9x&>^;-S%EzYBXsSk -uJXxXO=M7svhz_9nz0my&;B+E|g-I1N9J+SjoDA(J_s&eW%#dMXKR5%Yb>=b#(7pbTJsydhKU8)Cl-> -#8p)YunE#`wM6j*dM`OVX-CBT?TlxeKhuNwfv8^^}quUgrS1=4UVynB$^?B6%Ho3b1^@7o1hG$m -_32H@`ucO4Hi3O6m$FY*q~L%@@FV3ro#6PThV5USpP?Q8f9+j+Tvb)}zbKPrnlx(ZWPv26<7@AIo_pV -O_7Oz|hcWUMmYN7CH$jw#qmB~gBOm0eEXxOoXpW&Frlix5npxVEmQL2B6D3VnKA5zzG5g%#S_coIlUa -R!^Pl_kIl$$fv)4Xruk~H8efB}0zhRxFVeJn2bgi1vq1`XIH5RPZ&4e-JU#c}WX#a(ONPqtP`N^wRts -088A@s*_99~@QtsBH~ATJP)@H=c+zuh$Td!6_pd2AqSKZyV6V^5nlZTw@8J(h`lSh#SZi+hN>X!{WVQ -5KLND2qtDVa@6ECx?TlK6wvx;tF}ph^eoY{TO|;DVXQLoG$Vh<15g``ds>zwJ>jPa4ue3oEWUFIUGE5 -{JrnxG533UT%JD+$^W4JhyELR^oI@|`my6UZr&AZ(}6%BIB(uOH@?F$d060KCC(4>=In{T2j5;jF1US -0jJp=VlgE-NeS;NCCj`H&IT{RJ#s85~Dm{DloPzg}pP!#Kdi3bm2_eC8z;r70e -|2>(gy$I3#=t#-Z(x!-p#+_7$#1f=<*%$z^jpXGT`NqJ$V2-aWNw!V=TyW4){p;U^mA6*NUkkkXOL_M -d|73d$GoYIr1=5W5$e!us_Vnqs>Rm!P>%z5hL6hGwKDNiw$|;<#^ -Fw#+nNH?Qjn(eg^G&`d8+#8s;R>9%W}|yV!thRlU3-&KL3;?;A1s(4j-?ekN^~KGFfKaq<6$#^L$En> -TL`u3Wj&%@rdyV$R8{DgHv*E`6lq*#j&Iw(!W=(u?adFkgjT_zh^lTW -;GwAet4X?*~+OA!@Fwgoe^id7+cMkOsV`G%pO%Q_&`mk;tlmq_t@=x5ZD!uFRT$E|_;XHrF(<#KV2S2 -Zinb1Fd7u*{4^~?NYAKGA~g}Ua&L=AnxJg6Bd%ok~%ca4^S#`c)%B9!|<4 -25NF;>7>g+Q$I42%nFtr(Nz*irtG?w|7GT_5Kx#9+(M)%Ag~IL2NW`(fONaiYr!#as4gy_t4ivUlV;@j5o?w%ys*~m?I9xf%7+P*f6)w -jTnY}sNSY5iOpgFXrd*8^XcFJJEZgSbWn>uNZcC@)v~;s#M4&Twl#wkAt|4J{Qgh;%URN;NNJ=8_GYP@6ErrCJFmOJBjqs9$}n-eIqV|?S -Hu8^@!6rZtMqnfwdyMC$!m^Pr_?4&PLn4XV0F?uEmz$q%0;J4qxL-hwn!Y@0=!=ju!a;5ML8tWMsIz+ -Y|1%7J@qxyPt5swMIVNXBc-5z}?mw`*2rc{Dgb0h587oLnqv6?N^u1jCA)|yW!HAk?t;QO?*~zPEK0p -=!_|mlQT2M=h#ts+2ch{N_tvma!$9*KuUI2PS%**ZYf!rA~`3s`y?6_nVCF3FeWW0cW7F6P9ST%9Ywp -7DA~MeWTcgwotNXjedQrSLURpk#J@Fn_r*8GQY^+v~`kRK&Ru7M#GB -GAGCN?&HK)3+L^}CFqY1yus2yVrJd4Q8KgX@WGQQv9 -m-ziBju#hNR3c+_4n!oHBWt7EmQZY|5U$L8*33-XRWVxpEh30)$+A@+B4cJtz4_pUe#{Xf2$AEU)A5% -&*;q!V$3z3HmZy>#y3XLXl3>=dz(|uRpuu1tl5!g@Q3*lzJ~ANC-}``h1emEh|ff-zooUwdePcz^|a& -c5q7#gS28Er8S6}PwmMbL`_5<1ImhQZ7PtV@N5aW%q$6R(At@w}%pl8133-EjOPbP_)SzQ%A9h50RR6 -dBogQIyG&URij2OO^4-t1*eXaiXe*0bfxP8WMB9kRnBJd%dFj7F)k_z$)d6OI=$H*DrS0frv2hb$Cgg -#AI(Nfx$MX+erg;8cO$@;JaHi+HNl7XKS*ko3~3fWTjEGuRk*cMjB_Ok=*BUZz{V72T9r8#i>1NA2Dw -_3bbrXA4!rCrpR-b>HWr|a|d#rkS}i~f=xX8gvuV1$~tnD>}N%@i|Wt}}O=pPJv8ln>w~e43aET&ol} -`!#=zKLNP5)0%FNk$DpLuD}9OF8ta^Ynf0VQ8%gw)Ka}Izk|2u(Y!P7#wpi$EKlT5@fCa>_lwD5p$Pi -Jt^U@1ReWhMdMnE7SOV^S=vgFHCrF0KctuF8}zgKfAkhcE2EteWpp-r7`kB@vBvL=L?g*aHZqND; -}6Dk;}PRAqtI9a61mQJ+1PFDHx3v_jH5=4@wrh8@)>G2HCvjB$<2GsaDFR~;2n7!{|kSOzt4|@j9%cO -;zkiIdWb$EQ9LB_#B}kvctVtlV<5N9{jL1nVJ1fT8(FQbyRGroY-^GAytT#JZoO`OYlYc&*aPfwcBTE -P-AuNX5i(w8!CV!|8u_L4IsKe_ooUXKPKEQHgPSlE!ra|J+K|2^K(fd)Ak&-3r{pxbKon}xI68|ipwH -4R^e{a}L)k6tR@Q+9S(MUENmPoIe=4Vx^U95Cq|4R&)RF2R)cNXS^;wXEchp<7G~nT0(9!uut$E)3m1 -rj_#Bq^s-D!8SkJ?|_qhV~Z&H(2$Zsp;=^Rz1&NoJF3I)$}UgfdFmsT@$gP=fl`#$5ha{ta&{zVi=}! -{i9iqcjnC97+-YW*(b9%G_$KYz`?&;OSH9sl3_ANY^?KlV2R -E}aKiULz~y*T9{o&h1V|rQJI(#3QoZOwWx6OeTU+s2Nt|FB-le8p0Gsc~wOnxZb%8tZo&V- -25q(Cp7Mf!e?9#)_W8a*~A7n*iV1&~`KukSU8kP -M@U9=n7g4*tDLO(QX_&EcFR>nYvOfQ7hDs)z8#3s;>F9JZ-A>h&Ee$Tw9_QYax15JzQU=KL?zw()a4R;Rn4NWK1&tXcT~ -+)fz3$JIxNJ3Od!_9B3w)qs??P+nj7ZYR)&GGMAgBU<NIm&IqjSdPFF{DEGO1Ua0WZWom3|SWe9v)T%23zEOLC|A-*K|yAz2c>0}OB4|eke38NioCqS -5VItL`_4M3P$dXcsPc}`-fVA+b`%g+J&T;x6xD#AoF5iVMZHh^c5qJ!upx(Xsx&}2*W6tN;sBmi;`7D-~b7$s6gy2ua{L@rpq -X`%qE=^SxFd;_!9#^1@G05Uin&^*P)h>@6aWAK2mt$eR#WT86IIz)l9R0t -qBm&6C7x0#uuewrjT~0ozW9wno4bt!4tN9f(?n#8R>Q2Bfyhw05y3tE7-tX_6XEM)BCO})e@B4q>&4=N+o%1`t^Shnj?_57|*H&g=j2ZC@g&2E@i9eNH`7cFcY>NFSQ&^Ar -m$@%VD}I?<>uy-v8O9T7l~ioc;`@#v)4X}>rTK;)-%DtZcbqASf$KrFT*Q!naqkL-HX>G_L4k#`fJyHe#1IkS -G*_*gsASu=rj(->aM;2?)r70)X5G3lYIl%m!zn4D*jz7lCiDC`1??>71!J3*mQl@-lru@*6KP1*SjX9 -Q?I>$?ft8%NTLa7!>Vz0$4mG5d%lc>L?faVQ*iysgmh(-Q{w;If7eOuwG^58Qe-t~XDK74jMerTR=zR -z(9gQZ49pk)z7W@ic_Q6H{7&C0b$>RIf$KNDPH;k-*dfEWJ%?8>+Wqbc1M414lDhHEN3iG@F7Is3PrA ->2M(RGlTJ8=u8oEDNYwR9-$kaVnF8S7eQte*<>C*16)RlENyUV*Dy6@)hdZ!vxSqbp@IMUvXJmtt+1{ -kH?XKVhgi(vmfV21HEFlnHL$s-3WtoDG!dUBa3xYEJSppO1LsN5=+vc%yCCD%FjCa+@35(mRQqkFa_k -EzAD7VF`~{*`&y8H0({(si|w)#f@p!7?VDIbdM5C06xas6uTEu5}WtbvTZ$n1f%g>Ivr7d4hG0&J}m# -SH|Qsd7)74D*3u4P*t15s6L-(Eq#8Dt?c=0W|Tjl%Vghp9wU2xl~sEFZkzo47iJiqUyW~+f7K=AXV)# -EG6?P}=3L^~iTj;&?^xov5BK-ceKy@&ac})w>rQ{btu6O-=P1$CfcuT0>9c^Fo5BvQ#?RrX1AIsFvw) -wg;2nMrhIh;%%DK3*Xw^k~(PZTiT~BEAB|7JxerA;DYcNLBw-5BqWfIC$SI~71T~BWx%{|>dx*E@Q4z -_}>cjHR*dKUDW3R(mILqubuHE4N==uY$x=Or2wt%>GD`!KDa1+A%^)moVZ|5?yH*MmCZ=h%sNJL#QcN -v?`__&M&wyZh)}_L5u&-Z}U?@Pb+YTAGCpNQcHAk|6go#Tvi_by@}D!v -{`U?3;l@IdCb1{+4zFH{X@yM>eV(R#{oWTY#?`+0tE_t~@(m-L|r? -P`K>W2M{733P(neRgfPlZBR^zJyp0^KPOyI9&zc@p6jn9ztq&Kb3kgfnDfX$fS1Hso=)CWm3%AICf5` -w4?#Kwk&>gAmHwH1HfVv{Bx2)N%I3@^*b)_6I`+uqS8gfm5!c$v5O>&>#&wXOsr?X;+Pg3eY^-cA=Fg -+{-S6czwE%SC2o(C2m2(f${sDDo;H#cz;7`%UB*7ozjVkK*F4fNp8@Ka5s@FnMFCA} -o-gk8TeV8xxoG%WIFV_jaSXuRRup5LYmHV7g(0rUnsPD5jwWur<8t2h4@SHj&*_vcAD~SmvqX7jIekJ -MzK5#tm(epuv??&PSGYCGYVLsg4uE(8dInIZ$ozd`m4sDA*?QuOHfb+A#Xg-Yd0C@24qvE?~G{S>F#_ -%9i&JOXm8*TQP)e8sCB(k8MKlJT%qSmIbzHY!Sr;fl$twlbrd!!@B3@LtiOg2hSwqgw5-W>~VCmoH|& -ngSxto`W7>BU=j5Ug^|pGf)Dx@`*+)I%jGr$kN}*#%tb{YlW>VU=!^X6(F8#?OG?O4#b}C$L(Hec}J| -!fP`(tSVB><~AnT-*9E7&6t*vnmi|W*7R&!iZ%b5n7C3{zqtiB -6bs8veYp&^4ZP{x!2waL4p^5QM+#H-jJFC$kg!=`R4`ykXq^qqGuR7+)3ySVQ?!Han1t%O|Plg(<&XR -#EwdsWeDMH#4L_K1Jv+}Gutv#-l88AhZTtae#)-|zGBQzi4rvBb_CHpg=p!8ZJpI_kua(j5c-tF3*%S -CFm_^oUA#v-*+~zwvYm(wXC=gZw-3oACa_E(f;GiC+u7r*uZ7GyOl0&t!Onb^k9dchd4SIFshT+H(F@ -`dp#je<_^5mY-aAWbRKM(#t;VaP3OSd@kBF{rh@@cwaB_oLK|k4_6=Q)xB@w9&NH7_d(bOyyLdq@gD! -~TP;WH-efv9S)KLz?ETf&p0ECP$IsT?^N-6OJO1tdQ(?Ozv8^mOZQS2FTl3TIb4e_Ce5&02W|pD*zwH -!L?d37yd$8GO$sas`b^v}LtZdK#xjU};gKxqgbj)NOV-M}?I+vsd&({2FR|M|QINUI-bKJKy0G`o*RD -<>3fMWu@Hv#KJi|4TH@f;om%!dF|VV*QQgOz>3lM{izzbguFiUreIFc$CkH9z(&{Md6S^SnXw?H?~Qi -BPHNI383Nxi -#Gv0zdO{pWB+xW%3yOsVtQFOVpR(^E&TJ_Z`dX%=sb3SxG!zs+RD38S+TanMf<++4fbnot`(R8LLVp* -0gw6XU?D@-StDHJAwBb9iGFvfPD<|e4~SP3{rWx{}Is`byeo5-6N}+&yu1B+;dsC`w_Jr^&D7}$^wfe -);5RyzKuH%e-3%g8+TT3md(ql9HJ?eTfob+xd@l1v@&I+7iGc@jF*|=e;Q@-y5-t@6c4t@>1@Ld7y2zPr=_Yn45wNHXDvRXgd6 -Wa+tzc~eJJcd)j!>Ts`PJv~jmm2SPmjS4jXiWgXJaOpr+p?*b!aLJ5-)7P@f{=USc1Ck1YfzHB&$KP3 -qRk#&}Z;?s%0kZ0{IA}JAFo0P=I{IV=kMe8NR_y%t!Kg67?Mf4yc=paQhNyUt?f}bf1p2^&2yM$HA|= -kxrFa&wrr2p($z*dg|AgsV@)hZ6W2My!7r4LnT!Xgs|K<#fudsUP`SYUhr4v=DZ2@3?;Rx@M@ -su$`r{_`7^HcnjcyH^I5M8u1hMopHNM>8O5Zffr9-s0Qhs6Y1>PtSMb;&jH_Dq~B+2_GxxZl!*Yo&?& -Lgtm)J_hvQtCiq}T^Hd+-E&FmMPc_LHeW$X9W~ -%{$Z!eXbR7TzYo@!w;GJM~Gy&C$d8qnueOtwWNe?vcI0S@0J>M3+F)AuyxNtb-~hgr8nLVNMB)NOxQ# -?OGC5kFJ6y`BY>WKZ=_ISU*F4CgcfW3XKI{c~8acV+q>yQJA@d{+UBr&%rgI>vQB%lEJ@OwY@Z>UAXT~dR&8jXi`Gv5&6S^oaxYH%3sc_QeTfZwozk?cdZJ=N6K%3+(X(b*$ -M^PJx=sjPcZ7VK^T$+GBQ(BwFG2B$6gAun3c_Fk)Q=-2WbGbpZ=$ft%vg>Jw)3;Qyp -`}S!8v(Sw^ZltM08v0hPl?7YSycRI2jrrasP2QxL+(+088_w}XoAf%$_9Cx+Giz!EZKy2zM$bIYzL~s -zeiiA$Fgx(;wTO0J8l2@juF<55>>TLqy-C1h@!+-EeN^s`s4ddbXif~?luuvZ8QS-7eUHG_QQe~bRK4 -Ad_(vhbOxPn9k(pG@E--;7jRmn3=A-*%M(84brPAh{I}EJD!&oojo$1T)S-`tY1u$Z8JC`VM>(_9Dy? -o6EpNE%~%iGdwy-pIn&0kfIP!XQFqiYBYE09;1!{1&oW302 -iysBJl;o2qlH4l?7iF!I0^lcbIJrzS=^8)ykCk@&BeTDXZ2x(VH* -#pDyzx3(*@qV6^h4h(G=_}wPo5xDqU8c6SQpneG)8lRHOi#3VU>g@Egl*t8Q8KyrqM(yu@EYHr0FPqM -w}1!5e;bjnHIH#W@%D>iT!ic&bc;sTSsL%x75%x0k%AN-)v=pi5BJkX{1R&KL@de2cSL&l)M`MV{_Ir -yXgCkq-%;48f005t?2r})9YDP)@ObT+YclyE(U=O0Rf4q=?P@#lz-6@LE7#iOULY<>N4mUy_ZUpe3Z^1CHY>!_X;g9*|S32_o6KFEq{yh)(U$2p|`;<`0kw=UKMCF*5chekOeO1 -pDpF~nS7a2@_}%zlS|(CqAowu^?Y5+Px6YkB>3H2*71Nt#J72Q#8WjmBai5(21H*+x*s-RJMsJTueSpKECHm-ki$R;^O1LFCdT8m&+=Z**9Cf^J?#)3mrkgj28!l%Yck+^ -~w(x6D{3{ty&UT6ZEV0E2KZfdl6%7Wu?WwYi0}G;Lp2?eR;D*8Y(Lp<EMHI&Bu2O^K7%^vu+oD -7BdTiX+bW(^7X}GoWe29PCWmfh4oA5hrhC|8g_0E+7X{qfJg0osyp^`b^g1u|kE>+ -C1F(Ayk+KAsM;K>71Eo7Ii{B)cnS3mLu{{C33HEzWNp9O8{L|5iSp4!iOK%{}-Q^pJ2T9dv+yKgkn)K -y@<3z7beq7^k2cye}iY^R9_<^@>ie)Zp3#_G!D6K2i$4!7jX}v>JTWUtk_P+J6kbhl66MscH~*>b3lO -gKr3YapD_~t)RQ)i{(qot2}S1!8e>F@;Y?ik8PrQX=!q!(+nd{89{q91V7Z$YJPPXc`79HtBRELs`(X -lTM2OUO6=2$w0VaYe(SKo=_gv7O=PDJjLRpDV=a32=z^!`F}7H{avjrjSJ1%AuR>q0C)@j)bCaX_!{+>ecF+CUW@Uu% -9Bjn$sQm?IhOdah(JJ|oZQna0-pT~oCvPI4qnGHXm_k;7eXn(YS5L?bJt(<)yUO6NW%KDpy+$2WihWN -bT23GU7i52!wf$wzctJ_kfz8kHj%xAH(v}D-xlpS@qrCRE4n|D+8ZGJg>Y5q;hZHjefP1oH19WF^ZNi -kDpYTxh4$8h6nmZa2SxoF8&)4m&0k@vdm_k2k~9&aervk-8cbPs>SjI`8-ROGTMbJNV4?zsndIHjbM> -ZMSvf^QbU*_^zrjclzGWmEcmymLOR9@(5+(dH;+hbpN|(e@p_1@|u|%ePrkhpniC8TeCslt%bJx%Nxd -D4WW^4X{3n-!SSz^{50s%TO1>D@n3!aY`0mCyQ36ehrsK(7Zu=H>A~bC1i5m#Zc`6=m_v?_au9H-?dY -#(=yblLYlgzQkt>_GC9(JAym7c#?dvH%Xyx*HYlYH4!!R_jbyu>f9mvaeo>;S8;ZvKli-B;2-*e+C4&JMYGi4DD-Xw_S7Ox8|acnn`drTw- -rj$+J+aE7FVRQL+>pk8^D_GlDs>VL^W{G>LFXPW}m`pN86-s%GW6?jb$mK4Va#5^c}(VN7V&w{RwJdW -w~=?Qv$wO#C;y!r>TJ<6XNzMD))cUX4al?slmK*XWO75$xm&^ljlO)>rA6dsja#f?Z1NfB7X|YCfl%& -Y{L&rgCtvZ8SsxmwwNpeO(&Ucux0j2=^D___^bDR7>|G -r27Ml_(<{mmR*R6JVw?tY-HK6hnz+d$yT;#XLf=1iUU3(eV5FE`hm>$PxtOe6o0>@SQ%0!&{-fE5A7bbRTT3xzPdMMZ4&k52wo^+0t3QY9>B4%^&K@ojG3aZ`F3A`6#f1N7q_ -*7EhxFn1yniQwy+Attl#%s{d`4e54Pw7-l#cpTsUK}R^&zFeKf(QZiTCI5^yJfq^SlVUa(|Y_n5Zv>d -D7euvcNgKqcK-y9KWb?xX+Wp$JWyQC$luzrNSoyKU@yZ7tyylq5anR+1#huS15d%dgC|^xbHWO1?)z) -kmljhb2|7C#)Z;_@!qM!+rh^Wn9al@eGc*BgvEeYN4x-hD3S2F_eIL5v3PfmfEODl=wl5g-6(L+HSxS -O{wTfITI~>HXazhrZMdDofv=oe*)TN_i{FI)UfuWr$;^>8w+D{ECd1D;Z9{p$bx%gA$(Mx~4RJ$N_ob -u#b=6tVaM?jxlv8zZ&D6kR#L!{vi)g<GIl;kNjAYV@mrf&tr4Ft@+b>zm>{Ai@5PtU4B`Vy=?_xQ2w9e*^Tyv;8mb~ZAoLi4 -U)JV+FUQ;`=0>ki|oBW8{zJ`2c(00C*=t~;Ar9C-A-R5m>9GznJ -9PZAY@{Ak4Zr)pc#kWh!K!XOW#Ki80pI^JuDEw$jPibA})M7tVmJAva{e&lwrnt~H{(Z2ocnk2Ym(}i -fxQ(0I_C?V7m|?0vgM2X!zG9YZ;IkAa=xERfT7|!Z?@Nv&Pq-ZFZ=`-=!iW0*-x(`Ca!g8W)BEmG^K7 -F0mk9zA?WP}9qy3KZCz}1>7C%U2k}++ls9(VB*xw~K4!UYnN9Sw@p)Kv2{g`^4K>Q3dBYt$(^PK2MH$L1yRNpysN~!5`%f9G=baBJxZ&|r)H?UKf%c7W- -*`4HdYVL7JU~CJQ6&}^i$ct1iU)q=#PisCx+lNS>ZbjBklZBXWL0!lc>$2m>M>3ie&OVVPc1W1g>o$zbSXxp(@nleIj%8N9p-^Um9rWK7KpbO+^*`Z}w!?)Bt`sU-0HglIO7d_|yP{zRtadb!5|AiSvm;!siosR?go_ec -ioo(orpK9lqt@TR2_0md-8u{%9T>jW?f8ZxwxMOG&nnE)&0XfJ1Uv%-aXiUe@hrzMp(V!05hoOS?Z)- -F9S+b9CszhSAF0oM+}iw%lgW!_3Z9fXB^fn|l+O?~Od>Tf0i~!GEsm84Hb%M?~w9m~Te-D8PsM!YoPf -VW6`j&WrZbZpmjIrB3eC@OCy`!#NZA``^DcShvPKT7oom-++5R+F!>$=2Ic(G~Q0QynZQE*oW_9b6Lm -H_Y;Hh{2;D3gIYgQAL)rb@?J7ccpp11GCo()AiHY8pAET@c4KiaZ#xgi_U)g&6zZWd$F57E0vf|b%u= -{Um+vgvo1Nv%my7!CM!rqp=PGU{|T2E&v+5<*COps>?TobLcey^DPO(Iu3J+T1$CwB++Y3jVfaim}*JPT}87Y=7hXSymlx|L&OR -XN|W#3#~N-pZW%mpp5rH&nAtwH2*S;@BRFp&Gk+cy+yg^8(7UysI!{-_!i?H^sFtBj7_{>h4kX%r9pF -Z!v=?xZlV6A6Oe)7kFaL)uR^m~^WbtZ4)-;e7%v=}tu`~H)%W2?^C4Vcna_dkL7g2;4VG(on+;;z(gV -NXX7SFYulq#0MZ;OB!TG5ifwO25I5eMC-+mIjxo6R49!NVxb2`sK4&Ng=1pHBhiObD%jtFn+G~Se;u5 -cK73PT~pOh!NTr>{l%{~@=$8)}zc`nE;A5IQ1l7|QC*3AM1|q1CK-nC3mvyw)1l4SDwseM%0}*!^(G* -d1zQfzW}3(NKBfXviQnhuZO6BNvBmN17Tr_&)r!vnNd5p%;x5r|^0{g*Nd{i7f#hy=#q#>7i$K$u>gc -@h?iFOFQ>39$AI^8Nl^Z2G(Oke76>IOYh!;&)$%}{o8eSxT7lZZ<|2-amMbB4P*y(L{_#)JbgXcMR9V0wcoo;ND(c&9AULqyN$in#IjphHYJ$Vz(b{N5r=D+_`Sz2-W -}2S4yI^+PE0%Yq90RvP=Y!pC;Q$F|C$5NAKvhSkn^}b7v$cAr4BNhj+>xHJBu^Vl%G!CtaoY5VU7HNodx2{ZzS~vfdlRc^y_R*9%yu;XHTVXntHIdVX|n!FwAU_L1Mt3v^SLn2uvIh%P^ -{U>^cN?=yLO4|B~`hcPg*=uPHAkK51FSid$(6V2;}D{toM&uEn%UQz^|GFQoZ_% -HUL=IbZE=@U!l8@|)3?)FTF@ctxpz{YYRz^UZ3o!NP*iQVaq9)R!^ei@2vgF(=ygE~YG1B>N&4#p7t> -*R106&HNMlonI0D6VU@Wt~(9isXXmacs`j;B3ZThzXje?zgxx(q6PCwcz+PS@tW_Hc9X9}Y3ECXlNww -u(Hsiuqf>*_|1unsyF<&#A2PxpGL4=@S)l{0IkXn`S>RU8Pee2@X_lduyzMD|bf4_X42c$@yja -OOcb-Fqy$EmZE=C3>q{8(Qx;kQESD~5~}3>y^JkiqOKk*s_iQkl_LMDJ)WaFP*zEn-)?@+Q4QK~kNVjqyYob1J*Vxe_y6+2xYMr8gt#N*;cZWNpuL$nh -ncL->g9qJ3B~M9wUiSBZr*#HTU(@hA_lE9J*3$7bgA(tTwkp6)VLp0JO7ZUKLLKi#+hgW@=qp}>cB(6 -b>ST3&AF;wI)NQj?cJw$}q~D_*yrjO)cC<0q&lG(S?a;CH(D@eFw=!BEF+~lA=SnlwFXbbA-fO^P;jw -!!+Q|+N^6>FXZLUA%8RqNB6h5zQ^Ar};`%-RM^l{$-KCcHX-oG;G{FM0kPPrO<(dM1LCK_M%cf&Wj-= -uCyS!Wlc0! -v}Go1>Z0`lNi4-#`Mn~iS>gLlkj?yPc)q8?3VUWmV#hDf2=}i<`eta1ujP@1eD@=X`7N?k+)^zS7iux -VThQ<8sBg9#bp)>UhsAvF&%&;_@P1928XPiM2GH(qb4ixA<+;q)n8t$lw61*Vo+T{s3&eTuMQY%r!Mc -16p3Ty%MGcEsfaY~lJJNDJ3*>0W_w{NZ{{?SO1D;2;wXOezx*Ih7ON -|;GMnjx;X6RW%f>ccR`Da6g#2?~KEI$X&glg-+w;uuK_h0(izW?~$$e&~7!wvbM`7{IzeC)kTN9PQU& -JR-`TMX=D-@# -J&X4H*;Jnb`M+M2F+olWG&+9sI%RftL@z{?<@D`w}ZZuTGE1&!f|Y{H%>5KRc5r+M0hP`7!HkP15 -Mn#>doVG8}eYjQOHm~wS$l!3*=)ndacgjeSPDU1+wDl$LPLz>@XLT~7vnA4*iMB+FBQxLEY>8srS8Q(uZDdwU>+2X@pBFx -Xy;rOgw8Nj3sPE)DTAwg;5sj~k`&o<3T1P(AKXyCQuSZkOl-XmE)>06xFT6@PwHY#dDTQwQB~LsoeilazZ5Ea%oxwm%wFk3FBaf-jqy}_l10kaZU$h@tK`txsK0h+Hxcr2l-Rm6)ce<|j9IBCwuLDo -d`c$P_GzU#{0iMd()+aZx|4Zb;He=LAUWzk&oih8Mv`La)gYipmz>-s`;T{mm|qWKr*8zOoTp0_HcY;;Jvd4`wc- -(301{JUSrzXkuxzyEpup&illuS>_juW9_NkKvzJ$3I!ezgMya|L%z4pOVOGcV2;igX8=|yB6l3Il{l2 -H2&S7Cy4{#>sAHM1Nmm1;UT*CcaXLUu`57T&90Uj!I4^H6WAFq;!uWLL!Jb{P6<%Q_ -_UXF)<3IEPr1^>2XNBH-N|K;ER82@7DOYs;3ZL!59RjvF3a875no73UD|AYS}vy5v@zd#S-o;jL-`w0 -W<$kpz@kia@-Y4^|IJ@x01-))v=aDTg0;`=vo7)d6+Ue4Bl?@pSlN^{kvX^Y%QNBcX_Tpm7$8S?QS?k -OLwZ#LuEjqeS4iTR_Nznzny#bL?tYvj`6ek&`y2XKeVX{{S;ra6apGb{F{8f&^}50hyLyu2=x;O#w(E -peN~{6DWr;{K6hCp?4KY|NqjvjgaYhd -h~w6)#=gwIz77aD)s2|SD{DEnjAOB$T2-%?e9XI^i#F#`OCo4mmjM~&6*y>=F#g>Z21P=yRaU`j(?60 -@qIu1Zp7kYUFuJR?|YdxtXQW@|0YFr=`CYKm+rrUE}b-dc)HY}^(87=8|FnD)=0W!;JOsB2wiG+Yr5o -)(WOS6F1hu(WMIv-*5V$}X8427YA^JN*4FOg^Lhl%3p9D6@g?YC6{RyvX3Hlav)!aa2BAaFX4i&JO@} -&TbtsAX==-v99kOs8YPo_AZ94xqwp|9HbN_yoI`_r%5uMv+x;mY^>wL7%-90pMjGE+olP33@V&tBlul -9E#PWq|Z_55XE>C2DRyG@$j#OBfKU2ORV-Ma~T_ZgYDUpaB~&V7Y8>@#vi?`Yj^Snm>bde$@OW2>2kW1!F7r9{qg;XE+t)7mqfXg -=g|Any5##4bZLC9ZbX+hXtKN^MwaRMYJV5vq@Su?&tC?XzWi8S+Mww|Y#zNX#g=c-y(3+UowrLmG(Of -~jvi}R-@0=2oWy#164YQ1uB&cR1JrL?CnIJ(2OeI-g59#(%w<^O>wT-KKRW+msfOZ2KVzpWq`5vf(pB -Q>oY_|-_I{g7d)^FC|7$~aBu3zAE0 -RBXkbFnj1XrpzXU+Ab?+k{Y6_Q_W|7W3dumRQfnG)|0Scfz;j#aVVlqx-}w%4taeCyr|ZRdNiQNBfdt -gbgaztw%MnBQt%Z>pj(q6VaQn#zK(EoYpxHWc-pjHdUsuDnR=iH6SkyR#=CmJT)oQd -Gil$34T^uZv7#*xX}*}J^0{8`C9%NgQKv6l&uv@qMOrgVdJ0=adP?&qV7m(I!N0m|)Zjtr={T)s3R(@ -4?b7mW)}{R>(yqCN=8*LjxAd~6a5`G2w+8hyOF5kW^xZvI>z{lI@eb`JA&dS=&}?Z-u{55yaf+w9F+n -OeH_u25=N&d!{EcMmHQ8`VmI1dW6SOCwetiq?Z)9{fSM++BE6N9C_9WTiE6E4aU9EhaI{SZ!d{`p#G4 -BfUQA+X=rqvbXBjxWUACIx-4@Ev=#-;Q&_Rv|{gH;W3yT$kJqdoMa`LMcmkgO>FxG9)N42D -Pv!AApZ4-Yx$hx9r&xD2>J_?`6%!5)!hxwl+3X3}Xn%h;x@0}-NHV9FXPd;D_4u!^=JU36b=V(goy)} -e>%1)juICMs>(jt(g8kw9V?n1%Q5P@Lko|i3-O%=@Aln>|=A1OjOYQN|J -UT-%+jX4JWl#6h+*#T;_c6!}$v^I0`Dgha)B*F%#F}C9pR2`wK<;bAysDCYVjU1YTN3zQAcqkadxgZVC3s<@?}g{^_x^J)JU7|8g2`m4;eTc5FP5QVYD*zU&6CK`9dTr+`7$y -zTPH(5fA?}Sq&82GA@jHlHOG^o_fjJ=^e?Fq8H%^QV^8bWJ>SOPxo_|JcI>>Y$=YSu&C%6)uz|4kBwr -*~phIngAzH51xIBR^GTJ|CD-b&otq4DNbzd{?n|T}ZY+o_%i^FY3xDE$&b$CQqhj9CL>fLuv4dZWg_| -$N$A5ZhHx5UA{4Y+$Px8|GKp0u^7Lo3=q51pxNVhal;Z%#XCn*6TtWzZhc{-eF!e9y%>J};1%E|!GH0hZG0SZ7n#q}xS>T#z2BYCz9SuePp6d8*mfyY`}ibv7X2;Sx=veUy?d9D750PQQ)vG -!g{9K^$F~tXC4H67nPIhS#%^ivmlAzS^P16y40W5;>^VF?*ZK^_+uxVWKJQz7#ng_6^G=p$o|fl($m2 -ez77sDD+-UlYdL0zaT~m)dQY%p_dig6b5!}}=<;Lxr$2du*5_z(M|Ekj(vkN|;Wrv+A9^)#T%I_eD}H~e%==3dbp53SkNSQVN$~BfBl`=MUJTV{vdBCy -YBvbpDx|5uM!o8?OuDkDoNP(#l}EmJ)`>WMrZIF%h@lhh(;lW1txLQTo!-ddd$e;ptpc90_1?;&>rMO -JSakKK{yeHP<)eCs>#>nV>had`iS>x~PjzE^sDsypm`|(20#l?8StxTf)LG4aXxc}D&IJJcun%31y1W -m&L+c(FpzcKTW3$!fE6wApbg&Nl?1}RWNl(8*bNICS_wHZPMl_JX`wZ>dhsF0d$M;CwKfx~>k4tpxfO -PyWjK}ZEXmnv(ghsT^aS-$g&k+n=LVOxi*Q@1skq&?TnxJ^=F2#9#vWxa+=rFQ^gVZm_ZI5My&n1Cfp -?&CSy^&e6CDGgu+F!{5yRjYZj9tna$ym632d&Zk#<9@$S7l*i%u+^MC2R_RHfzsj?HMq=W1+&ma@Gj- -Kk)j%ci?@{D5qOik{bA$rmJ5+`eD`M*$XX4?$+1C^!Kbs{C%B*2BUwmf!38VIv>$Lih9wSVOnFheyUh -ww%EXeqx*~6!t2aJa`uP^`k4b9`f06jW?xaPuJvo{iSrGtXCLnWXcu#4cfq%F7{pq%&rapGTC7FuW=; -EO4o{r5$^E*u$pq_IqNn;*d;GQ3q`Nyb9CJSqXS~q!#5HX3_jiiF&z+#vC}@=q+-Yrf5VSh4qm@geRX -XZJd*3~v(Q0X;(pF-~8tK#M^#SY{t+jT1MBOmi8th7gSc9EwkIaE4x-pGzQ&AR+q1#oh=?2}bfY}mfO -?Q}1gsT*XZg&FS0~*~X+hb0&=Pz+u9ZxrO4}M+hK92|f2K1d`mb2-&voUtd09?F>u0XfgF6B&M*{v>=3+Cv=6jLEY0Ctr-Msp)! -v6^_&)(!qu`N7mq5mgDG$wSY{0h)qu4w33p%>zPKm6WjHh#dZ#tG=fsP|OIu^yyu|P-1g;${C?}(1HK -Mr8g9yxkC=Ecx))+P+Ax8j1CWk|QvGk{sDHOdnHqT3Ay*VlIm7 -zIZCMoyPdNPl4y)q -d#A`$REWkvc3-MPz;x3B6l6dTu^i0EGUva*1bg3Kh8e-!@S!RXdJzZ}B{Zzx~j|uMMms*aP`lHoPiSA~HPwix##Vasn`fj|jw#+Q?MxdzKX!*iobHDI+?l}m99Me(wUN+v@ -(D9oMjJi<&L;LapgnJS9W>h0m^JyV<7KU%6;&4PCR(;Fz>>6zL_lc -OV#$?8b2Q*Hjd8)uW#r3dE6|7WzeSE?6NS#))TT0LNaH&IVy>Gp(m_;d%-@qdns-cpkxZ*pTFEf!vS`ZC;>m7%` -Y#4YWrB&1;yiZtz~B^NG~vlfbD0v1q;qgZ5#_2aIGqQ=TCXCz-?XayVjN1{y;mIBvip{Oohs20OFR9^ -yeYRh%ynrqglSb4N#?pIgP=jTFFjNtYzhSz=+AE& -Ule0yg4@bDuo+}x^e^{oy-TS~_bIe(B=7GHlUcRR+jSVRq)^D(%6M8B3v$6Si%oJycH9V<~{+ae?mWB -WCFw>s5Tow<<(+@i1lq3xquoyg9-Px~H%uB|#A-e48_bC2jZ=KFAd`j2$hP{zn=nm -dE`h~@*4@6G;M4fa8Iw#aJpGLrrCG;cpi%-jDwYeL;?D_U79^}}wtR^8wVi9J+leL@&t@F0zNC^h$* -C~n~Ml15elzoDaF{o&NHn~%Ky$MJoReQW7FfTy3XrkMQQ48i*&kgKkrK2uGx)!H(_3);Jt_7PmDlgn$ -Z~}%ugO|j9n^f)$UHxluJxgns<&`gW0|z<}fyT+}%Gj -Z=rigt(?ZEr%e~iCxAIXQ1b7s?@vk>0nmG(&`AND`sr_i~~;r&;?44oR! -A_T53iayk8gKWTN&{}_Jm}c_T2e%Y+mc9L_N0Yv+|iI22ch$SjV$&&z=`K+htG+peL=~_1$402X|PGg -?gZ?$EaG1d!Ve`nJ_?Yu^X?KU>iSwhws>X9J}^O>9 -P*|3@B#BElCwuXC(sE@^lcAVs55d85$b_l-h23FH|h5TkMzgd^RRm)HNwt8eUr}H}PJo5GrLcN#c`NZ -?cCqKs~sM(oO(D4J%<}7^lQONOs;unD3SPfdf0lJdEJ1Z%?r|{%wlE9zuQJ80&V*c%p7K1XfS@WZJYW -7piqnqY``wyY*i^oDyX}d0lCit1Kv;4d&(q%K-Se?`|{h5=1yC4(j; -qhOgm?az7`MQ{RDNRZyk`KyAs(U+QVO!STWI;=xLEGZ6~L~7dRK{J!Y8he~k8fj>Ef0K!-nAA~yX&+C -w1he@ZhJh5gS2y$>wST%`9)>6|$Q?J)!I@6qCqHz9u~Acw~xk93xaXiH=D|7W^>Jl>DPV@r}-ij}sbj -_dJDmsv0T=@&d4cN@q3lfe65ED<}g#uC{hJ630icG<(B?bI%uK;1!ezu3ns(?4JAxy=3djA0%_`DxGP -Puk+)JMN>n$X@+xrf93yPGLPHH_Ii1Z>5wpLUVfh_?o=Zm+X?Q(L-zG@E*P|^%ourD?aqUPE`?nrU(3O{XCa=J|TKp*P^ -J?d`z61X9bM0Xh=uF<7a>*MHq|ta8{C3bMHXXI)l;$g->vy#Cs^azC`WWkb0>3Bm`#OGG@!N)9JAOSf ->+8jj25t9$I$peuu=(K(K;1+VT{DT(unDJ{=8mKcn*n~``v9t@IUg0mQ -K!fhnkyGVW0{n6(uMaRbk(*l@Dh$|}ruR$^|SLPqwp-7fjMLCJV%k6hL%KL*1e)vUn-sxc{uJ7sE^#@ ->s>5O#R<3Ee$j%W52@xC3%cSvSU=d(I0uvzg-EmgB5c)0u~*98YgiMFAyL|vckM-Fo5+sRS;UmriS)Er2&J{p6*zhmNbeQDBJ~y1` -LaU_E6~@;2^_X3@wq_0eBeQF=J9o}LRP)A_!$tX_9m%mCv-D>zCHZ-rN2Sj;Z5i(rgx<8gtJk^bWP_# -7drQB*~3ze3wQxW75Llp6|H{yx|6@}LAeeC&39pib`NVZW>@mPewQQerTW$(AElwZ@8m}4M`cb%Lwy; -P4smPNS`lkb_`V0pFpo>#`PxP2~Y`IwLXazbR~#=nTJ9DY34|~^132oY{a>?UOLMeeq -<};Z=*x(A^jc1P>8pD!~EP+zNg1)i(gRb{DdBwTTuZ%wB!4mn-C9%r0x@|DV7m2FyTn;&--61?atHU@ -0Ydq$OeDyyb`q+_TkT>UKEGZUUnO&WVR8^6Fh#f@%C#P@}kY)Yrg4x*%3M`vzzfVJ!l`_lX$MElmfJ7 -C35b(&7VI@?0qLq6 -c@`uY=kW6b}_}U8>`JK_@l+pbk1S37y=%X`(cA8i(#pM+y)^i)9n3EN(!}IG#agH3?wVvqsR^dJZ*}x -khPDfWC)9ra!e^%8Cg+&9Y@m$ -mn-VLxPFO#O7{x}t90$5g&QR(KzvS&*&zT+N?=+|$_qh7&xLwFZnCs_C4Mdpr(GT1_wawAK(+{kL;^{)}UrO3NdqhqN?$E -k7X_`rqG_E?HF3SkK|fY(^DyF -#|SZJ#2=MoeclsB+-`&-=k+0w;36(Fq}6*1HJ7ib?9ve;)y&_x3>YqjXEo^9gKN@R?G)Km4b -cATyA;cIr!p%u%}7^#BiEnmC>nhD$iSM0p`=zo5P|dJp=@SPZQ42r9Ua$=*L$K}ILG&K%NQ`vq;ty3kL;DDk@q)k*+F{q0P30(El*RclFN$-30;{uy- -;v1p-s*uzvx(-h=Lk(4FE|I9cOAdEcqoAt(%v~-p82_=onGpnARhtoQlMMr=jLvuIA=Lx9QbN`oKMra -Di&V1N*+HA?4Vf5t2>icvoXA_qu7bZW>NxQn?&aV()@4!u0FxHMJ9hxly?yHAUM)Kt8X)|j#8`daqyk -Llaj@|L96d;^e!QihvxHk9XA$xEh^s&$SkqqSlckmAkL))tZewqjdWHBy$_!wEcEc@Fb`eFzb5CeG6; -PBp6A_@Z?~Av5navKtrZVt!6&YP&cfduo^9x^Kz(Rmqv73#{1a=L@Az)`ukd4!-O7B;q{CT!%~k>RrH -AbR!IYVH4sZ0jC)c-fd7oI*G4gp0X?|rP==h2naOEmBh^<6@cd_xaVEOYtQ~1nT?QGk07oGbnXd`Rq- -imK}d8Jrd+U4Ya^JYG$I)SI9{-EQiAIUsF9}%$@Pb=m7$e-qLa}&mK(9SdK3Cox+Hg}PHQJ$UQ?}MiC -e0p8r^7eFO{w|HPmul@#Yj`cW_WZE6o}9ivsP)f<_23lXc%P2M{f%_KGxULCUi18c9WKQGTeNZz_wch -f39eg%6Xpet2@IGbanoZaF|L&$e8xSBd6^iW*3R{bJ+E0wQLF6hQ|_nO#eSXIbWGDJCD-X=njSN49l; -B_a_-T}3B#kl(~=aG;i -sJ9*Rj&3R(?L^v_;NAv6i75FkYRT_)NuHtT>s^B^2LCrm0V$Vj4@M=LFriaGsUO^JTv8=T<(B+Q{#{e -9Wr_He@r~S?$EnA$fE3bm1~k8j&5&_ZOIusa6*KzE|`i>c?C7`&^SR>bW%XJTV?N_*z}YvvgrpVv_Pc8PewSwTJfmE2YgjEeUKP@q8 -_G758ENR&k}okO7=xcmx0gur1_tStG8C1F$azr%&u7fh0}X|X&_P`Z9auj^f -g>E>h}NC%2G6)u^ck_D#liN)iJa8pw`z8-^4Y6zk_;yPI}H>pe#N-+HP{AQbms!HKX^8)!P~U^PvCk~N*>u|VzpgT -=0N&bc{}9rj9K*@cDAl9U}qU`+fnBQukKimx(!GyNa^T1rK?1mlhnU)?ZF*M_}-t(d}xdL_jy`cN=LBFfI(^JAzg)*Cd -nkyc#)<;QaG&c1^qk1X_Cw=sP$W<#aoXk_&51HTpG8bG4~|EYShk(rE?W8fS)T+ZiB|p2JrJq;Bd!+U -kQFDutO!u?1fr9S0u37AY$3`p#Lan`wZ+_M;<#wa|UUg&CBh|v=R8qwPxgf8urA!vNX6DdBg1{wV5>c -o64LgiLz*oiv{W3DEkvgyI0B@q37keuaiD~;n>Ac?Ri78{~Ug!h9o}jcw8GJ^YD3F8U9v*TYb4Xxq~G0n%_ucC1HC@cI_DKE%$>o+crdVVBt5BpH!jQxEb8WMfg&u(VO -q9sPOqw-2Uzo?dDcG7e{fhZbcEeMds(e94R+Z?mm7Xx?Fy?;zuHW8kIeAiOT+Y*zee?v|4=JcRpm`?M -=))0ba-7YVjEP#J>rN{ZsUIE1Z7I#prJ_@#EHXNsOD88HCOG(QI+eo})bn#o3`Rk44}_pR*)j@i_xWL -)&S8&*=FGx_Oeocg>}ngJg55zqA5&w_CHjv=2fZz9H7z>Mcw0Q9m`w$!H?{Mm)PO8OPh+$>L#xU-xQx -a0Gc;UNr>qytVFE<-tGE^RKjLzZyJ2&weJJf1n1B((?}@ZF{`>kw3sn^L-LC`sU6S`v-n%^F89$|?g()L}DC(I)(j-E -$2T;$APMed372#XgOBIDOXa{PIO;qo7u&HFQZsH`Vu^K+q3EBU2?3Z^WnV>i8ChdhOlr9PIj)FH7YeR -!|1J=G|3muh4!3sG9_Qe~a?7f$z(}_bK4|6s;4muqqk@Q -ef-IUXV`y>dnyhX5`OAIe(st_A-(A3JiV4_reaIg6-;uer%q_YAnBN*zhXLUh;jkJ3Ufg@!PPe_aF_? -u=7W(X-Kv&I-lmvp?m5(q_M5eABfm>KJpYG52YW%H^OZ_@Y!3&n(ASnPZ<=yw@f?3Wv-ZK=H1V#$Ora -5B8&Ih{4?+8q$)n{cq#JU`$tw)8D1+x{j`!eH|GhI*@m)dzm`Unt=}_|;yD`sQjiut{-IvrPWZh|dot -2K&NNRGX?_6u5gpgydJpKt_hIQsYJ6QS) --BR`EaBTnvK({zDV)o{^?C8==;g(TH -fhZG>@cZ$K0mb^nU+b@qWkLs?YHEA8A^hMDIVMz5htn9sGTM({2mB&)44PSKZFv&ucniruXx-_wz{Z@ -qR(mZ2o?M_I^QCIe)*fX|+P{7i#YpR^7fRmQTtzCH2k+k4rRnN3?UHI -PKi|yiDHCk?a;p*{-nNyO-KHS)2vDS>}Fj0pAy1O6TodfC)K;57O~lt(|*%l6G#h)<$?Iv~#pKOSbFD -u}C|&n?>5W4QS`GbnVpR!E*c_u^0 -a0jobd33uoiTI%=Pj(V4sn%Gf=kjv$|aA5)1r3#P^GJEz_raK}+XhK1vhso -SvI>>6r!>ob$!TBL(_&8eHayq>J@yKCnlvDRjM<&H@^*T`d;2kk*3H{Cn7yz@KRDyEcDlKH$D+99_!e -_kAA^kp0M`dC8Bc?O9{Dv?HG1S|laCw?<+qkvn522Ur(pGK%Q+;-=Igo)Roh -xm7T!nT4#pW#<^_TS|qwR%n;*z)X&+YrGHrm<0~PO70)*6OZ>pD2IKpC6V_L{$k(GdYx4}T#*OCHen$ -tFV9pXT?jEIH^r@dPWbm;YnnT6Mg2b5J9%Yv_-nYIMFwW54yU<&L+cy@fZFq#Qh1@#XTF5TDr+N+OQz -0o9_aKemMX&4JqK%*YD$Y0xje~{b;sQa7G_t2GTg-=#?!SoFKPPB%0W_&-QQHqfwuraHXW}pMkMJaV< -)0LML!nq*;qdhvmjHX7IHlru-pfo3!&unKK*H}aM=u%CF$PAqgZiy#Y>Vcv(Y$u -z56uUoaYKdXvuJgrdMRd4HOc5oNm)ZjN${KYBnsD!>ZRxR&}`Zd67e|7rFrMwD0k>1teMK9Jj39h3wbJsg$xYe-d8*i&kb*fWy1Ol@!olc -6{k^MCXtC;O(v$0OrT6_j7(S~GSR%&B4lFr`+AwMUQQ->9=9y!VmX@aHJ6i(EA`jYTE9`q0L|0B0H5H -Dn2W;K2(D>sluwA!Nq}*~z4F(+Ag$QhV@f-t=cG-|8#QPXf&kINH)_&qL^Z0~=j>7JMWA4WJEoOD1 -3bOja|W#x@QP7v)Q=umSHnQ$fFNpT4l0{N76|i)f!Q=fNSG-&b%i%{4}vZsbcs9>6Iq0&d~;cx3NsJ_J+n|cpbps&SY^G(u8l|_1e<;1ANTdU -|mjo;pXuGzup4BY2Sh4@N;M^-0@0h^|P6LePJP&UtQbrCj8IWVB@JQJ4_D}Fw6%n2wLPZ -$W+me9jgouLNGH96h`(11A%w#e_J-_ -pQ-hbXd@|m5z*Y#P?de-wi>$%jNRkj}E@BjDb2hJJipve!sI;_svqr;K2ka8oBu4lFlZ -M3Q2dsiR(ez!YgL0Km8tv~NR_E4KWjpG9a*E^^(qs9BJyGN68II-`m>VH8nqOdS1Mm4M%C40BLYGl~T -T~l$!|IH@G`|pUU;N&@$MW_Aj)wuSo4>6eBFuhh^z-AWe$qAJzB?xZv%{GGk?Eqc*Vs4L6JCJbWWa3U -Jq<&tfScpW)Q)n@`(e*$nB8r(f3E>E2n+6;FBkciGH8$2bTIs&rTa_Rca6!)2D8zr?7K5 -GQTP?3jjs)F?n9sz1k8_8tn|4D)O&u$tM4ml^n5|XFd`Bp3V}XC-F=_+b^ -Ik)Bb!BZHCtF`Voj7O2iV -UJG+bTythVRB=wmBPn`d!Fx{Ko&iC)7DE>r-Y%-CB#ZHIntsx`dgmk3G*=A0zcoMb)SLJ5GIB7c_zMo -OZd`v^l{TgPf}!_`JK=kuX%K&a5O~AMj4v6|IB3fxS;Z3^}g_qL}N(_(1DXHg`*;tUTkE3*TC-5B-3p -{g5`6jvRasa!8gQVvg*A2j}PIC)%88c&3e&Y!heZs5V!qd?03e%Xp|LKZrA?1a;Pd_m=iG#4(I{A@C{}=Vlqs&1x-6o|E}FC$ -p6fK#q4iaP$QwV~&EU$F0Wmv-cu<{C)$@%yq~A-G)7er$@y~cN_jZ-*`{P^hjq}-anobibgq%dneXe^ -PbwD-;HWtf6t^(5{}n`K_e+nR_;7zXUur={?)e+LPoX?U@3+X!@Q$M45-SG9H_Fl(IN@zv6DtpY>w$yx_& -dz0@DjkEr_MjT>$ER1ei;>JNBxrgW@dyL4z_h6r_`JlBW&Y6t#a!rr=NI9$N^P4Q^;`|&ZHLzXetDSH -I%DDN-An7KCBgCBEX+#j3NRPkfraWBRcTwOi14`Z5}v+)Cdg8M2-n@I%i -SHQde0)KPK>)`L9)JP1Eh~HG?o}BP^1OCL3s@j!9yiaXHTaC5(Lo1|R*rZbn!r{ue_u(=2yR>r*TIn~ -wyWOK{gX=&uY~R*8^3e8+=x6hU8)O{zxmruR!hI#T)5o88E>a$_h#maq#v`;U4NXfBtUdjEfiRBN~23u--E)qDSg#Qf~;g81g^IqZziJ)8o+Yh3_gI(mMP?#te*x>- -hfsT>GrjEMLD%gf6VL_@;n2p-zwI>kxQ&>H=O6Vq2?457jE4&8Med{d7C`YgH<6H&yI7Y?(YLAfs_ic -5SY&8e@0tN%jrQ*1GHPd{2L(?}Z!nou|Q{vOVb&nIs*K{ -G8c2T4J+LD{F^4vIqFcf9J}v`QOCwy#mX*ro3nznK+ygLj_TVC1DT#R>Mc4;?ZvJCsigq>^`>q^+rPPgBs&e1;O>;&J{v%x -afH+JE~DXk98@wC?%$3%?7Z1YX*W&YMg*?Y0PjFEsfa%#?}XB+k2FAhVjoeE)Sxh_@yYNe@C-@9Ov@H -IhZyhG_L{c|;AK2bVe>bN$ef9iD!Pwc(P(fgD6C1x3Z%+Yu+``cny{q3>)*q8K^G5V6RQeyft`-rt!x -fESzOcTg0(vHNUIZWRJufin<_;jmx!E~H?Xp`~hhOCKR#-DrFn(V&@V`2P@VoUT`rpU1bKcA -Zq4=|vrdNWmgPSRzJNJHj5t6thT3#%$xFQZH}gXcGWIUp>Q{R_x~^G|_@VN2Tsj`(nDtxcQ1EPasP)6 -VXnXSBMFmq=ob@wx^72lG-%69CY{SR*?~(oOG5YJ5aFuXu;yf>o_?t%c=QjFtdFeBfWcIfud6aGZc>X -YI{hX@59Op=Xz`cq;eVG21U8=v)=ZJ!-b3_lUVC(d;-`(iqC-wwXN=|MIAc*dbA^wi#*;HL9W%T%(Q%3ezlV$Awy -5-{k{{C{A`%AO^FYK>4_^?OK@$ASNq$qq~%rpZJ^W)$l^EYsO#6_#pW%z%et^B`Y*B^ZI^WHsEcvf$t -9c)=?g6}g4O$V21wn6%~zPaxE?evi@|E?IK{IbR1i^dzbysHEc!L#3mv!CZ;0nX@^IRC4_i-S1#$n%~ -0D}(oLTD1c?qH?&y3BBaWhtXZlXvhZ%)~+Br*F=<|wpVZY`{K$U4OX; -E!3K_Ov5$eG@h26=GcJkidKJOfoJHb-wJcOf%x~Yern?3DS2y-H!j8Q0JbPmA}FmzF4l$?+iZ-V=f+L -bN^osDZ6cRK^E9Y+uQ#C4BS1z*18?CA^rI_^DYl69p+oR@Mrj4B|^<4YJt@EPCOa7hq0e;N -C8*_o|7J7KO@I4GV(siW>b!*goZOjKoJ<2zxiB0%cN1aH#_(%pHC;jF}VsY9n($+Q-J9KnB&$w|)WAb`!G --A1Mz7q{vw9kmWIMUD6PMPC|-`*Mux1E?GZCs^ZKia-O+HRP1qLX&3G5w9S$$ibd4d(w-l4SlA#yoi9 -J?tyY$(h@fuHfKwM9-`7UeQe$j^g`dKap6tj8nIQ)qkfb>QS5omJ!LcYR37$J#xi9;k((+GJcj+n`x?d!-wx -WlglAtAd~z{gng8dda9bVVQ4LtwHP@V)VMmMKfW^SPYVUfhn8$K!P>yne=4?6$_#gZP;J6vIoBaP*@! -Yj`=@91MW6qWTOIhelbAQ8r!sz>R`e)h&ZE&QnBEa*<) -=(OtPE^KgyGmbG(k$wCm~q{3mXZd}&4CPrcc=Ld -ZKm3UZ-TTrZ>pFm7^_J6XMGBP>+pAjjCa7etP{L%S|@sg4w3KqtXAKfK;H~e$@o0GA=i2yQMyIXqoQ8 -$H%+VOU9YEDlXq~Qg6ey1hrH5T=xSV{qEScQiCNNATUnjiKnBFRNw7Vj(7WQ8Di7)i{XOx*}_L#FL~d8b)*l*wa6I`SLWMg-m)=cknL#`d|Qq7zBm| -eYm#U4ggFbfri1CquH$~Kbb?R$ak>4AN6Ji`W4AONw3}s`Cm8w%x4#T!szPCunKWk~>IDI#mf9NGFMV -k?SnRE3QRB1?g$vFu7sY!=;%uAyC=%nd&$xf1Z~GOX)%`O?Ah=F1?!(y(UbBGuhRM9|0_+k#+Ej5=-g -~*Fz41ppVw1prep>nz-V_-07-#dTiGj$zcY0&u?*&M28#BQRfc92ri-4k!PmU8$BEDbW%jw|96;Bgjt -Z&-2NcyNH`)J=n`v%&$D);#Io1b&5gEl0pZ^n;)H18v~Z{zjwUj;MZbIcMtWpNuP -S^8VxhxRjj{`DX4v(!9&ip8tp*qW)Og3gj%H&EGu6aW9o|%jXw^UoPbNyVRxb-*i0x8=i3{4)zT**1O -w#t4qp8&)fDy^oU{@cq;wbwP;^*FUwCt`IdY2jop@HpA+zK03Nvwb<#FsVNOa%NxvxGZusYy7;}sEk% -`3<*-wtm=*KhShkLn2Y@#iOtxOLE8P^H_2c3!!Hhpm8$u*uP@n|*aq7bKO=i}0v+GQX8Nj}1lI@%Gvu -88P$S%!MuYfeSA~%&hu0zIOAkJI0%YA12YQlqYoXIoresB7tO_KK -K{!9;nU)!-l+4(S*TLW>9HmGGuV{K5cMjhGvlv -s8F@SOa>tz$fCf3=_7#66vYj5u0QH^ro?qbF+nvQpUjGZ@;ab_a*2}gl8&30}=JDV?r3m!i^+6LqR<6 -vX{o;_B>-*PN{W6a?#(jUu&|L;fqK#tB74d97RaSeXq&@XUJcn4&}O_y;A=E-wY#=Vt!eS-Jszh(Su` -f6`H8#((vhiB>$X3I58@G~Fr@|R`&iclWbjPs%G>J}r%6XQIGCn|pI(}3?16^kzbKDGYq;6c89mU~hU -@J`o;wX-FN9keI%3|s5>S!C?(Q@_a8FLaNJ-|F^H=nc1hJqk`beuGC1J7ZUDJmEi$g4arn;Vv0NO%#* -2Rrwl4fN}=o8}aV`4bV?B#u3+vwvz1IHbKSHmUegVg$vC25QbdiSUTkiYV~_AgvXu7KmHPK`xxh8*k_ -+R6KYBYje<7R -gxbJ(Jy#wHC>^)7xwZ7KG>*D%3ueezK;YsBMxSD8s(^9_jod5#rhS?ECZ|JFgDTOT~!^@jfC)V~im^^ -fMz~PT)hfcV85orUOn*n2+r}<@;)TFXH=(xfICxlq$bh9rL*!vN8Moub4iE?|DMW6Z9ztz6U)*-lYf0 -1K}NhLyX#_Hyh8*6LER@{&?vTpcFxn -5-&}X2{z6UMcU4}hNx1_n4!&J=Qqvq8j{i>~bb|GU!owQ+IjGNyi{m$+FqiDYmbVL;Cp}rwSQ^eJJ=% -42Eb$aOU=5v7_df9xQrH5WHpY!z40rUA2dgwXxxl|ABH=p}UL%n#Gu{EOnz-jL+^({vYeGA@QACCBd$ -KEfH=WW6spuG|I#1fa%RdGM?{rl~SJ>WYxVo$7)vA1N5>^SktM)MQ#_#p5R_iuh+et^0-%3806l#Z8i -uoyp@d4=4dBeWZ2Iok8S6_Rnd^0fvflixa|>hSvu^wq_?V^tX1?nJ#=zx{JjsdPW2yPoxT&ZPH -&wbyH58Qb8%abZ5*uy>L<0Ng8LJ;;m7%y}ph9IL+7iY-pGGEqoR^FC%WZjKTawSjwS0jQB;HE -}-w&cnHz-2CzL@Wx$8~BsD}i1^mbZmXW->qxEVgoznbrk73_2HwtMA@uiD2=G4M~y=y@L?`k^@4$O=A(loK;f;HLOv$-?d@gn&8YeZT -Yyk!vmUUNFaGYq_=c!ozQgE3A=XyqZ#u;T`_J*w@M4i(y$V{~1J!+loha3}7G2vY@XT) -z*%+q|1=jUi^ONzR$dg^MiX}U}F^jujz90P5hdM%e0q>XQede91Nsu8hk9$y_y7qne=6P0srzownojRY~B-PfpP@cBnE18Q3sy6J;>JKbT5ae+qs1Qzd@fbJkN -tyl>R9Bo1|=zuw~_jl4t)b4~+3HJY-0uRe9S2s}ty^(jvg$NsG372RGfNv*@;r^0P3=I6HtAwNBmNZW -5wUsgADNnQF++wE8nwnNz8VHZB)fL)us-)6P#FVVy)@0ON!{#K)2*P|`j)rq2b>6VG7I#6fOn&Njd7X -2E~#ngT6j7{L^ws+VtHap<79%UM}ltIpCsZTq_vg=b++30fYpEl(z`ziaP3r-nZaQt#*QU5n$Jfsuj@ -$l*{bbMQpc=;91SQEW14v{v9J?21vRhmA$R7)5rdb&F+vd&AVoKB8UZ8?$w__t4I3)-12 -O=Z)k~OBRZk{9t)gO}X0QMf>&Ddpoo3v#_@?AJ3C{gFG7;Zv*thd)AsLX$W%>&3Gq5FaCK-(F^Ho87G -flzcpFP;wKJ;+X^i*k7s~$AHdwbn!B~p$l+TCd}q#wYWlGnF^F?D$9_v9{mLAE9ly`)4Y#rWAkLHD5o -dQO{ZKn#(<)B*v?3iRSzj4NVOG^9()4!es>_K5oFyw^?A1F7Kkzb=LfPgqmSS0{n~`n38w`s^f$$z5x5 -JM6<%hpHXBi)XJ%bltJ0+zEWIuxTmIAMCJCt#+V%o{;6c5~9jaV0nuy&$87ML~)Bf#ra60t-LyYcd<= -NbylZ~;u5Ejx$A6Vcd=)WvX7*`SW5sN?j1iD68FbO(2{t*q~F^gZlfKO=evW8zxLIyes#2eY(y8+Cw= -}BrQalD-n9WgR$!g|fXQUgnQv(#zYMVPXqk14bvbJ7_jp^&fa6PR<`zu`jWWM+{3h)u%_MwDo364--n -YPnKjmz$fv$Y4E83V78-FWNN8TS=h)=E_$AV6=)f8g*9g&26P0KXbFU$olS>`fdXs- -G1zGUoPPbJln|l{ZV+fLE%ZD#WGYFwA#vk9b*j70FOAY -Gn@9>dp%-PlT{SZ*0LI9$@_Rw_A@BknWK0b^31$PU_3eMo^3fVw_^*+Lyx~fdv1{+X}}7h-j}@>e%#crpIaK|{x!-ThrI -BKsGN_3fa6(f5_2-_xa(v@@A%P^O794|?yd}nduHP=0e`y^)IDsMTU$)~2A;E!tt*LRpSDh(_utsxEe -1a|$Kl=>$Ry%k_#*pLtCWFUp8D4SdxHZ>Xj=5K9o;fYUjfXVA>i>Ou)DrvhU;=ceydvWc*In>ZxlC&ZhlXiuWA&aOxe8G8Ugf<7h+H -XHE6L^vfWjI?vADTfQN~c8l8~P3(!&nly=hvBgUg_tTqJQTHoag<+;WowNjka|fm0y -zd5IFO-y|U~9Y44b+2Qoe`mPp%#>r$3<*+l{ANGtV>ru)jrduKqTZgQ+WPECWdmmRwJ@fy8>0G}|3UleM -_<;VLf)0~4!D0N*G>3VUx571xZYNscaBmP_yP9$Ieg#!^ywn5K~9u?7R%ALjJ9n~?0f3l`@)?vS7J*# -XuDl3UY{-k`@j>a^~>zDP(Wc~e=m%1IxsN&y_TOQA(-Mtu%P#l%nRDjUO_4in(|+bBO% -?Rh-eOhuROO#Dt>`OtCukDlfDM)XY$RrF_qPgDfA4% -JDUx-4*9cIy}n$X_j5uKM{^K=M4L&&yQpOgnoGsA1^LQmyt)a*T|!J@vR7L54=TP)V6fmDKL*F@1B=)|X{u0$6`;zy~HnI3p`7UGiY$m*@C*JN-^H+IQyR%&%l$kHXJGDqD1Y5_HUInm7v ->*puO|1^p3CWbRe_0}%uH-L2>N+QEY9pOZN9sSVG!ht@f=QuiQagHqPPw?eRUms7Le6DJYJZBD}??W7eF<$VF_h*zQ`m)d##9RsJKW<-)NQk0wd?F_l%5@q-@H*#k?owAtS|GTi_%h?Cu73D$jyE?8Dn<=mFaDZo?U9!Aw -?rcs_eNKAIuzR~+BNYoW$| -j9Ui+y>P#YIoH`mNGpKX0)zela+IE+)9r-`Uwxn9>k=IsKTW~YB=Y~hHpP>~qgQ6mYwB6@c}{QDY4eg_Mf>to;1{++9=%tiy>RGrlD{IKCkkjYUu_v({% -1yc@EQk$hJ6q1iK-m5CGO1TI+9oCZxDP^HOkgZ5FzTqXwMJcumSu`R%|YG+MLi&oOvaf_o+aWc1NRpI -Ny0Q+~zu=Y&5u6YcRfD7W-fg);oHPD~&O3zR-AR*EnPBwu~JkdD#DOFt3!|8#}I6jEi|K&g-7#x%W6WAqC|3Af5>amB==G|{bXzX -=wD%roD`wnLj?MQpEZ^Mn2?hKw!kPFMKqGw5BQ^nb2Wt(<-0q1S=pZ&Bb4wsAKAA1;c8?=$U&ARAZls -4wj+azAM4^&)!SLe~70md;TfYE+B -sXU6qT0bI$aK3>^|}eMZkeQ|3hsTJ!y3@V|DP5vGnXZmda*yLGbEH&6C64_?|y<+lh~qDSd|rhxaEEb -mNvX)~B43N)*G|53n`u^%_1{$9MFj`uZN^={rDQ#P!!NL!;F%uU#YcHe!QwnR#<`w05rU9+9`XwAY`v -sH96o)Bcb{chl!U7UQN6MNjoTx6iJ)R)rU`=d_jM=tZMxg?A;{2VXyrSjbb`Of8iihDD~-C8w6blX!h -TluX9bRdcSf<1N#_C69-R8>?Nh|9GVT{ -})Ak3kSn5r7s5q82SM;_+9+{{1p4J!j?PsumE01#SBm39wXWYR5=ACg=+g; -R4qiyt2m}kiR^a-#D_x^BWayNYe!i~0W>QX=-LToF%+)^A$ZfU<3@_aB^*)!TwRSt2PTXwm@FIq?Qi! -)G;{3!9BWf;dMZaKp)BHlO3F#S(z@{Z%gu=Mc?B`W=Gy}1T`u9o&P=^w1@laH*k_)MGOA;6FKW4yD8h -0k?HEYB~a@7^o)?mW%5e;#m3&bcw6n|24`#>DRMToDRCs1?8cOvIjZ)cnV}n>FWu9p`^EYW}h1qUZ05 -Gk+`QKiBG;NI&7(w4W07eXbw1%X_kbac7pmm}w%P|1XoVf_F1dQ$FXc_NCi@R{4^9=i&?xK3G~@E$zQ -E{N>nh5`N7Je*J(S_qfajcRpP3^bEb*4%$2$unAk764yXKPFnkC`1I7Q)@54Oe(t%arm0vW(iS1A{kd -lQr8dKsMztMWK3|o=ntI<>`w@G@_lZA*+d?x*e-?B{bZ3edYBuStCh{*v{4LHo$MQ7fFRA0;xnD+|Y! -ok3Wb!f=gO|}pm;2fA$el3ngJs1u^s(-^;#D#XpHR}n- -gm>5ipQ{x!((K6ncu?XF%}x}Hjc;PG0w%vZx7n$L_o6+7;H -IcEB1Nx$ubrK7POP5I<%kvpquJmoDei8i37Myo80H;@f7j8SGcriErmpLJSjs8-PtM`oU(mOD^z0Z -6l!S^uElUB6zCfeDcCHPuq?dhtX)!fy|JbUyhB0k`K<9ZPB*6H@&6CME`Wgnr1uIH_%U6gw`85jLPwUjbSHU6D3dXw&;Ne-&J>!(ej1o{t3$8Zu11`2k3cWg -1kRdHgu^z&)+80lTO=J{DS9&mg-9~pZBE5918NeImH*zXOG;^d;64~`N1FV54TYcF3}tT#xrExM&6%; -cMQnA&~KCaXYAMOh;yU9?XiurJCx}bG#S0S-k#Cds*WlgJzD>r}OsIblc;^KD3w}v29( -+&~4}3}ZsPldk<-Cb{G3h+Z@qW2=VXjfnQCEX{xmW8&!HeO7ub_Y4J-+rJ))@1k&vuT%H&00xo4&^Hc -)u*YG<5921Yg)H0txt^w%C^Gs@(d^V$0b^m(x|bMwI(MfE#0tzJ>lAc$bPYZ=l`RQD@&QeboT?o|^er -wU*a>ttfXH<7`cvlejv$~dK12j3%DiysVAH!0kdYX~RyRsCHRJD>b -$^%}9bc7AT_H^6%>T*kO`)8>>dQ@J9m6D_h_Zd5r{X6l?8&6>t?*PtHzo*?fqQd%c`C4u%~Y2P>cf8@ -Pz0oK`9kM%z7jN@-5>0DIK*NK+y!f9fO)QN-cEm8LC-dXgWcQN0w@Y-&m-bW9Br>-+?R_U84ZfH9ApY -JGs|1{3@v!GwBW4Qr%jygXHgxjbS-({h%q1*d_At!bwiR+qa>m7{9t(4oiN3K2*k$s|RiOe-|RE^1TL -wp(s_!jWHxIx~DZOe_KZ&_$p=FFwPj!VUJ%D6#<23!U$Jhd?3#h&JU(;l3+(Rt3g3||E1Ig7P}uLu8h -%Y`s~TqJGvqpgLSCG_kl|Dw?`_t*Ou1k65Z8_WCdmPOLezh~o(dWimkjDwSV0kX-BD!u~mA~u7TZf7i -#i)!xG7b9~I3VA+Cnl5v08hXxG-VC>GNBxaAicrl(gLYqx&~EmVe?i6r9kJDp)xJ8{0O`jA?NEe%Ikd=)xQh%Bx0*#>EjoJ9UUNqqR2{eL+H)bmT+D?pxIZ#H?SgW+plD6ZU -6Gi@A@_fh`IUlxSF1&+YX|zpS4J`q@(p0^8U@GXbg>gFc;vnemY)vl)KTuaDV`w|eYcUrm{<;>`7S+* -5N4tDZ*CH>^iL^S(_n^VKpuv>$@?^Q1qLN{2J!1=YEy^oe4jAx!NW-{UrV9O=@*rg3z&^$)miNw!0=8 -s%N1V>M&fk)A$QqeL{qNKnmt30TBQ5+P-u;<*9I*afbCNm+Q*Eua7<(=6PcawH345=Oy7tdvT)Pqz>g -SU`T=aE0_VuLAn#~(rbafHSKf*Ob`F)UmYcZ$epu41j&tc9J@J!rVxmDy5&i@*wJyp{|&gG6d!nYsaC -Rm#eKAVoQcqeoaPNd)64b`jG&l0N$C!gWZkg9C|D|lx!?RW9-?#KFZYF;xp6n(tUSgqc#>{GO>_Hwbr -w#VCQ|B7A={HrAny+|LqiAMaGS87*zwG0PgPMoccxrhHS_ktLMmuKKQrIV<5$Z^K?OO1kK@kN}t0+?Xuox@=z~!$mbgFftS=X*TQD-6;*gPc?`zO$q| ->Oml1E4X(Rr^EGzrw9Sg=Za3A%o3YTNYbsXa=!?;>3YFrbe#?u!yo`q54$>W|d`5f|d=GZu%Rg>jiE9 -XZ2M$mO1ZNBSYcYUwD2Jrnf=wX=oF|0Pf9Cwa=WZd!YGi8}l`v@;7V|OKq9?5aZ?_BIJbYuGlvuga^ot~J{2H_|6SCh+7SixczyGB2dtog8 -9-J4nS2F9_W58;e4>~h@x6`IP8vh -qF=+LR!6>_C}Ucz3=*`f&HmJ{)eFo+f(OhFQ1NYS?i_)umqHcN0ZE^{Tv=`6cAis{52)^*KDVe&s~q? -FK3P?J&!A;M?X6s@$LO%Wp -e@E!sMhh`FO*ww1X?bQ8vb1d(s**`syq`C8`wCWqvI4u1N$;+yYMcNWwOFc*%$*`auWKODijBoJnq`| -aA~R^t3ptEgXk%2U*D-O$c5tFvW{gMxF%BEH>WgKd$gBv_HmgoNbag*mBKYm_3- -FDBeIQI?G@8XXmd*ZyYC!RevdQWWU9S!EoeL)#j=H1?{V6uz;-|5&FZc+I=;CW@M9@?3tH_$*P_diGP -i#Lp%L6xm)Uw}W|*77|)^tj9cDta3Jg?*7Odf0|p*W4F>i>k|gv2zl5ktm%1*Fd3a(2lK~^Pn>$I -hl^<2^1(efURk>k2^ZOa>jTa`6{%7u=0=m>9^ShdRW6H?hSa&XJZ!|Ql`k;H`N5>qem9a1)E2V?K(t1U7v;KAc6cXChJ5A-3%itLrSje&p6 -%R8XX`^p5-!+WuPz_B0m?p504+eg{t->lj9Z%$G)=nB-^T{~CC7Q0D4uNaz+y0@c^EhzI}{g@wcvj%w -BJsg(fyFty_|I#@3UBAKne%)`x>Dxm7S2yhJN9 -$|G+NWuD^lfW8_;_$Mk3*f4O<#L(rN!3DIG>C!cGq6v`+`PYP16I9?G*?5)Load{{ubSxz;m88DrP?5 -{9}e{;g=2W!kF8XG|-4d=CERPb;#eU6$TeF|DX~ueWs}j{bC63EJw6T8r+M{^G{CKuuH$Dr~B -k7wI99zHP7VDz}p%DA;s_I^IIG||_4QSDvE?b$Iy^X)>JWyU@E)}Yb}jE!3t4YxlUu`-2smqV{#j<&t -8MPfJ|J4!o91^3u7bMN<2Zaqsqd(**PpkF(2zK}l~*n2}@m&IN;8|`IUlYPCbR}5jyJ@n0b0Q~gs)pH -q#b;l-6wejj%MJs5}5=33Qrlk$+O{DFmkFhpR;63k!y2goDH9g1KQ4W~YMA=rx!w)mI@#%!2d0j?4ES -U$?$m2)-Od;COK>Jx}zZd7lWVAmpV{(T#Lk}%Y6pw1aotIycwrcf^Ge;km#v~zqM;aYM+Kt7_c_3-;7 -W+s{WNEjF_KD+cKR-jr+}lqdjllm8M^#)c(=IR`-h3dqMA>%6TYs)WCDtGV{b0jLKY(3XBEa=*v{>pl -V4iNQ|L<5soTb$1mF-b$ws22k;I6M`FQGmECY0#{OdS}L9d&18Ofo)BzbIyZUzTg -0;f;q|#QB$j^JlOpdnPibXqJr6+HyqRITlWk=a8GS=Qdk^!idbo?>Ns3N4{y1IJrAOPc~tXyeV_1f^T -Fjw$y#?8B<)&F2jGqfpI!BlKC?ZD)wC0C2~L3VE#2&=c)|JgSg*4qT04bwf)Bq&R%EML_G$cYnFoE4rmOWuHdk&buik$0ctyG*>xjJ#XOciDKC9eMX -DzPlXnE|0t`;ky}lHzV@ydcL~?@2-fvE8x3Ayeo{n`zYUCk9XHc-p%H_PvYGtBk!)@yZLxGKk_bz?{3 -7q8zb+g@!cZ4TNHVh^@b6TXPo%=5?ezZe>9sX7w5x%2%Z -U%o1tSAjV%C>GP4gH3{Un~th!rw2Kh8})>Z2Y9#4L=R){uU;P0}DZ$9YSw#Fm5$|sSl=HC~V+ -w@dy6#fF1lJW7hKfbj?<-?hk%9+8(Peq5Iw0>K>t^hxer7zy&;qOg*kO+i%1g@xE(K@H1AzX={@Agtr=F7r&ey<09wTSf2Ps7L;SISXF|t?atcc*tHVWQ?cj&7JL=!5 -4YNq*K(<+CI))SYq%qw6Qt-?cYuu{?wr#aGWcy-H&zktpFTae)>}>r#w8U_(I<2bUo1_W3{cAFMMWhl -+$RJx-c`o{(Wdi`7^rw<~VcYH@Elh3o4$Xq_IYv$0PK6v`#hRpiJ=^dOWY8C#;ZpJ}gUjcB608ccE&& -I4}?ES!URJEV9PlpS!(FXlLug&pIJ@hpCkNa)aCBCh^#J8s -LzmZm?=1lTA`qZ65nYMJuqovB&C7*$;{S4}@H|njAs#n(hWhtY7rg`bmcx4tfQ^zS5*A0gY#Ql2cX6e -rq_x$zO+^dIb4BBPJ38S5B^_{#s1b?mR0p8bDt(HDoJvN~SP`2P8WZEj!y^}dU?;ibazwzyHenUHdG5 -&uFztwA1?0B}@!WiS<(?cR;J@7|<+dzLs%Yi5O{6qD;lXX$&G5$WFhvo>eiDS(I?C##0;9Chg^dj#9E -rg$n{nBI^J)Vb-(XLq!{Sv<|Yrj0yOB%R#=}?nV1~^*qb9{SghVX4#D|3h~{?V|Cz5SD8ik8Hl@tZZN -(>`fyo+IyxDR(%&ndmd_meZo|miw$WuZo4bEq%N=>?@+$;~gw*;;O$~8ZdCB;J+>^`Do>_(X{P-#!w{p9@`v|G8 -~ri+$cBDkC?VFa*WLmAN7*FGvoK9u!>D)>PbfBvx)JWG4i_0JTWRq+KcVXsk|3s_-5be__l7tzft;cX -Q=s_=faMAGdonwkPLrqhSK3N4(B)0Z@xfVo~;(wex4ak7FX+q-zvRGtn3|i&jYzLa?e9Ji_}EgsGjOe -?#wp%S-$t+J@p=A;}VUYBkk-N!;W`aUqJhJGyVhpG{4N8ESXY2u-j7d68&-k(|n$JgJ-XMu=%3Yag6eZj-@B#`85$I7>D2%jQcM1%l>-D>5p -UVzZjLHWwgKL{UR+fG+ungNd^utQS@Y6yu47n=bJ7@az2c*yNIS|{MK>`_`{f**tfGYM7oqe(ryTOHQ -UomuHN6yGQT_((fN$Fe>3BwMq-JsjMv^}s=Z3I2cA3NJ7wZlBwi@wuln7zBb+SiWsb>@tNiSqN+ln9Z -dLzF8-8UAdcSQ;u219$pCkJJWkT8)N;vlCO8Y|jjBnBLo~4b?f{2X|bHCOY_CNc=kr>V+?P{W1Xj*sN -I8-C~mq&b%z4XJGiuRaGar**M+&M$kUq$(rF?=EG@_xNDUC5jOo!KhKD)R?)UL$1Qg3eE>JnAjCh{f{ -mT-|4QAH&$rWBqeb_MyR%Tvj`T5u<#(yvNT4ikPMB3=O>j>XG$(K5$-ih}-O`H%R*_N1DC09RJq)&#(7o`hezgV8&1YN)%>=+7p{EfIOitGF~AbM)VVud(QkTqC4Q7jj#; -OYcX=U%9!|uUP8XRL`-~L%TDJ$>5FQw<6p`SW>h@NIQmCv_mCj-W!;~p&6D`BavFI%m(#wc%(>N}ayA -yO(OiFn++%zD+lRE8Zwbp-e2b=UW8+4RAHTU1GEa3(eghL;@$wruETZu4ccmS{{kHzdnP=EjRHP`|ulr-;n>PMS* ->2zWud(?M{&Ye4J-lB&1g`nFqUzIbka-YBt-0lct@-=qLD&x3LY(+gRDYTO8lC^(QgZcE+vw|LIY8N$ -xzN`~XZ9e_lu*O3g6>PwBfeQv~{sYCh*~AHrY-SO%~|(D(5xcfnK(t|?1t -*0RO%lv&gwl^w+_sh*x?QBW{!(|+N15t5cTDtkwMVdU7Dy*(OmnVO;S0KA874jNx`Q_A>n`-(S^y+l-81MY19uLJ<2$3nv9=w -eUi#yQI!GujJ-))g&K=q%=u*Q8z|~!uHikogLRXbTSwG~|C-S4MO%!sa%b+iG*yIQQteENK@v-s?Qd9?>LMYB1M-)AYVJ}Zg`)>(?l-!6 -S4+{UtHj4k@N;HqwIsF1d~cP9zoVer_NR8cA0k@^ea8-7Fi~pN=g -Ld$)vqgO~V`O3P1?P$ev+d}1=?f!y7Ngzi$!HgCweXjB16U`vnE^aGi+w@3P)^+n91T8nWB2hZ^?;7E -hPLIMnmEOL4h{o{_7T5oP8U7LIIMf}S_gqkwZNmN*XSFUYAJ(*pwTUw+ZjartzAwW6>oM*Dv^(f^c2u9G4gTeGxaP@mXltKS1PI6Ag89l$vLr^=l&p -uCyAia=*Y%rA>zd-(8E-_LYjc|pKJpu-e9pPreZ25Hsn-a(3< -UM`S~&MB=0*F@QjA#ljO-X=YA_-I6@Qs%@wcBoa$p96X|P1sJ%QX`w+?;PShC6o0CSG-2*z`Wv?6xz8?YwaZ!+Nhn_s`bJ~_U;Hj;U&m8JB0J#_m0n)+);UKcX7+bNZdfIb>+>&$@9 -wkd54f&IA56g-!r{M|KMCRy}xI!M4QNGThgAPe|NFhHdMG8XE<|FG44?}{eR8x=);?=%F|Dj?<7t&C; -6sls54vo!X=6P%0wC8wZ7@;`K$jMygzqIDVsF#nNx|IQwGn|E)4Sc*(>x_GA`gGv1!+g>2*0cy -N9qIZYR!Q@bA04qMp8pn<*!v3}Zfd3Y6crdm?y4p4G2f^1^77_|7~1+!(*xKH#N-nJ)99&XM@Qn3CjO -GQHD=!)?5u=KKiLg@DIC%TVDN>@VgXlQGsE7)P?qvolif{)@^EF8bZec$XOSu1M;$MTj{GgHH;do+16 -98Q=0CX^tANqOC^{nei@FUTWGrj+xI~5nzrN+9@6fj^)|)0R2xFqAl*}V6q;lwlZd=9%!OpyIa)Lhrc -iiuk&7|UROQ;2Vj?)eJ+`d4P%S&Dfu$2Dz -%UbiM1aSAQoqgIW4y}ajS?%7`|8+vaK%5{;y1paNpUyY?HVCV>*kJS-)aSqeYC3lJ^y{umpQ@{BvoayLu|D0Mouaf0=7ava_D!j`rCO29#YDr_bE|i0R{sQ)Ey7 -Z9@*tLw>QX_|GjU~fTlBU;RPkBx1_(f%IslM3Lkaj2as=3YTnL6xTtyDhKf3w|)**{lvdEX{|s-jP-& -EZK*kv!N>jJV?*Z=++(Jnri`q6nWCg}l(=&GC)AwtsAo`)6t)ju%LdK_obc -`&=c_F9&Jh;{hl~K%BGRCCd*OEkCC&6jACA9De&?pmwz5%1co@)7T{s5sTTEEX9bVXe{LCyhC#NdL=i5=R(&%qHU^s2s{=#(L -%&ebHy%J_-BqS6Uu@P&bJpdDhXVojC`3v{m!UlEvgsi>;P;LE7+>L%``oMHf6dy1ZMEb(T$$b=;a>C~ -45bMS%fpuGgz&I%-AHJZbBgE&`Pr&eKdiP|LY%Npot@Z_Nar-IL}J?q102wG%j&szSyQv}=>+FoxhJQ -7!A)wPaam6Zksyf8rd@X_J_4{GTAd$IJT>59cOqSNR=?caeLt2tQvFMN>&nM|8TNsidJcdsD^ie(6_R -yA1q$c|wuNZ$9v|SJeG7V||nNNR?{@xTRX;+PYp*`#)3Zkt)J7MT4S2E^jq>3z@GGbV%60xSDY)44*V -F^I6K=T{w>-d)(!1RyL83>cf$}7r76P=a072ymw=~b9t_ZaIW{sIHZZ-zY@IU)h+{Xa2&s9OR!F}^lr -+4i|jh@SFWp`CAw?vBETzbnqlSj#d86=~PgF=e0U{{~<6j^tmPDvp0z4} ->8@^x`|e^>4f}FIdjJcPqShO{51G=z)PRQMQ3hI8&26;|-e4D|y8nhvBC;Me>mE*4>czn)6e6yz>e(C -m7ahV4faG0S(AasTI`_*sQ|@%lB-eAbWUE%Ib6lSn&<-U%0c6YWr -6xS4qeY)UT@T)w+FL(87SvxMheqVf=lpFV6I=@#=70r)QY4qk6~n4GhYd+7{3kEBNLF=0=dQAsJuTH4OeXPTZa?0iKm -Arc5t!aK!(8#j5FA+DP8%OUVbL^V^NkcPWQ_Efw^1k~qbfD?HZ*4F0Pdzj@6q*~~>-V-XFKo{rcp#G| -JsFAg4C<{S|+KQr^r71~4(?e~9_D0&*OM(y}}7wz-xtcIM!djxq_C#nB=?;vxRIUsv#qJD<8y`8z`q& -$r=U!Q8+i{KmqZRnBbu{O1?o4i4iIJFRK?f_qI6CD*hAV1mi7I}}mB&}@-CN)(!ZqA&$;To~ZB7LbvL -yJTBHaU1F?`mcFo8JxZID>VeZ@_8BQXDSbXgBPCz@s-X_Zr{d{_?1t$`42#(Ku^Q-|)NWM~lA?I`6HP -ACJtN=k>y=~LMYta5O^u?SuyvHx8nsO)gmKy# -a;{Tt|xRbQb%eXv~RXl@|k59UjXB^>~%UA=$p85g6coEA?Q!%_sa-DZlP6`h1!g-?AF;{ghzhB-|@ut -<`&voNG0iU$KW_nT1x09B9KT#aG19EM(rcw7+%)DM4BY8FcI>*3veEV-BAGnvgwFfabsk3+yeEs8{*; -k;wlq9i<@n_BV|1IwXMP&`(bu(?EwcS$wqW>{-XYB_6PCLZvca|1`r%ul>Y?*kU$9y$}g=}k&VT1A*c -y4^Bt%F^bNszLHS%!R~Sw=VPBjWqbkM;YaEX%w#e|aeq7j5lJN^buY*HRNbfLA@`g0kJ6*^EtJvO8}O -nQ{Y&cZd@jCaVox+B>7l#P#$8D=52uW)37pIq3CbqghB955qG*1CSj_$?3>tY9Jkt@-w4WL|F3JGcZtNw(mb#TfO`i8fo@0O+9T5F3x08;7!m2OB0oj%$C4h`GVd4baw6*$5ifBTLO5qHYsxi7#lveZYuWz=|`f9WBwNA)UrlDWM^K)RW&=yo1Om83 -EEtK@`Y;qbM4q4i=B^5=)f6$D787YmGx*_S3O*H$hleMNuL(Xxty{E=(OrLbxgmF;O$y6Mdc4PXL+ZV -T6f%<;vJN6v-FMZy9~eApnpD3#P4BkVjcNy#!Fj@^1M^`OJ1Auy{P2fWU5=_2LU_gKfGpD?arW0Js04 -aV`4n7r_*+S(U!5OTKFENf;sYCOg1U!BvlAVV|N?jLoOnqCyFacxd7@0F@FUGboLBX{bV>@mYA-0){HeX8?ffr=l3_ -awZ^-}vI7{g)kceyz)wBMxJ{8q->GRELE#*nI|N?YQ$QD=d%MsL}lY(L(p|F-uJ){euPW6+LqQO^=wS -M}Ro&DLt4W$kzi--C_JHDeoOyq<^e)erp!#D2M -+US*!Gcb$aP%VxfJRF>cSQyq)~M$cRa_K-yl2z##TE@vO$!3ykH%_lz&Z{T++bJPXIZqutN=_b_iiM1 -Pg54#<0-WHEeB-o;q`=U%pUGgrjB;8h-TicP_()JfsoNfYUwHR?_;yiVm(Ww}Era~}VnPS!)+IGv?aUmL7A&I(2Vjm7gio(9Mi_<^mnLx3C`97U1E13XxcW*qr!I>^p1Mt66O;F-&_5h -$|olG679QU@`*9$Iqw*%FGli-RcoF(FDEH|7Iie#1Id_@sfG<`nyBx$$e5DbEIE|pco#O+f8qdW=UP! -feIRJPTYlFxA9(@!+z&XL&wIp3y~C)dH;Dr3(cZD5-Ua;u+8;1an9#oVbI0(?Lj&NssMA@Rr@u2`6~! -r*zJr9l?OFYS0qghMA?LJmj-R0ot=5vgC{LtUYBuKsfZ3$o|8coZY)il6V%4D#&Z*FLd)^lTXS-$9<+ -&PokOt44XRYhAJ3(8{CaZp^xi -Mg7u`?lJhE%+nvTWZES0}M;a3p_n(@p}BFu1Q>SCGdjlounm~qtAo6Ie%_X1nv%?jcUf2ysdgCZF=5> -%>J5nme&qCSeCJ{e7$BbcYqcyWeg?j`Y(2CPG`xpT762hNV6pY$3V|`elY)EA-)}cPOHa$to#kyd?ra -O?gU+?uX+piwO!`Ybk?n9yeIVgx!aT-AMH^}jQ2sjx80^_B6VFhEy1x*OLRD9{W&`~=P%idKz9dGruM -e0cJi!Y`^3$`ay>vB)zD3Pfb~vbZ*p!Wx8?3sF)CB*7-yB=&jHRn+vU9;o_~Vh^HVK>A&({S3VG}20J -rD!1uS%?mpmZmw~VSw$b>y920P`xXOc>oEs&Jd6>)53nmCuR{5mIXa`@w#a}F$78~iFsU}+$@!Nxw7Ar}yq)wqtS$prj%HMALMBl@|g4~-f(q;U;oZjpf(3azPe;#Mv!rSt963;Aa?mZM7v<9{ -&i=Ms}Hs8{ZCZB?=T-lFuPyQm@R>(UQ%(*4)r`g0O;#b+FV3PR%GECNdFqm`!CQso1wZ$qfay4M_mqX -#UY@CVf4$+oqKK9uBK-tY=@{e+c=5^iM)sc;}`L;&ulE4&^&)gZk*lQ&n;7gl-n$4W-^pDEh^9kQvyR -FrP@k)nYeAfiMcOjR^393KZm%R4n7C-^I(tj -KL0rCEW{q-yDXIDx_=*iaQ;r4%4kwBFE{y5C)$V0ESEGJt25`h;fs6#)AdXNjB7S`A7u3NDw+Z8 -knD;jhMPirdMZtM)C@g*K`%*>a%c+q$MdsrgcRtThrpEu_ClUie;1-_;(Ld+4{JIV8@_#%N&bJ%?f8@ -HhZ1VTV(${AbZ%Chhku37Rdh0iKCP4-{4BlxE@OScmitbv(CSy)!5#I4Nrlpx$S0{o2lcyq|sR*LS{=AkxAjv94v!uR83d*S5BNB5A4@JfIzW;`me%% -DeUIoo~<61LO~bPuvjDhQpQDT2w!MDXO0>QT=qua*2WC_`mG7ukU1=3-SJ1@~x=PcCNh`E|B#>|7bUP -SWBqeG8C@lUN`som^DOuPd_+%4S)X2aNF>~$QtfE7+FKokpbFwI&~S(y4Y$f54|2PIGYT)5c|e{*X2v -Rsb6$Hoa89qZ3k{Ez1dt;hte__3rFPk~ls8noh1qi98DU4N3=k1vrA0j)TP_UFXVitmo16`3E3R`h=Rzd*^`pLn!l5dYVJR#4t0tZMO%vgbL2UQ9IT#dKSQUNnPVa1Hm~8$~P3^+S -8RwvJxEjiCSU9EhynBX~XsoFSf&cLEPNLMzT&Y~@*RgbQpVbmCTLd9tIt^+&6p?W|DX_?`j^{$eKR%R$KMUNodLb`h7{_Azph(+< --6E|TW8=7j9zArNwyudH&?AQCQ8uIw;r<6YvmF+PGdNS`+&fTG>K&xMA@91D_Iz!sw0kbV?`Gi2TYwp -4{K=oQji$5DI>fE<>Fk8a94+J18I#UlY8^_W*5U6L!fhN&s@7C7XcgZ7w7AL-V-LTP)tUVU=3Bkyv5q -k0g%tF?VU2Z38F0>?`Wa_kqP@I!k4P)KOt@O{o;)V?%WsIzZ1R}Zi>@xB56dp_mHpUjT^84Vw&$58PU -T@8bN95gyEK=CN&opU^EVe$Z%q1eI=M4DxK39&WtIzHW3q~GA6|Eruj|1}+w;Y~J24=&JwxylJO4T4&DqmrdzQE&#_X&AhTtZ2@dSN6nlsKgHO8gtpH= -ui8tLc2=&zwSdn%&nx{n`*`Ger4N52>fXd*6PuE4@$3QrIO5aZCv(EN{L3-kNG{9lJEw!@R$rqtmt2K -gbCyHK^~qeyGngMh)O)c`wdo@8S?qD<(!_YZU1!vNFLP(@vbgJl3wASxRq+h&1siBL=*q%~Z{oNylZ< ->IC!LzzSKY4{w+vr9Vh3DsK-o$3zZZ_mdpSBs=ybzoM*7Jt5Wcl%RJ -iKk>!(y`fmU}|^&w{Ubv`O7(Z?i2+8j0m)=B1T!_hpP9#yhjB_;SnD9f^#gPo2cfQkgrBV~+TK?j03B -C^l|`nQPVmeDqp+q-|r)^AVfk@!&>#_D>V`Q8{aQKa~D_rh%s8x9M`>3SCs+80RJ4T>dzccQ^ -7bfOn{ye(88y5%@2P1OGjLeSi2fCvrUa|MssR9{%P3AHqK~2L7Kn;r~goZ?nV*0(>b -%M+%KK*jIcoyWuSD-77-O@{cb(z?*cO#ngK@6-Ugg2g^cQSVIplf(d)kwZ&V%jpzJ@YKLGKuIzTZ08O -M935UI@4GOl-Bd>Z;dxTz0_Mn+hI)`g-mEVeef4qAK40@i_~~0$vcjAYNE+i74JRyx}5VKoJp8u}oNC -k=13v-9^E)&~Bt%G_A}^vHJEUyHROj-!jZDc&QYvOieMVFs-nxuu^`{GxN;Z-6L4(2s_c4!<{2+-!?tk2)0OT -%SPyLzkP%I{$=+*_E_>??O`^5oA{>f66BQ=_pH{5Z<>}L6KgDw(jI2GmT)8OVTS8XQvL8u13X{7!**3 -CCUnC63XRYYcIn(P0{7dNI>&q~zRw*6?~>KGdFuRE`VJv|e~$Isb1m`M_4izRO()J^5PPhVKBRAV(5D -RY!RTeY>{)Nz_hiF;dAdftc|`OVP%ic}hAW3ZwGn)_x=yD!%w#ty|@oVn{~zAs8i4ZiO)wz?+%V!K^AHMpmv*th+`8oqY>GH?GC+S^JA -^=RAiEOcB4BY2%W&s4WVe-C6~`^C>o2U)z$*P&fJ_c6R)`*mv>wqNaV|3SX5(`B&T@9^HR2Xy-LBiDB -WZ+yi4tzY&R`Ly(E(~f!Yo1kYGagICO8}b***dJiu5KE`Wf9b7X|I3f;Ih>Eb5!)uO&;x#cpKa#n4fJ -FeI#R`+N#dJLdA5eZYr7%O|@rJ7L; -y3FTATB&dB;Bh6k86<Zn+Hx^0DgnPg@<>C-HL|4M>0Uzb1h1C$zE)O;W@Y^y8~hA< -{KEYPpU3ro!j>KQx4GA|`$u4FTw^3{AO1J|!^`Zt-=Gfe)O9+5`)lL5Il1ZDFdfw}FW@&*pqH0Urm5q -0++z#VWYu+SF-^FK*T$d?o3Whjhd#<50Y<*>G47Z98^qlNamOq4jJ!n~e`g+l)zzjEcvc?QnTHAGXH; -LSukc?B{xKduuiyjc=Ng}@*M%Oy_mBe+Cf%zu|F&`&)Cb%b<5qYczj-%n#QJca_Zvdaqpmtg=kH*%(V -#QBVVEMfFt53VL7ADj=PSfN5cfoa@Mw#jz1ZRNbJ=s?|L`C1PC4MKrZ?V%K0^_d@7_&v-+0kx!F&Hsc -&FJJBld1e#`NO4$UX4O0(V^Fh-)=_DdPUugRXYrm4mJ^1+4#|)An=F9)Abz2t3OU+>Y;pxYojQJ}MXT -r5yVJm{*WL_$`;}&V;)K;+%$ScYr?);=Ud0&L2%XQO-90y^XRq4C)f|FG)U&>&!u)nuD(X_x*5hg!_| -j;|~+Jial0wyc)mr^jm4%FkuzDI1$IY%aKVNfm2V38FtLuEZjn9aE{=~Qs?z -@TaE$aSVd`~xAceu&T?kNYwnL<6le@au+4n7tG`k%pXFW@<7eyQov3&F$-UE#jO!**~7_(Ld(jibU}BL)1V1 -UhN-4{rAT6#)B6Jd$abzWg9}>`@L}vBe^L@wjpI*Q3cr-xUckHP4#oh$ns{B3QUp)8l#eP_q2wW#LxJLBFX8W-bqjgE6Mge_C*J<%VJxi -U`OkYkLK8(T_`m&LDeqQ;%u`hc6mgf7S&wqOTzUUDzw!>4mj+%a^(C@dyqF*Zab`bs6U^;KQGk#k%Qr -hN$I{Kc&3)doFyZ3L@A-?Z^i+#}{heThL_dOv!6SrKyPlNVFo8rD`?(yL~493?DDs(LnWzu=QT5UKbJwS9Gy3p-s5)&w^(EimdZe -Du@%pVlGu(K&cDifdhhNUdjJ^bJI3|GaNfP7QK=Hj%hwZCALEF%o^$>RDCvxw$1J|5OBJ&x);rBb>dU -m|GU+nXb?-uyYc`lxL-DdJ-KYn(#PL78rqfXa+FVM_>qZ>g!+PnN{)1$a=z6`&Rc$BpbVHlCUCuG^aaSGNiT*l*zt7%pdKAZDQTCbd#D0Ak4#SjR#l68rIP8PrH}=>b -eX`$k58CF=Yf;bW=*^0up~5rGMtqeb&WXh`T>);mpI&WmV==a4IPUjX=j2=x`v=uvS-`ew@By(mCblz -jY&-(o@H@W^I(DGS58iX2Pg8<>ZRyy>8LjZlN}TgqU32nd@k~LxK2GVu=e)w!C8N9x?=?Modn@BbKX^ -aHv4vjSHiS~2IpXCX=sRjC*CD`nylXgs?ifcN?nw*b(68?44WZAFU)=w7*?#)2w4?Y&Z{>dREg#x(`F -`9x5cd{>_8N4<{kHJ?#r^Mzy;D(NETg!#>qt}6>!J<{eZJpM-;?Ta?6G6S)NlFxaf5!FPW_I|aJ6Zq$ -t^x!4mIB>&fG8dFPuxdh&Vd)IF9a@b)cp3JCR3KPl&!qE_LW^qoHT!F{KrZk!3vn)3?Vbvw4DzFRlX!f|51u0dgn7hn5~Yjd% -`o7<*aP}nNrW|ixY+jkEN+vBz5j4!;Sfm=Nj{Tk?(O%I2DB(Ap{pal4T@6|d2*D)uwH;!)8ouy32{m? -Jdo;7}KN!epD%lG==dVto8S99un{+!|d%^xbt0i09)0NzQz^=9jqPU#BnRek?v{;d+;@ALdt37=y(KjG>pYxh5|^->~YidqN3JAwZ>9SgxRZY=k>m$p1> -bTrmw=#%W*=W74%+$Y{yd_`NeV+i@dx*mz`0i%72X$+X7VaY9t(a?UpD@P>6;Ax?*#W)^nbOf2KT1;;Y!mR)d4IGzx~(;6yvLgX -GdV5tw;O7j2_MaHosd`L5~8UPr`3E{2m&xq(`Y2=(sZDp$%Q_J;o{vx2zv~YmedxM~~(HD|#I9XY->1 -ihFdNnHshO?q5$%3+o8@^NDF;jo%f9g+ln`fR#Opr{5AbZ{dQl4EW(W8G2o9&1vW#SMJakM!k3qVeEjn5QRQ-|d$S^&dAE1N|YaeLsQN1(o$jVl1zE#l&+y-=mlG$V5?Ikt!V6* -VZgx3N=&c4geBLkFn(=Aup&BE`PqQFlZML8VD+bCsyp})OHP;uU$adggLhcSgcl6*kcoLOPBfi8jHW1 -)^7PM}?H2lz?68gy=E#(}VZVB_`A+|3Js!=O7xr63VUN)CIbms-)~s8?k}<8*7liRWuW|nZx -aKspuR^-IQO6pZZmAwoGH${*sk7nQIbr0(OTuq_t~?s~MF&ny*gET?Z}0fb2}eJ8^RdSsyLSTL{D2+u -60~H(sReg)_n4_)jGYMn=FS#!fLS|n&^Hst6_5OJ^zXK4<{iS%7SK-``>B8JhU9jN|Frs*ZBu-ePC@l -P{&xH}Fic6}dtZbp8DbA^rC^C)MoIDy!S{w@`_a}QzRzoG6y+G#Z#5|)c0Z`+-zlAg&UxJ%iffehxK; -$^dX1@|2e5v -YGS=>LP4KH{_zX7_`5_A5Qqc=21414?%_-kB8dM@lzvdyvO5jpC?Ysq={Apu&QT?OR94`snT5DM;@fL -U!>cyXeU-t(W+Pl+}D4d@pZTee`AgTUejKGJe)FyhGKrVPQdSS&yK0tb33z>lW0Cbqzw>MXQW`;5-v!$ECCfiZZ0JaM7+ReDuCZGiRHQuN_E2Y9hGy%Mm-pLI*|3oNOb&gI>^8Ff -RQ!G>J7I99^-23u}N-PQoMaKCGWZi-7c)TuwkpV8FRgz>|(Ydd~-iT%iG=nsZ5*ypGywLDl)E4x*Vowuh5?0huEI>>v^AszO|R{`PsPs5$-d2|FE3*e>1LnU*_Lr)42ui?J#~j#IAts* -6fvXCi~N_^Ubg6pSqoEAl^B2ywAQ2@S8*)GN$I&Bwwriruku-~=8o8DL4`eV2+{vO-|{%T9Q -O!(~Inr^8d>QM_e5bp-p>ZA7anmIpBzrs7kAL>(_(tTJM>ujH{l!oRiJ>b5ReG#5pri8(LM{;Ly-+|o -QzA*mxT;udgv#z2=}(M)~ -j8_E_l|T^BNctp)mh)rL}*eg6$MrY=C}ZHQ3BQ5B2Lj?6j&!C%BjTbsTnD)w=`SGjeYa_iea)wND7y? -b`N8*v+5p=1+F>8-_MJ?W4D!16!fIdd1J*2zu$EJ%IG__F!$3tN$0~rT1!X{Tse-y%xlR@Ez<^7BXrF -GmgS<^q+x#jbnMYApG9^Qx*vBaifgjzJ=I?fcqxy9v0-w-Syy}HmZ;rMlon7)^lEupq+q -mUm>O|skx84bq&JtOgVnyzIN%#t^e{I7wye1*Y{&rQ0FHf6Z?}F>seZD0J{=y%;?cb=U3wx4+9&}Uhx -rPoTybqCd5~uTet=HKosYMb&6>4Wmnz;8`VNT?;U@}ZH;3{{QQWqAh$gmUVjJM^>uHyz7G0;XT3T{h& -?=UEr7DVKlEYQ+hQCj9QRCx-^IPgJk+7#Xv5@_gO>g*78DLCEv!9|3LAaFl`PmY0zo;N4fG32a$LHWO~$$t}>$tp -~Yc`W3gtk!IfR?Y3p?(ehu@ -~~{p$P}-&YRm8$TC?`}bM&s=u&74dQU0MX%wzDE3j~`93x{whsP&$>nk%f&Ny~EaTf1I`%fMeVWzZc; -yuxw4KeIZ42#q)r0VZcloOI@OuD$>)@xJSK=-9Ce-W2`8D-$|Ni@8&Jbn43-{qPf9O8mDWA8vpL_)M4 -SD-S&(^k3Zq9l2jEGd)uIazRctnps#c$2?(3kXs_S>TP?e~Ll_>L^!5BygeH%wR9_5p74WIU?;amHP^ -rry6Qd~JMxV{t+oT&L^57S9uccYzW92DUyP+Lj5%{^xPcHqQgxCmiru{;U<>6|ge4e%dy+{%LWRR833 -gkdA0-swmO1^|+sN2F;Vbd?;&y2;-#jh1}FMM -7J(zU%*qziP$?>=!(H*To07{?vLBK1q;zAo<_#+8*rHngs=1Sn<#zUC50EO4WFM>+UJ7S;<4CeXBRO-)u-6=5I)hkJcx`yTErir -V-!1>NxgQ;-W(m{ouYop2Mt*x-lH`D5Fi>TIfG_Xx*^#p>>M!N}ON!6gGY)kRf;21v-IL)r0KyK2urq)r06him1hye<6t*M0=l;$3tx5#wIvr%)> -sylCUkR9wW2AA4GAq1iHSq(~e07g^AH<7!S_^UWaek_&SZ)+3KlOqC`DMrc?<_mn|A}R1mRP^f*CHgs -y&t6!%g;HlkQ;|XxsqwcvV!@6dC~rX;WjKc7$5362;{d>{J$FrWeC&0U6XdGpY5%9wj$ZR+O1p+__HZ -B64$3fUCwAzx)%4cZv%FD1?&qIHpdPOz&H~$E_&Bf~rVG;q_SoUqOx|I>Ss>pAfZY};Rio2a^<12xY~76IB; -CL+;`*p}(e53LQDI6?zJ~$sslB?XX{R1&I%5}|LYKCI%yAc@w56jE3v5>~+4!Pm4_j~ds~Ug7D`0ybq}8^Ht?&52DBBRoKhSyojc`9r -Q4Xg94T0aT9&aSB>rt6FG^`?V7}x>R9`40f*~PuBz@O5 -5n#z1!c{>u*Keob_i*Z4{-2w7S$>tScf9%Iy@M^QY0q${5a-ED1_!po~t^;>3?#FL?V1OY}ugNdHXMS -xcfjBY$Wxa6s8?Nzd0lx-FC+aW_o+XQWvh{e@Xl_SWe&TwhHMqwb&~$}eto170?0P;nUai2lk4Ccee7*#$C^2`M -`bi6|rxAE`8UA-?X(>_swtZxoMcPPfwDTwP}W7hdHHbevWZJx=sjqTcC(b|M-5|KOkC1^iQH~GX)JLdIQlhMB|7qAX-3lHPJ_imJ_Wc`T@}zqNj+q$`Uk)sDbDpqESS -r5uH!eO7u3OYlvFbm*@(j4-u7DBIVZtqRtFYe*Fk$RNQd&AWt -l>#z^5_L43m$LF>0FoCZ!*nhh7eOZY2$QSj5IHZNa$1uoZ#_=uK_^9)yTT*%WCE+OnoxKzX6MA(mTIb -naol^R@4xHaKpn(%dmTM=&1@YxAZ9C*T0zmOB2@r4r(ARMI$Kk^pGH`QnNL{eeuf5TY -y9VG_*M-r(BPsIp7y>|!!OmyzfZ$or@omAtgD+_8)u6$bH2xbk_)iUHCpG0=gAFG=%V+pW&+-yQxC7xN4L^f -$5aA-i-3V_Y+?{ZxhJQ@MZ`ANZz7)7OVI$#C89!kI;Znk3gv$xvK=_a*d;{Tj(q5-L?d?t2N80a{r+> -pKPyHsH@@#*SPI=lpgK%5I1*bgqS$E1a{S}1!N_!FRBlSJ)$qy$yfN$?u9m!)w0lL&gNKhF(C${8nHX2($ -ujB7i>tev$AS48Oh@4g!q>0XM+?3ivI6U&hiLrzz8F?nnA(EX~wlG-5xN;VdpN=jTXgmSN2YhQZ-9IW -1X+g@q=2Hs@uaS!@ek{+42XgH^!25HqNsbm!S6%mo3oJ~Fb+Ii^CZ(~y;KlMF+aHQ!Na=elKJSukBEJ -jr(tZgC2m_w$IqS+>d->bE2X{aNhJLa;=(#Zh22WtsELHm4!aR3Ou7&NcjnDq4Tds^rGMu1XnM`31!- -84cqp%yt^=W@n+@#-&lT&ZKgb^8Wx+yX@Qnwx!G>W0dF#$l5cXFi!4re{oH -)Hm*eY3cTIV{zVGJj_!sg$>bo=7ZZ>7RReQBPGxX|Z@W5I5>852S3pJyv`?&GZqP(O+yJDDRc4XNt1# -Su&CSp2bttN-VjSJQ?gkQstvD;0>6p)iZy>}-IZqiH0LJ!Qrhqm>onoK>F3i7Wq*@;xl#AahyA?^b<#6ezrc&IdmOS4Ap6dG -=DNJd-OCNjaK0KneA%^^PD9=0H|O8|LVwb>t(w8jQSIeR;e4CH&C%xcYe`=%)`r)!=CAA7`|r5_@Ada -CS!S4qYTe!Y!P?8cKFhkJ4KK0I&E0+M#J#Sy)E?+tpzh(_FKvC)Yy7(Fr%jtXc5feKJ=ezV%bK^3dwI -NGJE7IR!dZw6oQx?a1XZ-%mFs4w$uALNgd9Xe0EQ-kF=g{)_CTqch>SQg7?c4mgZK`fiwfU($o` -()_9CfW0|%nk=*mAgI1yF=d5jMINBu)bc{2??kO7YCu*ql -lwUfq`SJcL&3r<=M(@&hJmoOLhjx3qh!pcYgo*a54HNAH+SZqBt|#h8)Sq|(ZoEzu7HK< -@8%Sto`7PW*Y^VvHoj*HuRG%J^R7XBp92qJ|#?Eg*V`sNqLJGl-TEttDz`AU@FwqJ -bC5ooEHoYNEA78;C~zB&Z22W?_-M{zS56aBl-DYf>cCBNKDNof9~D+{MaUo@K(6X0$*0(Eaz#lfQ!vT-q8OPdHIk|2mEj4*7DSN$lsP<02E^|1K$C1a=E(+^1;)7 -!~Ui`lws$t3q8UQ`P<=(!6ggy`1hp;!#lWZ4!B)w^}h-)TCa|M$qK{WYU*Mx%Le~jJ=`%S`K)f)e&|{NZu#IBC14H}^03#oNWwj9uCWx?B*0cdOW#^DdaxI>WHyg^Rr}n+}x+w&1QEz)$E*OvKE@P{!-lCeaSsu9 -hQr=!(Eoinqtm!TJmiWH(p-kFClh@sX*1r%^ios%!#Hvb83Ds@a}r -v6)c#46`#gpHCoU!5?&&*ld={rhtW_D18zSWusUEn+!OS#lus3CYuE4e=ox|f8pxiwY+Qp;vn~~_jhM -Af1W;8x&0}B^Upo}YPUb#-~5wd4gcNd|8KuaR;|9{&b#iur}W-6Ywx@Nfd|*Ee`v$ThaY)#(_@<-f8x -n4Tg$dR_4G6Uc=n&=&uxGHg%@9Xx#E>qU)!PtJt~N16=jH9w%2)5_A -JDo@+jfENgMvGB?9@4=OK8__-Ft)?diLtwr|%8@!uv-I7#KNd@Q|UyhL0FI>c*(iW5$jfA3ecn%FN0( -=Pb;%ELv>Mv*i~ou{)fFOP3WDFTZ*IEep~!TIS!nV&!eO|97YVzdQc_u>E5u#!i|%WolgfwCM>m5@#m -ON=`|gJ!fv(yqkpmTju|du>b#v7Ep2h{oTu>1yo#ry1)77Y60ET{ddx<{nIy-<{&PAh>APFpL9hOethB-X0he4#F#{8Gue=ar@()5ND -SP<9q^yT$d6B)q>h*&-(t(=*gRm!5YOH-=+h+P$tIdh)Jn8~sFP?B(G^7RB)X31CZgp;cMz>2dXVTLq -DP4yBYKi(Ezvrn^+YcaZ6JDyXk(KccM|JD^+Y=m?M76}??ZSX(Gf(AM3ac75zQs)Bw9qYgy=e=Ig`yW+2K9J4z!p}gZQTvT3JltLY87KU^BCvEHQs6n` -9QRR1l`nWG}{Wc76lb44gS<_%36&;Cmy;;4e=ghx0*Jp@Sy$L8jB3mua?>o*+LT5yau{jPa>BW(L@Kh -H#%TV{mr%;9{9fmJQcH!wk3#cvDHz8MqcVQvNp}GipHV-$Z%C~D=Wqd9-5ik1m^fwKVjr%vVOs#v>)LRX+OeUr2PnoN_!FRD(%&g+6QS5!ri65gnLN)5Dt_26E;Zu5bi1ML%5f;58>X -@K7{+o^b_tY(@*#YnSR3kWcmsBm+2=QA=6KIfJ{H(finG_sC^(DLU=IYaKgg~M-d)DIF9g0!fAv@5zZ -xiBjF;#QG`ngk0!i{@EF47gvS!DBpgk+n(zd|#|Rq<*AtE*+(`l0curJ|K!hVD|5%wosPPjGUO2VOptEIk#Yo$Jf8>Bw6gBD8q5Dp~ -lOV~izkMIz}{)CN$TN6$q97;HY@L0kHgca)GmB{!Buaof;E|c*Su8{E)u9EQ+J|yEOTr1-z+$iIx4x_ -#+$tN5_*q?AX;nsws2!|4mBRrOH24RIdlm${g;SwpI@H#1vaG8`xxKhe92wW}Y5k4m65w4fv2{+2{)W -Otuqws`72>TNbCp?yL6k&zF+=`Ro38%^Mgma~T>R=X0|Ab4Wf5Mxje;NQRm;MP?O8e(9fZz -4TAGQTnG2s=hn%35O6?#!>nupKz4q8!7#gPdH6t>X7D297pMwIDyhHF?Ap}5%yj|=_edYxRS7m-ib5$ -C;;PYW$=>krE&D;nnmxl*~HJLo~D`J?9F^sf$?z-ymFX@sF0sa{>3N?1FnOpc+X!%a`F8aFB{3V5iTH -HK>qFIZYO!U6pw@6ksWLa(WRU^X+*+F{tAg&ST?2KO#WqhL_AgsZzY^VdM>5(<U*Y@^AlA2s-^8WhSfy+i3O&kA9@l$@nl2|FC%Z{aM-j -J+3oqy4T^Q??J6_E7oT{cvtX`X<=36e;J6_d0kEe5nnob9|o2y)ibw)E(J1*w>PFKtMVxI2lYQ1pq)n -(Jw_zSrmW?y3mCy&=velmHwU3wLey(M1A)9u1e4c|%i!!>?r)1=d?!FCVq(8$Z<;b*8gi>Grclqji>) -gvE8JK~Zc+7m2s!g8WrL5ZS-Eg{QHBuk6cbFO^M35+qgXU_QA>PpR1gR)Fo;1Rn%)&JLX;wV!2JMCfm)$%8 -v*KwM5H%8T4*eOOWcfwAtd=z$yRrA3;o!9E`N~hE-iBcr=N>J;K&?`mNQ|Oi8riaZVUZK}ixAw>Gkzc -u#pDsP~pzq+yF%kbHwfuH$dZc*RVTN0}M7^Bs5l@oJ&m -sSEJkp`2^V)b!*Q7U9Ek9;WJ1XLtq^93pu4ldqxt{vZRNHkScbXe^@aMo}Rjw$>vSf(x^W4%a^4YVV# -=7YxS{~2vDQYQWCF`?>Vo8^(vL@FZzpeoLJgLj?2q&eKt_wB?RT -Xle_|ne~hq!@NvRJ2){zuNVtq}65$64XAu5`Z~@^@36~JAA-s<8bA-zX?~?iwevfb!;gf_95tjL0OZX -t+2Ey;i1V;*7O|VfjZxTrS#|eiMmNR*knt2@~@#O+#Igcl8oJ4%t|H~k}Qrd^`8!~;GdBIZR%W`>4Gc -UM___Dn&Cwz~zhi2ZelK8S5*K6hxtBL=VjF0eo>7TGHr;UVPBdi}J@bfZ0%{)#B@z)Y=(9Bzg6JO5DG --~ECqlo`B;W)w{5l$og0^wZ3`v?~ieu!`>;Wwo{3BNDxNqC1$58=-B`IWO@i6AuQ)z -<-DwZu)v3=%`oF~sEzI< --TdCo!7p2U|c3go=GoL4U;{zk%^2>*j{IpLQGR}z-b@oK`a6Fx@xEy8l%QO>j06TejIubH=PB)*(?l= -J#>-d{gdVELSq^SE+eIE46ee-AmYF6ZIHiT^U;D8jN|AlD7Zbp>(6m(NW(Pc1i$N+bRv!g5|+&co*pm -E&X-zh<365%J}`tz4HN*DaJ1|6#(L2+Q{wxo$xYo0k(`KBwh8xSU6?B)(kXBImi~H*wX(FCr}GoyQP9 -M*P);!gyp&wxvr&=`1cc*>lEa=3jJ__Z_euGLw- -U}JTrRO@olOz(<@>r^SJRnrDe+~$SgwHlkkT!{hD<~X~dr|(?{d@X9?#L|6Q3r!f#7^5q_3%1>wgC -%XLX|9aA;&H%t2vmiuwZbu`1JeTctY%Ga#JX(av{!gAe=T<4=7DR2p4xsFDz`w1cbDw!V5I;(KvFCZL -6*hW~ctCH)m;)tJ4Sg!jSDKYVB+C*JvWgwhO{7k}fot9kJRYd#@!g8GvO(U^V;%|}q5x$kMTxTWMWmO -WNwjX5Gg!2g>BP`eb)f4`Ta3kS$g!Q8Y-bOft@T=0Egttq35|-`DexjSy#g;*+h2jLYJNyu?1A$%`sJ(74}q6x_>OC^r#z=xz636IOYd@?e*8bnZ -M?hvj?`SJkUb5A7iHPh6_D#rmC-*z7scTaHa=dsKFLhKYo>p@zTIO`enM@4|&1=@Ii#7BxOGZlW+GZzd6)yNPCNU5SM;pp2k%##k{3!-Mg57a_8Hr-n!#lHGVPAnXASx=GjbY`4#hk7PTKD=0Pp0K -4PB3l|F$jYIzXr&=;xgwwOO#sM<@+cP&=y$6`&pBjcsW4YoNqs4h{*skG=`sRuA)bf`{y9r2r#JsRgEk9yj$W?v>wyF7PqxQq)-{H|76i|7!so2W -vhgHQ+F5m8f3simNyoziwB$hQ^Vp$`_JhffbN6b^()$-%e=p*KFT=hp_S9%0?sOcB;+72~8#JsY@Exe -dNkP;-866E}dJAaXeZ*$W~&L_C=5?=nTYJ9T(cs^I%-QAyu4tj1EH0xiud-`9hvCmSCJ(ltE?ZU+#Jp --r4-cD){TsW6MUvgFK`8;t_zrwC!r$>H@dH+JyzgQJXXYScaKyXW3HeY*Yg_xBg -(RXa}Ry?EY^9bC5O52p1SqmJEnOjpMQS-hpi|1{I -amqTZfZhe&dh+_dl4iQ4BWrY`x}Rou7H&tc2vT!M -?Rc;$EOpg)tP!d_r>_Yac|py{(8N$Hq_Nfyqe>cC7a%$P*PZ=swF2D1 -^^L;l?pK;ckt%}_H$E8EPl9R&Qz47x;!E=KrmeHR*)@5E}x2T8MXB}Rlb#k`E`79z)!=L1kZ}w{qa*@Pfora--ZMKRkZOpU6 -r%zee9Noioy?0JlA2@7hj#Ww=*Y94`0@)_Sd|3*T>HKWZ%R6Wd;l|DoHwvfslSE`2oNAA`D-E&1%NdnQ>{Dz?jkPtFM~E_~)e#?HLDYg}RJ>J6PH -e6)DXj(bntvqe8;@{HWp&z`ulenX$r!54Ce^qaKfu^(bqy}oz9F74v1+%KN$yL9FKOA?_3W&iQb9XI{ -d?!~zq+wUx4IXn9sKB;;C^OboM{?Tp58y}r2c=Gu8m`-mb?mx6-Q*hnq>&6&oel_9w%lXS@-0;oI{(P -I5p|hX&t4VA9?139hbMCpL+oC;}?kQYaKWX46Q_Etu_~`dVw|cXzzWS4IXIK4x=G95cj}^BW`Z)U953 -jmhS@)7pM_*&+*yS^CdFJEcF<;Hh%WK#3r{QzHo)f&M@1vie55M#LBS)5vjqmYNWY|}|JLUcGl*P@g4}R|YZ`)2-jM&bGNPWnS2RZ?7M}{Nt(D>wY}b`KPyYhL@D?xG{QQ-EF?(dYfIzNFm -x?7(T3hu`VZzuS8sZm;?D*wPP29d14Q_=a^)ez>{(!1TT$OB>qwm6reJ3=A9dXZyIXUwj~IUf{O}9<8 -^0enYgqZpYo**WKJ}^zbJNeV0#Pe{O2zvdgy3-x>otz1R3q>%fNepW0jZ_MN!)^1dh62RL3{(d2ViWN -d|{a6sh1JLf(d_+!{1W?cT^*O`^|i%O3Le!c0V>i1{g8#w#*d54bf{v&PZ>PoLJlM}o~@0-!(1@?Z}S -5*nc+e-G0JskPUxyD|33pU>yb?dP%#UI<3eo(Y^*Vn~~UK{Ha(bjA`cXYlre`Vd{-+nRn^0xfUWwS=C -{h)r{fKzFw#(k33G^JO^6?4ny6@Gg|%ED!xv)=gTe7gUYvzF2I!$+3+l;5*!=&!Nu!g`*Fz3JE^zjS^ -6sV)<8alS75>dM`Nz4uM8`2LqO#}l_b=s4k5_rwn~7q;2j@wv}-?)tr@San>qC3t#{qmahuYXy#cK^3e-<&+yYy8Y_C-QlTz%MT)`%S*(%)}ip4I1&<)A>(-G{ -Lq!e8+Fg_DnxiU9+&ZvHYclWB0xI+MH@@z>`lt%=X0HJbumYX_rQQkvyaR#FF=WcDwAiYVx#2$F_gIB -H@hwiGMs^$V$i6i7`FnLq`KIdhfPphqT@^>YhKIFY3_$Gh`vz1ksZ`~A<4{%+!kM;?9d^l9blk)LZybj4*j2cbOvMAF3@@8D-=>^} -4P^Zqlx&%E!}3%fq;3#i*LxwR}8RuWR5~71_@6&{_8-fh?!Eh&)i*zNaIiga`+9VT80JvgQD%cyT^?(wyBKCBD9dU0p;OV97xoBxBp+ivj -?HEw^`xax~#jRVJjerCz0!QX!Bmo>}&WtZ}or`#0r*~ce!5m&#Q^6~wBg8RK#)BBAs|MGrsz`<5;>_7 -j?)RWWwp4_iH`Thg5z8P9FXYT{$X@f^h?BTt(!`JUz4gRyqr(JQ@!YiXb%~_gsW>+Ro2Ae7thWaZtRc -Cw-jdcW_m@sJT(_cOLs_CnfSrKn}->~uVPYX_f}4{WqmR -C;LSTnp1T@sHO+c2^u(JF?|d@x;?A70-3=(pV#Mb)Lh@Wk4meSg1D`soXMZi@c+;LCRvObC1ONM2-p>yVY5^EX-+oEi8)cHHP~54W#vHz -I9Z=N%h=jqCUJ+#6O5I=;5BY+cFN;MY$?u8aF5?>6tcK99-CCUNN?E4`o}0>F31UjC5TdFe+oFTZH!zGfwt*lkMmsqR7-K58J`}~KTe^c^4j`#h%pX1*iJV5$?X$bdbB-Io=3^Xhocaal_ -x_pFk_%59=W>6Wbb{j<=Q*wV?ZK1W{pzqUdALPKIWipZ6(wP#qN^9`HtlibMsN9`bnDF@EQo$=@WIr9y0qvaDM4 -eF@6C+fu)<+^alR?~++DA}vZ(Kj=%5e&<5~4Q^fBnzFWm(Zn{T7!#`;Cd@ERIf|{q&j#=Pr)^bJxpDsz;lmvp-gRk -9=y5p4jv7t8aaq7oFWHanl0@dC{W=e|T=PWp?!Oo9lyr{mKyiYoC5QYTpAX(J9f7%uO_&?q-c%ka^sH`$l8*g|k;4Nr<#Xk9hF8bDv(yjNU!}pl#p6?C6^=j6Y;7utkqLwR -++^yB9`JC^{ed%DtJ@3}4!bF!in^l`tu_*H83@_p6wz -rHVt>@XzyoqoE1MOWlSZ>o5r;^sF@(Y;RZdcISBd~{~Fn8$v8%^W>D<=1)9i_@ZmUO4mJz_{e-am6El -9R0g3+M2&`D7GKCf)Lk2;y#BshdhP9)8aURf#H4+cD^g9fxmeh_#1B_?T?DwG2qr0-a^$~3)mEs(_}T -6?RGq}#AdLX3T;`rX8RaJUkCSOrxE5UCX3aaZE)rr@(av1gSp6@RmhLDAvv>c{CpeW=U8mn29wQTDgc -k!5C(Tcu`D$Y()i;ipyb%|^B{x})4M0*D@5_@fXOV4&k4(U>S_6zpkGeDJ&$h$X;@Ndwin~MMLArtYx -oYc)9J=$4rewbFT1e7YRM8lBu8Ap&o6Yg>;d>-r0WQfb^vRez-GA1u7U5^Vc?_MW+wJ{HE4FwHSjTZv -Hz`s^@H580`5mSWmU&FUG`sSb{hDRH(B}FWy+ks~CSO)Wv`=u&>a#4Xf -%PAy_oCCRec_z4n+(Qe3+4>EF{O*T$j$Cu52^_5!n;FW8)zPAyWw{ooIR3?8%5j?QhJJ$!9rnSQ7TaL -6ZD~IlSW|9XRdBkscz -8-`O7TMTr4UMr^)W-{P&`D%SWDG^3#m{WcozCbeDCFy|c}{D7gDE6gp6~*{M?^qngj3e!ctQX+S&`Sc -)_;T!*%uaKhC2-5=B3cmSaJ;JyaH!&|G$ne6N(xYCKK*?Yr@m9s -4#P*G6D64qqW~-BrnS9dufB9y}a}!!;%+fp=2F!?jt@fKvnT8isqG -!yBcr|C*~n%u{Qx%9`f)?^mz?Dt-QwuJYSF)0LbuDJA#2rT;vapd{Rt(dzKazXZ-je(ad}vmIu;WBwe -dM8)%y?D=?Fm1Dlcg2z{-%jewunB*CwM$At(Tg@hidA@Vhh)9z?Z%{$DoL4=7l45N<`d!5XWCtRG~_h9_Bg4=9`kUE=V5C#Zs=IAal>`r4d52ya^p+E^+!DX@i+oK+>&@4Ax9LxmXF-{vZioPe -`)A(h^HHu7vYM4gD2y9s|);MM>Ydr*;xx8i0UUq?xK$d>>f1Gf;D8($XG0B&x1&6mF5Iq?%)scvGqS^>lfy{th -(Jf3~cN1C)_(!>Rhdry$5WWO5@Kqh?sk+H!iGTa*$TE7JS#|d8*+Z)%>Q>=@XU} -$LWfACh*t;4X1pzOitfI>qp!d{Ol_>Yron2Q6WxA=U$<5v9?*1^S_mys9)y7_RXTOJ{a>iKY+y}yL0A -a0|QSZZytgeo)?bV;TqmD%~V_-PAomq8M!T54nQs8{VcN|*X2OJ7U)0rKEahZHu0U!KkqrmNH8*r>ja+uS&$Ei-z3pTFisTmS-W?$*PR@HY_>+Ti#R}T3X25Al8_IIUqosqG(;CEk@XodAWeZqc&A-~ -#a@(X>Hf-+_Bdx64$AH6??>E28nfzuf4Hq8}BDfk~t_MgUv1fDqrdfp}UtOh;DHskgs=u!q@weI7Z@t -et#LU8@@8DW8Yu*>cl+I_M3*TGHDO^lnAz)cUQ4^c|h%@;j^>Tw7V+oJA>=q6FMsT}Td(@v1n;~xadG8Va8sJ|4xC`IG%fq$l^? -V)7BxtGr+G8rLQL|2t0FFwTF!IO2#h1&q{u@`Bn5L)bBT4GPL|E;2m`1w_$;5I;5XNRnSgUiF>52vE7 -5hF#yuZ?YT;{j#F4c$eBB8d=GP*g{yJA^)X|O@LNZ5B(~u1!e{Ud{@mp+2-B<4rQ{SmFX2}~enz67J3 -MW&t{&u^1HH-5P)(Y1f!BD>Rffl+jJ2LJMtsLu9sC+dMhK6`Qmn(oqvv#$~Y8WmmoiYxq&WL;vJ=7r&c^UkbeU --1uP{ekI6eZv0`qZ4_<$15khX^XRU9l*4Dwj%OK=m(}m0-uYNh4_{kPZ&zjXs>|j64|`!s?+1A$+)l3 -gm+fs<2bZ=`2EzG!`D6&CSp5Pt-C|UcMRO7@i-1~ot|kyr(sZcBb%wa-UAhu4ZkSLOL*P~4&X -Y9a&!l87d_*f(@a^tuP^9#gQD8;eMna&kI$9XgWy)h%azNXqai+Dw>EN&2G0S#`-m`VTZe7v{0Rzs3x -2ZxoP_&zMi(BWJ_|j=-8UZE@bNBx$Kbxf4MUz9&eZtJr}0{u?oE*HuJEkvO!=yo&*=(t!Y}RJmh1YRi -3;?C6;?!kHbegQ745P|+kyHWOj6j2Sr8xjZJn$zL$V8Z(3Ib!Ah#jWW%o#JyP2Y}OYoC*wWYT0&s(4j -EKt-o>)>_lFhXgWm)Ju2OW@_!^EsM+oj688{d8;WQAlTdcnxjh%(g!1-+ro1f -zw<+AsRW70J2@G_r8w|+jFJ0hrD*?AnH7yTjsThp3h4YN8v7ZYTN{(St;f6a9hcpG4bcP&`DBXA8QY=vzc9h?Wt(kNiJN^ev(_L@yB4 -nFKWu9Y%Bt(Nv<@L<@-CM)W?Sj}U!{=m$jWi2g~mZKj~1L~kHEhG-no1w;#ot|s~j(Q=}dL_Z)}L-Z8 -UR#}1u5j7AUL^O)%G@|o~T8Z99bPdrhL|-CWMRY&W&xn3Sv{A-S`7)5`6ryv8<`P{&^r385nZ+xS@@o -N6XND*LoOg=Z8Jll&@V`-Lm45Z<-PLj!)JC}W-Oh -wszpf$l_GtXsRbm&rOx0`M1(WW)%mg3A7G2<7IF%E}ip-rChmG2VIoalE(es-aipLT}Fgw18_bDy~;i -*ri8J;h>MXf>1PNM#aD%9u%Xz1NIFtJ5;E*lAA9pKHlB$L5;s>^Wr$Psj|2%(M{YnJ|_FlfyX~r@!!< -&sb}|LrOWyV(n%Ss;U)(r!cr5)>=3LYotEIrQ -56h|gv{++0(0aXt5II0(XQSb}ETa=F#ACl_Uz3$U<2D2 -(}#$aRz5Sy+HYX)Y^r<*l$Zq@u<<*=e0^TV}C=05%jQy5-9git7}+EsFa|bz2qp^XZ;cz2TK$HZ9d2S;ttq*Y(b`3sNSTt!C#n!RC1Rc}Yjd;KgKT+j*#Cnz|OldZV>4mZW?OCK`{~yDEI~A_zi3n}u96astUSQ8Lf -j;sP_9HJ43Cj$7NfL+3jF5 -OagEZBVug9l4l{iG?{iX1fCyLb-BzW}&m7(22T#Os$eg@eG;IDUgLwG-mMJMF4FNzfU%cj0+UnWzWnJ -<+r^kzpf(lzgIKYu%}K(J~n`{k8Uy4TK{R`ky1=ac`Z@U>S`ar6F%{6^=GH`4j#fBw -aMcnepVUC0k4y<-0=z<>A(Z1EC&LyOmcxoZEPTrD5$DflfPJo(?N_kZ>E|9ZL5T>qCB6Gi9k@rZl|o) -j6Yi&ys~{48xGwv9HtTD7(IPo&1%N;oYC|9hT3{)48pj6DG~qZ70RK=qyB4HIZBP%DmQhcH$IbO_K=ph-YC0WASq2GkG=Z33=Q>LJ!M74*QpjLs -^Ddf?m*=o=>7%ff^^Gca}ma4t}9NY5byj*I2!(x3sFr89e#_hzh(V)m-+gL|H3Dnt6>ylO4W1=a3jzJZ`3LJ8wJIAh;2HH -cgQ##jjLs8<8UuG2nVzQ{2Rv$z0!!T1L}nQssP$isZ*+>(Jm&%I6>%BIsw;&L%Jv69t>t(l992K;QtU -%vy!PqO@#JWIp@^~9mw~vd`w}ir&6y=n~LAA*D1J=r}ZqbYaH&OV^R$9xVK)WQVG-u?n9Pk|Dehox3X5*f3CIvT2yA#4U0IdMpn1 -*{>X6wr537Q6v&1`Tlnh*BXfnE!sd;mR^!B~HwwOP1VnMujb7Ivx4#<^L@A2XIKlQJX+;#X=}!$LtTE -R5{}TCiBqA$gEqU6!uA0PF>{!2#tP;>%qI82ZcVjv-|4(~g0#;SE_P;i0Vw2c#BO5%9kfa<-jhr|LDxef*8f9HT1)&^prgAtAx3J9 -cUT?^^G>-eJ9Kz5DE~nV# -o6J`ZVCfTsZ~lQ}Ms?+ZdCqQ8#56hSoLq{5891>g?2SsOMI2>AJXipeCJn{s94T-zg7v)*Wd?mAD`1%Px}SjgX8K|) -UVNQme*moIJUpecU@MATEIZqslN?&IUI6J7vdDJX9&>}_G!+hJwaaCwCkg89=UwSMvRsOxLiBz=9$lT -j2_l%@@X%it3bE+YCtpE^DKh?0Y(<{-LZ!>ABUntvpQj=Lwy@nGq2&hZ%1pTYxMqn*0P^ix74*fKCIO -OE<-#1Z@`}cMz4chdAuHLI6(V)zN_t^R<=PO2ak<>mt>VNmC(MbL^9wy+~>22&!B#z!Tx(%=vup01fEzZrp8GZut;fw5G=UUPhwjb?Qv4uJ8D)qu++PBwMosamfx<5(Ys`F8;V2l=0s{ -4n;Qk|X>K(B{kFQvNt61NG_7snNV^W2ZRN4zJ*v8#@{hwQ+7f%8c_b-S+I$!GPd)QesTeE=WEbxS$*a -*Ug2h3>yX0DpwP@c$I_xE*tk+ygm*5C6SF%)~7>h@;f -^Lz+i5+dHHs0+ylwkYmtOK+ogwYnaExckDmfE&4m^^=QrOd)^Pf`90s^6Qk7tnsHy!2_c%pzgmC5xZt -?#N80Tnx1^uof7s5?d9j#HFlv{rSB?|M6=Sx@W74kxSvoUR>mi#`kg27C)*&a08cieuifc8|-EFL!`FglK99uj!C9hP# -)1W7%wWj1+)0*d@H#caQ2M@VfZq!JZo6k)e>>2YFpz`-OYp~New;I5=!I$H2npj4@y)>~1;~w2c6E^m -HyCw<%{qNL7DPUP!P3+-u2Te441oqoe!(0LSJ85DXjy=6K_~jV462N~@UwkwXjq?#*zz<-luO@r|lke -7)KGg!Yquq7YgeT71{WQ#Xhul)TYoe1D>sES?ruq5`m_j|M%D)xee4Yj{@j*@bvl75{ -r1v3B#Vt0#O^nBeV|*S({|~c$=sn;x99thnJAfsRX~GUzJxUXs0pp+0gqhd>u8BxMbgA?r5)ku=o97r -!RPuhn+aH6!iqpikfFTK*_#FP%^d$74*|Dq2CMvyh^O;0`4`_*iX58mBnfeR<6ofyR!f`pQc})XdxbB -gpiH8C0i!>1o81aIp{A)bm&Dt0@>x()}T}*loXb~@gUyOgn5*fKLGW7i!9Xl@-zsW8F%MHE|5>+8vsTPa@VJ#AZ8d!GE)F --6}R{D$cCls0j=yw2yd2-S0VI3MYRNlwV;zetaI?g`__=X0V*{ij)h|7*ovy@%8XOFX`oAyKGh7~dUkz4* -QsYVX~K^Bh5hFjnGWYPa$|CzvX&>OD`IuySJoF4)T7D44Z|?P%(ko*|8#SI-7Ea-JpWcN^aNiR32Er# -5lk-o*KmCeD{NalW#N^VN-<=l7IrHT`!l)h_B+yACm+%l)Pe@HH+wX=PjB1DjtsHgCuEP8W_VkKx$!! -m;&N+~4fNarJq(`gfPHL`XOIrH1tB?^!nq{r$h1X!iRXTuQfY-NfL*gT*6{JR%Yj62$cB({+Jat@vjZ -si~)-2v|Qtqf -Lmpjhl@&`Efz`;r2;&|pv9#@y|*s(($s}zz4{yQn_;?(t-vuZrP2eN$Mc|5b+qn7w_adrCfT;9Kiqx^mS#97t;e)g{)qkk!1EZ;%WaE|oVmH)CIM`w6l(L -cZLg&lP}&ae$0pMU7_P~1>A9_{0}`Q{IAx@+Z*aqmw+283+PYMCRLv@MhtovWocFon1vF9TqDrgKP~A~mpteG -7E2-z6PF=s8p7-d{LxhKii{ZnEi)WsB<{}+WNl6iTd3j>px^+s&_wL;*jvP5sujh&Ui5Aw9iH?meD2Z -nipOqWpwu-r1Pi~ESYLb`>5yfqla_-jeqM!*+&HdqY)CT-9_w^&Ar2H*z_J>DCj2N*sZfK8uz^JFfCO -;_U`Y1VPk@wb7BcjAynXz^K2)4Ir3EQ75<%zRr9YSgGvqH>i+g5tEXe))gvx-YpeALd$vfM9w73c=_lTp(?={AH&85oDo8Aw6ew0qv52?k^%jNJQ1RNl(PCq2qIm!1A)+GVDdDq4io4& -DqWk+&1ZTvqyaK#T -Vl1ufG;2k5r1?KS^=mzyYNf$BrEnr%oOhhklWwrlv-mJ$qK^3X7Yc5X~ZlxC%<+ks`#^(3@*u?AOD9P -4WiOPj(lhIsA9lg- -415dl9|Qh0;4cAwF7V$0{x0BG0{pmHEV=C@#0B47|NeO*f{v -7yz`q*!@IN&OW~Au`A^QXWao|4#{8xd$0r-1d@Wu5aLfitIZ6{KMFU+==D2L|PV5J~iKPF`NL?MSgC* -25_-&W0$G;8u{AO4H@E-#H7~szUelqZv13w%1>wy0b@XLY! -74W}v!S`^Ez|FwF4fySV-x>IK1HV7;BY{5|_=|y`4g9UZ|H1|T;u577*Hb}30|EyG>aU3!+Qp|+$M)? -z53au&I3O%EG&ndcC?Fs(II63Uw|A$Gg9mpoS_lrsW&8`pAHl&tW$R1H%H){J@~VfB} -I~ck_l$o!Yl^UJnho1cn6$1qT72balJi*SCMv1tP=3!;CWMHm_Tvx^>fA=-9rUsl#C6qy4b3(15V0J8 -rzy3)gXjkMoAkQNRxfByvz#)NME3c&mP0(b%!Q2}Fk0w;y%$O*h^+q@TLMSKZLgMEsy&UJc~+n{K>gh -?BITUDyNgqXGiM9|#K@7#7ur4czK=%PoJq_S$Q2AIcthJzN#E-{ztlqBsn^9yBmKG%PH9V0hb$ZoqZ# -E~@gE9V~KPtGpg(QT -bjW&Y@-FV}TxS{@fRIjU|EPeyg!2`H{=b%A~gxl+{2eoS6%Kv)vz_8$OjPjtiiitan4;uvZ4h+1w)+6G~e6O-Q -$K(TnrijUGC8%DColVsHg#f5B9mfMe|;!xu*5?4 -@L!r2lRoi3E|qlUP3%u-ya@`aSV02%=UxTWt_k7-k?6A7hl%(2ki$#5km*|?A_q9OZym_(7@geE{m{> -OL&9JQkKrGZDkFpLLgE4*Ti#k8>x-Pa#j)0AfCH)oMOat4?Xmdibv+e4z7NN{o>%kgW~Yv!{WQ|z7yYn|GoI>$CD~HIDPuGICt)xSbA29H8|e&n~R_IYln&MK1 -_75%ak#g=$^(z_cA8BY|&b-6W!$&F-(?;$#S2mVErQ9n)^Zf^C -jO{0sBkf8&(@#wq{5#wo^kncB2zqaI1RReeaRjaMf>KR>>D0-)lV5AkR(D;8<2LO&czfgC)}C$seEodI?d`kx_}y`}Uu)cO{dL#fd3(D~K7Q9+?Q^H|gvsR -7#m~dzS|8xtb;I@7weh;8i{CXK9#^+)-u&9TTD5A^?5Z|h?zqO|YM$`(YDDo4h;t`uX|tw~pw)i?44-U*C4Vioe@Y_^e#JcKQ=ob6W|~mz -#0!&f3(&qngV-yq_P)Ypd@M@jZUg>K4-_{p=MK?tpReeU(dXE-riy>3_TYnMY?yFF)sabKACUIjF3Bm -0{*=X7hkEx*Jl|c{#O-uXS>&4?nY@=`kPj-Ui}E>owrV&Jo)VpKm2eMbI-vO -Cr%sz+<)rSsgqcj9{%d9uhx9dK&k^Os&yevo*R7qjP_9=yEC0UEd5xdHE>-*CQ;*I~o={1N)koH-*g4JrNi_xJBiJ!U2SJ9qAsZ@u-Fgv=#kW@W=1f6C*-4?k3uw3n5YDO;(mt -W0M^fe;k@EDJGrM6cw_lcW7y7qs*|M}_ -$BsP_!!Loqq&yT4q?cpPaV0;L1!?Esl>Il~e4{G==9qK5NiX$)ypV2=KV_W$y_CUwrL=r1WuI~>@7p2 -ey=CBGyOdqPL+7`pZ2zW|OTLn_uC8w7dPbFNIAGt%An6M{^S8N==H -Ib_kf3q`=mTmCuQ6&Da~8zQxjK2|Jk!=smqJ+z4zYs(1nHM0dn{qFQkQfL>e38gX7LIr;MmK)PMS2tl -1Pn4}$(7U+O#z*dt|s(Apb3^n~6dR_gR8?3S|Q=8J#NbUFGlXGp{UQ~xm^N!ErkG4!0{N?A~^s2c_!v -~lVg^`3TNK-vY%@B>ml03O1i|AY4FJoKxO^8TGV4`$fH-S0^0v-OYD4_~~rQ>RWnA%`WjGy0as_@J&g -rC%I>Ugx|(Jo*{eGV(BNlstSbWhi(U2p$|K-;<0a~9C1j>VO3IwlZVfp_6(irYuNLLx;@h;b>5=ebJxwfO~d2vXo`Nwd2x>(J=( -*cFC_iclZJdy@2S@f`7n5)u2YZp?^q{uX7-gC)BDNSr}kGousm^0%150%MC_MxC};}#+{K>BgVUb7(I ->&Dc7ji82Oip%=)M(YUSssPZ{NON|1rmNp8uUZXuq5f3|Y_~cx>=M`r-Fv>8oR9{?mPB_RM}V9XzZ84 -=ge8pO1rwNYL@{L7fM~p2>q@&-6*%Kft)ZE9G6}0poAl1p6;j&3w`r{qXln|6yxO7!T3r3_c8A$OGqt -rgV$+(@(CS7buIK?IZKRLl$^Qo2Ju0^`w;1-+_l}DIYna+cSMq*q0aVnLHTwOdbq-rcd&DOEq)0A^o& -7&M%0w>MMCLYC}F6vk`+AmW^}omuqMBmPOC>k-16WVMafBy5_jt_p8oB6nGeQ6g;@tbC}bf19$5_$$z -IlKN~*D8$5J)t5LtC=tq3NxM$Cv?Gd9aB>nV@gwz@Gz?jG2h37ce8gUt~alU2D!1AW`ez{@J{c;U>C< -G67@UVILBzfxOu`6;m$H33w=NRJ=Zn#o1CjSx -3O%Q?=LrjhxK!MOUJA}vH(0ZwC8`rCoP>2C>K99Kqe;y$pzzrmC>~s6jBd^tqbd5~ -@0I?8o+YH4bP%zkEc8jeU1E%$?=`Of^ckGP7)P+$?RJ@;pD!5~8P!!Lm$H*{=g!HB*JI?i1^3C#^S}da;SK -Om?6hb4q_pY%<*F(DWolx8TsF}nUmhPQUjz@&frt6vVK#VRX&7VteRt#jhx9`il>S4n7m}Cfo_kIf6c -i|au7nSR7vfg$dPBam@IF}z9^L{E%AUakeUf3%Hu$8KQv&3xi5B@U@UR3tyZ|2l0X>=*8>|SWPYQI2G -2ZyDasN;H4gK%ar%z|nK>CuClk0hC$j7C4Id`s39@zGpd~b1(+`7PDzU8#%wJ!FY>EvOBlZSsokCr?Y -C>JH@Jg}%31M~Ax)Ky2-{QI%^eYuq~$Nal=*sx(?6DLmmh_uokX#;cS%uzhJ>euCDLP(oxxW;O%?Au; -cA7jv-Uvk=WgL29EhxAkSAt51@gUrp%Rk0UinMI2h$r&?d)bnsDJ}xB_7W$-hbFRRi!9yc{rm1o($0; -QxWgC2_^!E0aQBhGcD=SNKUYi;764l($mxBgAYC^hYuevUwY{!` -N}J=$YslxDSEiRC%xo>a{=cGgAcA@$OHW;^^kKR``G?su>3TK^q-L*WzM(|W6aQF4B9h&(rZcKB>%GO -ufBczE(#A1pGWkDAR64zu>Qj&yCNFEOvGDO7)@4fe)(g)H`S}7w#uMByRKk6RWIgIN -l3)%y1TG=z?;A+pg4eWWx#?_1iS6qqm+<*W5%a$)+uJ%FLx6_uT)oPU^M~;*oI&_eC-F25lY^`V^9kd -PCN*;{bkQY|!l+iYQ>&{i9<>r4_FhA=t2IuGe*+FvSzsAWgKi(kEoIbr9^1kBzht$+mxp3japO-FOD( -BCifBLb<9+Tt8k5}W4b*0iJ@<6*FpQMd^G~|VQ5S)jp_nf0?E8LIZI+EuYBT;tbrI7;mfBA-E^hfu9h -z#4cB_<~BfA-mD&y&ta9(hDn+O)w#mo8o8%$YNl95`nfdpianq?hy?d%j#75YirKBeaKx_Rcxmb?w3R -v-pGiZ?rAyKXfT=-n@BxXs2OeVRF{2SxOG%f%Fd;FhE8|Mym6a0eLVgc`(+!T#GTjrrwiILh2BCbJZ= -zRJro-MW{#hWIn|Q#VF=jqd@EBg%!3X&F!MRx1fP04}RiPMme#&pZD4Fg -h-=ntq@^v9H)vA-eIYmKisf1&6H--~eU`&xRCbz_SC^z!nuK+nb+Q=7mh_>+j^Uq88EHzKN7Sd)^;?U2~7qL)(xF%vH?c{6q>ebw1w7I -#t{ZZQM={MqkjQ2wFVEDzx_~3Zc2Vw43>jdf)=h{a0^cDS-jS)jIuS313E^*9hL#ce2|BsLRMfAJsKY -c{QIkq7$gxq6){PD--tFOMQ^xJjZjmrC|OVo89a}FTwMt%fw?W+HPc16GI`WJK066z)AAo5}C_tSsS* -Ks~_<%46pVZ#Rb+H0>VeKu^Em3$j}^NgE#Or0_E2k@CW?(XjY3GMatyUvTQF^p?$b19jy+H5w3Pri6R -c_Y2lE&5LKV$hEGH2Y8L`|s!{EwC@8|CqlQ8tbo1@nYfQhUX{;BPU0h(HAk7z#J5LA?@hn&-j^ -3((k(eW2~hvB@@Q?jDL8{yc{cai}3yT-&cN@V?cYQzLS310rWBRFQi@3@4Ej9KT*H`LmoJHGJc`|qaJ -WR;@rtG=9-gZPdOO-yOaZu>GSDp>Bq39P&CtX$C*4**P%z}5hGMXR- -9Mc{Kd4h+#vL45dJqS``~jAf60#`r4^IQ)|oTwL<@O7VCKv!Ap&Z2h`;_~=DTInr -GEwebDdD?P;)|eCv4$_t(>r}69zcpTql$|)Ce#B-Hu88iNfc5S~zQSafw)w`+cY%qMm86UgR+|Dh>Ao -n=0(f3$d>mbADKCZ0s<^`@!(}d<>h9oiXO-=L+ielx=@qE9H!XQikl+b8OrTW=`Rrccpy%6Djxpq-eP -m4dnIq=FOYmhqc{g*mN!Z8Dh@U@VCd{m%fKz`U3HJI%3-=5d-r#4E%msd&CLH8J+joiYD;)SuyVcf)VK$%dITWxkPlF6LLbC(gat&rV5M@z -WLMdfU=Z&!0a(X4$f3BS`~eC$1kjKhm#rE~2kyEXz2B*Z4QfsOk&(Lgsjw@8q5~^E=#=XO8W1{c{a~H -RwdF7oIfM_gv?)U&hfK6JyQ8xXHCJU-sBhJ>SP%*}9s>wk|FzCmZO4>fiO*hlbBQa7;LVojCG^%vmr} -?PD%+?g5TIA!XEYJ#RYvpq}?No*!`4N9Ko^XY333*~L29mU9Z$uQ7;oN4h?TLHRQVCM}$Q-da9E?SXM -Wn0xX@|EzyMuID|OZ&UhMsps05N5xzx^MsFntWe90{yC3hy+3jG?AcGc=DUq}mXNV^(VQT)m&p -BG?jySP&)h5X$B#77M~gE@!(1P8Ow>o;w{`zu^w0RmHCNg&|4v$19M1ei#teNw_SDJaa^8;@(?ES3*+ -3ryoH^5bo%+}yPpA6FIw@xP^5r94>0sZ~b3zv8+qkF7`~vrcH@up7*_`3=U!|M^TKetMb8WN<<~v>cX -I%00(@#&FJ9qAruDMszzr1q=7rnQJO^=KGju;(jRgkvS&jHWoPZOS8u4^_BSviUy^R6&G^f4f}7H -3vQSLru?@(AE5GG%oTA@lY8TaK5}22`4#3SRF2Ep|Ln1YR9}N#^s(QLi{;OlY|)}cF)LTD97$W`dY|z -*`6ewK1Hw-;5>(!h`6}+K<~#ScS3C0yvr@j?$%ZRX{VPJ{3AjJ)aPF^Xy4VEsOfNd~WOJP~uqb^*zmKDSb@pQWH`ZsgL&p5Bc^22)9mnZ%Ii -ub06ssHyb9D(WU&(|FQoUUwkp<_19k?$=nd*anfO|tBvOdNe9P*{)pFDn7=K0=7LQ$rjIVV) -~Nqyj6G}CtclM*|NI1w!^{)54>c_618_>tbI7!#SCSZQ{O9p<9=X|GIYVIs$gD)`6VsnU`U132 -~MjHENXNyQ#hAhO#l{apT!aG|%UrFZ~05j6eHE{BfMQMD81hhK8#BSJE(f@?}jD1RceEj>&@ob1ID6!NbZw(06_R4fn8I_arzUO_?%9`363#YV0dgUL0%kO`p7b_w -MXJ*thDR{lM2K|KAuLMhv`W%^JCC)he}DOy9^oCnKl$C;L|Yvmayrr7n?QBL=7K8F@+4KpkU_gZob#H -*PeZ6a2G%*YkfRd!Y0)^nH}G!{MO*oxoTu68iqhpDp~YiRYiJw6Dwgw@UGbSu+qP{R_gQ~K8&%N1Q?x^_jj69iSc44zQhyFA0P*#TgQ}^G20rhlPBV -rx;u*uH=*yyx>1!s&r=KFM>G8ou@v-0>%)V%A#+t}A7K}NeuYouI^t);JH0lQWCfX5cHGbbwu{l#Nou -tQxW1MRzt{=I6`EL_7dUVP)p0Ueaz -qmMqS^4#=c9EYmyX>xSJy)yPo5Rc|rLt=H&SNfH9Y{teMka9PZ{#3?$Os%PbtfX9(qWPDg88M`OQ1&a_# -)V>iN%ANkimEZ{`qxy@9`F%N7-@8PDU;Z!_kiJm^ndY2n|T%U$)K_Z#vzo=IX`oF~~o=OeBY*f#w#=K -i^^`1I4Ho9%Ecm@6VroU^%~L|m@3Id^a0zWpLxmN_j70zFZMyR(W*RO6l&&f1FSHPJ$tOrGj(Pki&5# -xGB(BffXdP4IgXd~*Qby5=tU^$8yFoomg6*{pjfzHzO&^G=g`-`Z8qJ5B0sYb`|IfTRiIO*3MrOih?H -v7dMMZf0*&?8GsX#wJW0*Ux*z!-Kl|dz+?Ci^*f%eC&jnsa+=|jF~cN>ZG`7UB^tC&^u=8gl;psdz&W2OiYN2ojPr#Yj0@EWa>L@%Jiw~vxv -^t+Fz!%d)q5KU`~x4Gkr?Jw5OfNI5Q=7@^thPJ2qlU!i50Kk -t~Sp%Z6JdMb8`w`qDp;23@pteXzJ@%&m4!%&looxG&y_xw=`KQU;)tol -Jv6ls`Tpgn)KRqk>QnL%J9jE$cW5{&PdF#W~5};G8`GD85J2-88sOq(=*eQY0k7{Mr1~3CT3bQQ!;It -j?B``ip;9anoN=9nPti{XIZi$vZAvRv#ePuS+*=kR%up6R#jF_mdN(ZHf5W$E!h#-(b}OUkq6CF -iB&rRLf4?0F@5rFms}6?v6;Re9BUHF+NSUim)x=6wHrOMXaxX+c>*ML}gjRY7$@O+jseDD)`wEc7Zg7 -5WsK3;hc%g&~Czg^`75yhan10Hyh5`4#z<`BnMV`8D~q`J%w1z_Y-sz*OKSK4xP -VSHg?VN#*BFu5?LFtyNDXfJdWmK2s2mK9bMRu)zjRu|S3iXx99&mylPQ;|=RxyZlBQWR1YQ50DeT@+u -GSd>&`ElMs*DM~G}71@g%MI}Y0MP)@5MU_QWMb$+$MYTnu*rV99*sIu7>{Dzm_Aj;+hZIK?^WEw6oGY -O}Ng2r*sTuZ+l8my9%8cra+6<3OuS}mz|ICog$jtc6q|D^Z)J%J3NoHAQWoC6|ZKg+-SC&tfe^y9VWL -A7uQdV+SYL-2#B&#f|GOIePHp?U1E88dAKRYBlGCMvyDLXkkHQSzDl3kWvnO&V-o9&U~mE)7+pA(W3n -G>Irl#`s3nq$u?$tlaJ%&E?)&GE2%*?sK(_7HodJ>H&VPqwFG@-4BK*(>eU_FB6~u2-&4u77SwZe(tJ -Zc=V?ZfdSQwH-!&nwR-&p$6DFETG4T9FKmpgxpA7pn7WotoeeEr`sI&rixv&QHy^=a)d -fm5^<1z6Yf01Br$dL>9ysBo!nVq(ZhOkZUDmT3g@&Y5G8tA&_D`B$y28*&(?yNUgfCw$KCe@`0>EAg6 -f9C>ipxLpEiQOEqNT0eSd979o&Bd~s57a&c<0y||>fthlncy116I@pR+6TZMmI|sZzS*3B55xXCk04iO`i4=!pY5QUU#_f -o^z0FU-)1h~ntt#A0i4O0lijQCwPFQCwAALpn@u`ny?eo;H)sY_r%RY|*wvo7I+r@Y`W4wN==vY&ABK -=9y+nGpAY7BGRJM64R_{DQUJeM_OrGMOsx_O`1seOgE*Q(=F){>Cx$l>DKg=bX&S3y)?Z7zN-d)%M*U -foMD0I`oHhD65*#(;G-PyP!;e_HF+Z6Gv5T?WXX@nkIqlbx8|qh+wvXxrSLda@HWEYrq)`iz)8%Hmwh82o28)hB)T+pFY^Sv`jF8f -9DDM)d7LKGqN}PFKOjEGTw^7YvOBR`sD@rW_|Ze{Qv*(U#Kncn4f*<3*Tuk4i{w=hvzt@n!} -O}CY@RTJI`#A^0rIipeq+r{A}lDoM&I8D9aO>rAraND_078Tu${BCxA-+aL5z!Kw3mvBt;zE8D^8JXP -Qi#a9woAhHp%!-A%i9e@C0i)Mj2no5}yS&6-8f=JsLdw)qCOSu`fz=6It`DO~2VNnzP#zgtocJDK%wv -!ESzN@7Rz4)|G{KQil#u%*Mt+SJepY_?SjHOSe_$|&Dn(QWn#p7(uvqb~k@-JFvg_3PSx(jSt`y~myhQ+{_-9%vgHh93+bXDo$=;1u=mvHL>vPaTjMfsNj77}@A=ps4{K -JfSumY!kHk9EFRAt}9O^{qNjJ7K5?{=J4QemfeK}_(%jxJ3U@G4Nbdk(u08jKG{cUwA)9LCAIgl8Mo!)Kuv(7MFl7V8mzsQX`~=CNGP0Q5jDhN+=pF -mDe{M_z-)dsxcphc?en;BER@vyTR;P8v-U2@PfPhOm|ILo1?C=QUNht-~QXU@c}}XcbsnBw8XAYhe~gYl;!Bf?vJX*3U$_ccwHeDSmd;s9N)EyD1U -s5KST3}}*HQR!Jmh!z>0__#z-c$}x0o*g3 -Wv1L?2Fxeh@;Y_LHBh9UI;(7-(uDo7<4CjSOOZER)qMb(HHWK*5N>+zDg!r#DhWZJ%58xl4jMDcL-P9(ED3(jCVS;f%?{YSy?)Fk2v%FqD=A6B}{7V1wEF{FJWO6!(~N6RV3=qdOisgp2 -R^DP(1>Y8_%?`e1iZLJeffUCdA+)>!<1DnA>8*o6_}5crv40fdc+ang}(=dH=TgYx?=^;)QXvw^G -uaI(|pYIEoHimXX?OAxAII&d8XV`_>}ouNZgF$Y4N^++=A}NImCgz)DJT%;fz#z5r#ajLJ1Zw--N%pC -it1c0rK|nBJluto~Ge8{aoB)Gav*&!6Veou~h&V=2R#rFO?^YY`?l`1dV$Pl>IA~jm)v>j-E -rYGJ)Mv=zyH=vjBMaMcYl(wO>F+u|9(;Zdc2P#4y!wAwARp(|~lOwjbANWtLDk6q_7|jrm6yWD+X2@G -}446Q|yT*_GxbBYOsB&*ptoCH8TJr{FSqj8K=3=~kYeCMiux@M2QnE91rC;RN!w3Qu?vzC?21JRPF}& -!iF(gVun8ATIp^_F9E<^QD2Q6^i>(o+*+_WDvX5jIU!gDJ@CZJ;?Dl8ogkQ{vjJR>8w!JO}JDZGg#5z -v7%?GqNkxE=pl}a=G@D>IR-1&1V|C;0Iqpr2DT>DSt?+FUr6BvgvS6&Ed!8d&4^3?T@^gf0FRXOR0oM=3WuQBO6i>a@TzcNcvJ3LPbHoY>mtv&{BoEc1dYS7V7rlp8F*LeO| -98Qei_N7~+piTLQ#To8*ryNzz7$>>cd#)5ECCLw(AQBu?lc??D*5?N*#3q@IYa9{dv9nR(m?0No)&UD -v5_tEI!ur*I=FhhSPGmB3e5h~aDRYPQxEm`x?>B$zcI#}UWoltcoU+lz3eVC51UKy>HsS~0r1AX65q3 -)18?NpFTy6pg|#;jy39H4XH77YP0_Qs!P@~BOrcsi`uyh?faLcQYgB_B -DDTG#{t6XTgyl6Q67V@C6$P*yGJrTN;ydc&cAAbM2rCr@Hw09Gtw}V3$`sC9$xs>>oM!K4du+j5V8K& -q!J)(@RN`@?#H_>;nHMR6NCUv59g#ty`pIcy&MHz2SntMOp8=%mXKP%AY;Q5m$4X;94#JDK2$Un42Vn -5TMZKeLu+j!w0|b1ggh(N>2qtDT&W<`G%M?q{9qq8Rkvm5K|1r-sA#jBch=Vh7@7L;mNjQ1X&`rtIk7 -58+MicnNw>W_}NaamZZ%4{D~=?4bjJ_AM2z|hCge5vls4VR&H{%VDM_k0_NI^NQr|v*M1<5fd -OicslHTUGG{#Z}&P#zuU!=^I3UH!sP9p5}mel~K{zBo|X21|1ABWiigqrV%In4D(GWS^#C&BJ|8z$C| -Oo%@WlwU1K<9pr*xQb}I`qUY?yV$>q3SnZ9VKx$iPy{)S17jrNMZMqLji9dD8v+w9RjAT{hZ=)Zx -==OYUyVQXI2&7}oz-Vu@QkV0+3vkkB)qt)+C0;*>kg=}l;B%G*djR{dOh -iK>=NA1ndHog|pNmxMxkk`_)%dqa+fMoDjAk)k=o8Q`HdWLXOFKU -(;@FNQHZwJWNs=VIP=rAAAD|fnW-uo56Da~aTQnBQLMTZskU`1(2;;mop -B?+2Lx;V(^S|@1w}S_oWzRktd_E6K9AXksNU&Rw+&zQQVmIgkqYHWYA(t$1JSW39T(P_6Z;saX-i=JD -nY1S3rw-hVkEwi5h-`Z?3L87IqX1E3)rXw%g**n(UJp#NoXxmJL|ed?(J#DrzAy$Z@l -25y#e`h>diH&rO7#i}&;y?14Dh0^3id%IEew!_sE46zz?}A++A|Bb}cX~h<}~xH!zDp1?WM -j>ry|#Sw(iiD3yk#tXOI{AabFuDW}ns4?HR0a#*EY<+nH|XSwJAq~6#@fU%dLBq5feJ^}De9&DmcKYP -ExdP_dc@Jg5i`FqCrCk~E(f-!zBU*P!tIUf`Wbr~ao@1s$jq)|0Gfoc!0P)I+a#{g!u}Wa8)%mzVWAaOao&&eIaQ5%(Z`avNlV*G{cNP!r<~G(D_2ep$kt-m$0;pIupYWkZ@}7pdRyRMP#qs;{)a7$frjrjdpeO`6X_Rmw5R@F7HGfZR4;DJFBjP -?i5pZT31dLHa`KSa&{*4sV{n7hoblSa3412LUma9J-46 -HB<;YCs6(fVtT$KW%K%Vx2k9kc@8VUPuo(Rkj@F1H}w)LRy0htr0`a3@`NafuCTQ{p!SmhPG%8GGF9; -2{sCKMhJd1VsvN=4*VfpmmqrMy5l@CzqQsF2(3bAP*H1h%YRj#Z&N5M2OZ6lz%eFiQHfx~0COlG2}4R -XPU{s9mZ#iNf&8=~6@#BzNP=G^xHvr+=`1-973{&^sxo8+qegV7%q2EvdVA!v+|k?mvxszkVc6LW}96 -k8oL9?3x8RwvZmMtFMfWZPw`32+1%L#uUJW7_+b##qTvR3%{O2NMJaLa{Cc2{2>zwvzDPQ7#p7yDKfX -GrU2UtSk{JZXs%W=dlME&e2svVX9WE -ZoO#mE>Qlfv^{l?bn6ive65#i8E7y2`h+(>>6@LEL4uejr+C<0mzE-uZUj{&i=@y2Ay^8pja=ep -BueuA{_>SJZ>U!;5o%HS;&XrP5oVq6vb}zHoA84}q;~^`eIL%~67}=Dpt`9mZ6a&-1O8qj<*1T572j2 -QP@Uqp^hi0a(ce-rc<*J6KDS!6#0%5ngkiW3J((`DZC-Xv>^rIf!rkFvCwzT9o}6P^8s54Od4<`>pn8 -Pp<;34ZlU}1aAKMSS*tq$F%1xzTmzM%o&QYUJ>Ig>dYS7P0K_?(u4r<_vy_bkMuv*D$;Je4zeh7vz$`SM=<{&ob`Phox;=X+94lniH;pWS>#+jfIqy*$t4LNJ -4$%yl%IWyz_JK|Gb9j@J`{v3*h|HzRox~jJ9V{CCq^Pa&ih@!w-fs}1v&TL1l~P -Mtr`{YtL`A)J^BeJ_HjALA6e{Lq+Wvk@131hD1^RmsBJ3d@fEv5=^v0MOXVFzHBMSV;(e;fY*Id(C@t -+Q^;t%?0y!Mx0t!70ZtO19u7L<@y$dq+&dOgGn2fcI#abM|D1ZQBP-~>rK1)VJub5vgk5HQ(@xI=Pec -h|WhP?uN^5;>Axr*6tL!>Ch#9i|SMT+v#hDdP?Q5cF8Q*a+6vs?jlX(O6ik4Nc3-3Jx!)&L4j-g-ot@ -U#Lrd#n9&5cliagt}x&yNncYe!ysj$gbvkSflQk{Vu9%V8*OM9lQ|g8il%DlNw`8InKW~??5r@03oiJ -q?HpR;{$qTBnhEEF_`Vok>FkDV>Y}_)7}NPyTV&AM$Yo0tmbhy03!!1rbnnts_;Txhk-1OMWseunA-! ->sO6kr&U~?Q0V)S~V*>3O>ix#xGwftvIL#+`nutZcxJ+S?bEiR~v(!rmhxFzM4o~+Yk-qT`czDW&boc -3GrQ8N$o5*&1BD2iu!}sGlg&J0MvOBZ-w%H(iAT4KwjWtIn;@OcKKqW^so7guwOYObeEb5)>g*JFj&*2G4Ur2ck+$>eShOd=7kU9z;sI?+O%XskNg -SCoZ^^jZ-fy6uN@>SGHCBi8N*{6pL_P=#T=MOijE9Ww))!1`qid&&ho+tfm{=~if+T27Xs-kiRO=el! -Lo2b_Wd{JsBHy3c3hwDcCoEYOuog=(IF+9Ft1H-uRK32W<1R^WgH>uDwj-Bw}f$Jv>?tf5b;Ri3Ic%k -#s5`^+@pt(q5BgI`FAk~PPPXfQ!q5j!RR1;vOcUZh>g*P4jZ+IvUeJ)!iplu(wc-2`XMF+a!uftb!1{ -Uk)Cx^6nyO0WMsYQ`wSJ>O-XT_4Ad0{UW0}gsm)ZaxesaGxopz`U(U8_JI_jc4~;V-U6*xR9v#FG(_! -D@;)61xOnV&3H1EX{7bTx`5d2DB^`Ea -mRerVy^;1*up%Rr$bmV>q61)C^K&2XlQcC`%Lg=@jhK ->-`=LX_L@BH~DX;jP-SwBe+uPVSC*T!2!7=Kjh{wD8881g9n=D-kj0xEsa0viON{+j~SW#5OjVGG(&cd ->nsO1r^vdn!{@5k+K0xcs%$9l{j|^lIZZijmaWR^q`*gN@F?t853u?Bk;Z+Eo3;z@l8dDzfzrXxF-r@nyC?v^LVKuU!&WA)5{z@-oMn+g&}Qb5A8Y3&lTNDf`LyDgvUgJ|&e -iYTK=Shr!0Q01t~lMIdh8?p2(U2^KhPLKeA@;k(x_IaceFf=X_FuTW*mVRp2Q3s -ejT?znX$dijnHn%(x*c*gII%&s8F$s40K=a^AzF8%b&FAg<7x*9LR}kEYembYfP%n*}s< -0N%V#KWoFO9f~}|5X2(cu-}HMzs?vO=adX=g>xakoK{h&GXWY)Hv5!@qzdF#BzUS_b7fr_!z)bbh3IV@4b(TcEq?E!r);VwNJ`s4lvmHU#TeJ0Ia&@+=dlFuCXLkp$WWg%dW%NcFGj@qXM4w}7WFgMJjsDar#`Cap((^lO8XU8lz=**B1+?N -6X&MmuD=QD1y79<*71xl;$UpLamZm&&beJL$Om`*^W5*5spEV&LGW^d!tGWd*wO4k+~~)(m}eNWt^{a -3eh11XUKQqqAt?FI8qbCzwr1vQ?dSwaK(;Jn8GMjA`zMOog9im&dZ6$mUs-Fe_U7sn5r=emn(!+Li-= -sO4AFe5P$0hc7O<63>!$?W8?=?TxTU-+Y|zpNHDi*B>+Tq7qI$k%M(UY9yhlg8EsGajL8?no3(S6;^C -d4pIZ_O_R(ff7p8SB*YC4CtO(ODEL)peu;C6GyyOL7FdiGuY@0MH0xK^xC%`H8zkQYDZAC>@VkL8utc -^b+_pnqx|~~aM4v=4Y&R{pRCBnNpQ!yL8xQ_-cG3~-8q^L!d!?l5n}_OMG*x-GfuA^*NYG}9NfxCRk1 -L-Jjv^jtEKHVWXv09{BVwJA&FcYK%#VA^gG2|agUzQvUD~A*3%aNSVXF<>j2MqfgAMhTSe#kErdq5EQ -PBtaH}L$6AhpGMJtPiEHZ`)Z(L0dm4a2*&a?R7G@)eJD2Y{x}eDVg=tWJOtqn=xR{&9XVzL`(n9^G*lB( -pimBH8^=UG3^KHoS$UWp^g~%Zq7Br(nVqpXEJ2BH%s7S#(be@|lob2l21Tjw7i<0iXa7129{EMmCIz& -b%YQq!s71yy}$Qd7TBLpS(_r%(XEQ#@0}k%FJ+iGD8QkyV33*y&_Bd0z`021M|1o0WYSoWi1(+#REJZ -%$vkMcHgjKzN9xvjR%t@%O|YpTbQA$0zPXirh7w2WUI%WBTvU0_tG=+6vb@) -_@YLa+%^k+uC6v1VPXa`Mh~tK~<hKfy~rn{wBThOO1nM!3 -Krk|O==XOuduhJPzWJ}DeZ;P^^~hiTLZ~SfhU!(29z()DD6qY=C4z=N~kFCAXFlz>sq-IohQ##qC@2w -(Bu`^_Gsl~iVzwLFw)mz0@M(=9$Fcy6?kE_bG)oo*xWl*%fY8?dir{(mTlzBfSiIR2%EpkSYZ{}ek~a -i5=>KsP%>2OMFrm$B)7?fGYxxSHGr#FZz0%!TU{rUq!{`M@w`nbiJ2=D_e>$QlD~;p;3%XDtR_x_lfF -QeG_7VDbYr-78)TVAS!_m@Pd<-lu|t+&S`K8nl(M86S^h~`giso9Y$#;2pVJjjL7ouemZ5%BBkl&47< -MX}h*hH+Kv}`Z@jhhHttUpoh6Z#P4rGPB<2~Vk%^waxLp$JNhrcOY$p)-*C2~Tmi2u0=d>edhZr*{&9 -JBiRbY%Q=RC2p#-ce*W2@y1xm&*G9?Ouw8%F2u$lUyNzG~HX6r3B{p6=s=03JUlmv(GC1_LcU?T)Vm^ -m#ZuPOk;OMuR$z7@)&T{-}@37!|eC2!IP!r<5&l(t~=n>C-6$MN(gOwOLR31a#Ja83w8xo(n{8`*56= -N;f#Ldf9B)K%I22@#N>B15eG0qVwq1-YZ)iiv`?w6rP!9@I&IQiP{9KJwjaytj;-5}N@q*do$;rUjY2 -j&yQZ8*g~}G-uk2)*$r-IV<8@9!s;|awH6`$x23-zkM#q{>O@lJZeAFOD-f-DZlSx#Ml?d7lYs@4HPx -ndO0v7G04$O-WxIK{5DoqFpa$cuc|1)gyuh((3RVhWhAY~WZxuGEvZ{6Xgk-TCfi8xHZBV^6Qr%F+`Iu@V4Amcrg@-SJ8z -5@6P^JN~QDL<5j?M|C5ujyEIWAH=)%$hgy-?03={i9iSvja&_D3=wu$a-Ti0$)43T^H*IY)2sSvr*dX;sG};t{2P*v -gdpOyoXl8a+_3o&@fbATZz3&?G%CGEZZ&!S}s5mYW?90fuY^8pZ)4X|VJ2aD2Zm;r-pQ_ao`Oao+sCd -S61Gym%gvdS;D9ICG5Src-jVKT=|c1#?4}#BfsFP~yAnjcN{ep*>YzLor&v!i4yduTZNt5_g5~jM7lV -gTRD5E6jA32lCl{Esaj=e~r;>MxG6WnR)#QxND8ePP%y%Zq$)8NmBaP;~e!oN4$SGPJctLQz6gADd9wn#V^S&3(MfN7j(2-9?P2a`@y4AXR2_@UhUIO(gT@ce4n^c#GvqnI7VbCD8Q0olX4;ZjDz2 -`&k7+9)`vGe6XIbq$aoke*?;*Q1i-N_8nD7#vLh8GLuZlfU3qIbCAjmz0<2JQCHD6Vebtd8_dlXqQ*0 -nG^_s!v5kWR1$e2_q3wS?FSSS>J@6YqRFpNx1&PLX{vBHu5sa -I3pF-ErRHF-<7DF%Hjlz#@&`%-gvLK5m3c+UN*!qivphP!bA^3N?nJolgr<*&4U@h^3r9$xWB)qv7IH -vvwgy7HVeYp?>JY^!qyN{*#lx7N+0M*IwqyA0=NRY#b^@=5J6RT26ICXDyQ6ETSsE+2hvNe1N(%>b0j}o~u#_JqZY3?A=}{1ii_ -8C^kXKfZGam03j+vuSi5V}BOFYE7FyiLg&Td`VD&_l?{uM%S4Uo1?0przv%(sd^gqq+@M0`{8!UDtwj -;rt@di}5_EkkbyKGH7R0LLW(we68|nj{VhwNP}cokPoEf!I>q?ILMO#DC#7^9Da({iGQ2IV+Tg+atGk -;_==hoYDuia*@;@nBz7hbNUyW&&O_x1Bd#-Qe#4e;8(!vcq8{ZaVe}7;TyE1@C~s^j;^=xNfbAw#hXT -3#(HGAJfUuRNHK?ZtpLFxqpAlVu&OcC5JHQbS}bh6rC%Elm;^2kN=qp&hx=qNyQP0HVwUT8;H@9P@ZHfbl!g~ZzR_E6h8E8?(;cr4T41M -Jdw)Xy3svp}jVSes;Nt_6VKrM*Ks<46c=K)8T)(3@Z#yrD0I;F&Wh3)BObY?5vH79pr1!q#`eHmS8lK -Zb|EunLbm8W0UQ{4Kh2xlf8)U#10|dlIV@-TKh7)4ivrH&9YFRgLMKtE{c -B66O-f-_0CvgWYwpLDYPLlWQmeJP)(U1IJZ^aoIX!ZAC$-l45zba)>_PVpfkjbS%?z@^~Q-bKs>y_;~ -F$~S}}`|lT986zsUgyW!jWW`VJ~RARZPxJKw_sw)`ysfV;jF~v4MRg_MX01-Lr(_+;TP4u -y!(OEAYsBF}JB*3Dx;5XK@$xuG>sK*QU}%*{h?4y*81$K&`(=zE;@H-6BG#PcO8*kMXwx!6N^%wvYNdar5E_%j(Mx( -%K>e;*V54_|p-8;(MMkvOOq<^xJz_>2axmI$Hc(Q9sN`QC7zLMAN!L=5WJ-^sD;&PeGhSX3fBCHt{Vl -w#M;Z*Rq1p@|ABjII__#d&DB~j{)`)bFr#uWP5z)yX?ltpH3&9ey_W~w?7Kqjc2EQkUa@F>m<5L+ufJ -b_wa`uYARFR`Y8Z>P%+@?q*P>ZRw>qH<>TNBCHijx^xBXv;P0o>N?6gKIJ?xp@>g;$Ck5hhd!oEa=u_ -)SiTlKZ+aCHfNT5aA}0QBC!Fpk7Q@OnlWucawaR(mzSQW>XGdwMm<%@tctZ&hAFOQujioHfj5;!hd=NM80C4uhBsZ8!oCOxrT#_Bj7WbtBJ59A0R8udaLY=M#@u{E}g -_hA1Y23@S>4v)d7U{o0jlC82>N| -&bxCve7(a1OdZ;76eyA#f9e0T_W3bhQ`gO-TymKB1!<`}ZO+^SbwAPH}t^{RKT%!2k#r< -qNW5*x^sx?}ENxvVu!nfa?>sHz=r!Oqt-WX^d3JsBL?l%}m-YsZOOZ%6y4WC~u1_wgHKQAo -z7t+t>;#sdBgSo6=1Y-RS8?S=HCJvd{MHon8zmX!DDx#7?u8%z1zoGnTuQ@u2zVUfcU9J!^#*9v-Bfj -Ay~lM|GjIz|G~~QXn<{L)+w2iGHhA3IR*n*LfC4q#8u2Hi8>soOPW6{AvgS-$pBiikyv=UOAXNu0HHs -~{?3QE@@A>IPs1g8$wBp<|)2X&SO1hRp6J%p|Wk6cK6!BJu%ed416hHZ!vZlN?JZV&ax8x$(L&(>hTS -6q86r<2%ONi!G`gu+%XTLx4LM3?v-IndoX%>4eW_d{U5c_&h+Cgn7JSNHZ>LE^Xy~lR2wgx9lOKq;RV10PvnyhrSIb`t7O~*m0GH{^i|WT>nzd -^)FdIwgNW6Q&W8G;DV<38J|;qjeL;$W1bdQodjSWAu06@D4U}_`Sa+!p)K2liVXF>Y&wp8kvnwY47sy -U_d&*42X=m%7mS-1D`gR0yfoo@`@!pU*`u-z7!<5WxSah+ -WJe3JAXg=kiedaX`%6;!TFekc_&^67!Vs_^CYsY~Ugh-J!0;C9mGPVtAgA_@a{ -5eQBx-`3Emphnjuj?rZ^dnLp}&R)gWEI{m1zTM(MqG1Z61DhF6UdP<=!fEgIV=%^$VV -5aSX&5Qovnh%KjKe54rd?9!l5>d-;pXD&BJCu{ws%VX_o<)fi1&$g$O7}2@CZVofcP#%C-+y14xVq2@ -oULP)k+_2rZyS$_N5m{@0eS+hO%C6m;VD?`ath!7p@|<4UY1|xx=fyyUuu@82pjW-jgr}Ya-}1u9qPO -x8#+{-mh7{<%)vE@Tw&f&mYFmE;A;LxzA@+9S(WRUW|$&}Oe_Q2S;_|V?sLwk!4?JXRdKW9HoH+0rM -C$i>>Z#SpV$|VaGVKcVk%XtT6I{=L;bc%04V9m!}EVOdESW3$-}zcMae=i08_TfnS^?t;h488PbbFlL83lC=lir(tfws23RoXE2Do6mRohvtMWy2p%KR -hD?2k;|`i;rdqYnuTGm?Dn-MIhT&^Uy;(4$*=zfTTZLg*A+daZ8(X)*Hy)-8wuU=mJb;puo$a@hJZKs -zh4g{{A#n~bpa7kKlYKVnV(qpqVqS2<;U+98$i6zV*4>=j}-D6P5&1e0yrXY4(EUvmmUk120SjT%8t1u>S?50HCO$^QQn5aTc9P>-#M=cWJ;=z}(T)akBuUDue!_hvw!x$ZC;GDh5Y` -|A-J@5d>unQR=sicQY@6QWF%L=TSu#A^61o<9&N}Zo_nha%-u?WT(Yp+zcbFxOcP}&6JtK|xZ)dE39& -;`E2D8Qs{mW{m(3|(M(bZBU^H--!{HT50O4uN$T|K#j(n`$y@eqH+YSfSUeJLIC -SeGC{0#ZCy*@V-8{d(jf>I-I2|ALt#PnB-r6K$P_*$Za($ipQjkYy5%#j1uepR%qWIRrM>mhfgdy=ie -G@3{rV*mEt_qH}kUUvu1F3s*|S@-zHwnBUzgUoC})p>q5x0Q6#bInf6X|ASf*%j#w81Z2H|Du=j -4Hm_JRVc;`BEUa$HQEo4`WmwB)U2vAm>ml>-5i{3wvk6O+qvQJw-5*eBZYanFjKt5N!i@Y$pc!oztB= -94xshl5)9hm^rsZajfANctc>{pg~*|&Jaqsy7P<2a-%nIj>A92=w04IezmGC=PXxyM;wYd}`028BN50 -lTS1M9UUaOAQVK@0x2+&@F1N>B9>YC_O|45TSCesc8i9WeP{JM&h5KK|YAz)9dhC4P3AZ3OrchGxV_=dBp^C5j}CMUkx!)cWm=b -n*0hN9zNth;07q9+GDHu@h~bLTSYksWzH~q%D89#0k*6>7HXN9Pt8)*U1x~ZiTi2ea*+c{%ygc7P%7N5Lhz3?tHs?aqmD1=R-PJZ3S2JK%?H_NYXwS -1GDq_mB1d?Qcr4xV-Z2kN4CP!s6$iuieLW;Chb!1dXzukVq8C(04%evVI9(IzOmR<#GvO$?S>m)L`LF7VPtSDa-Ow~@FbP_|A388lg17| -K4P@VqxOT@KvGfia1U*>ObqPHdVw%;(`+R{N;;CZI1`6ezz+B)6b6Mu%}4;nj3G^=!^DCSQ6#8v?Pn+qSlewl -cSm^J%LHTholYO(C4ziXU7g02_6HNwOF(J`K0g{ed(w>ZC9JlV4x+S17m-KV9Ck`EuRV#ET70UBU(ZB -vTzT}W{#E)U}&+tHdH7@=g_O&hL*^~wnYmD;{NHdxBeTCt9Ge{CW=Exro~u^I)ax2QRALJQTQ0v;ZBo ->l1sc@=^s^fG!M-^vJospfDAY2vM8KQ+YyLl$D2C)JlwG0@9Vs1W=bb=S99)Mu6=sx@ --~4HKSNN_xdiW#-;)f~7Lj0h~b@GFVS6-n1`zq1*o)X+bf}`uv;N;2sPj^ic;i(T0-RNIEf{_BCdtSV -|S)>!ST+J*qo~zJQ}%iU3&+Hp^l-f0nty3JYuh?#e?hsJ(={M&`!CDq#dbY)5w_zkrqhkU#-NeOEnK*defRgQ=w$UoqcsrHc9fOra -T7?|+0~L>|o_=koc;R%X8OF+d0ij0ha+GT3w6R{p`WMtBXhXZsW+oTkC}3@o$b6cVOmzv48=PG@;&_d -aOgGH9C;(^;=^Wf?w?HHSCwMaE%yVa_(=@j&F!R#=Y-7F-Gd@ebnFP;0X^iIn<_ZLnfi6FMsF31{AfP -wtKtUf5hiB<0pVy`p87Zc6int1NJLwJEK6)^vQD?WwIQg3UA;3n<%4rO2&)yc&$swS#4Tn`$;ycnW0K -gF^6HV))T=0QVOv)%@kow@erbHnJqS*z!d|mf+8F?-C+Epb^Y`4+>N75tbfI;`6bDIbNhtcD2+@IMwo -QnBuyDl(<5aA|<@T)el}>eNLxy(-JzIdSPeVm}559qO{oDZ?CDUKgbu7-<5On1ba@G02IL -3CUa!e4--{PpV5pB}xO7M7FKGvPrSfk8$&SRXzeKT3cPL*>M2}Eu>5)>&a%H2BCA%<+gt*>|Y?Qs$Yt -tRy{(@@;|)Gy*)xT3&137UQKQY4Z%-=4^eyaUdNRL=P$cDXD7Ov>T-(S0?5*(oF2JmI%IysN(Re4++caabHv^~#}kjLm;Ie=oPS^?8|ULDy-E8q7q+^ -QWoWs;Jo$pS&|(%Y+WRicjHJNLePR~w3;B9hkh9tz($`K?|A`6>cBdU3G+Wi-%SnS;2!B;B0okEkKSn&){R7yc9g>Xznmze9tnvX^HYg<*Yd{T)iF*VcjkAB)u7$cWL9ip(BA&kR=ILaSVSc3Lc%J2Q!Ce!8%5M -2GyQHxvVK#iI!osNN%!*IBOO>1AMJYD2gwF`y#_)AF&Q)j;3)tuF6TABSH#N`TJY=S^vz18Y(qMs~-Xxirkj1%HPKLp3OaoE>BAq; -H{>ap<NxPM?fpR6%{bGr=05lnmNbU@1}WvWpiDOfx9c-+syi?IBbz|Ff>E*9?~J -)iQnIva=>FMVb>`(3sTk{#2w8j>7ClG{QVDN -$d$4&=uvszB(GSX6-}iBSa>#iIb!8c_w7saN5IVN?NRC{-ssg|oD^>gW#gnw{}TVn~4{i6I3BD?`4f* -)vf$B}Nq3-%j-g^3>gMhYg)FZP;}{cN8+LS%)u;_{N@zAnFIpH$xJpR~_c-UP_ePN6>}n`D_|>lH16oFQx!ElPKf^t|8 -F>d)KhV{M#VOncpkeOk;!=*LbBsu)IRi3BsA^L?9q<-pQ*u~Y1*H`KN^9fb?ngZP~fjNn9(vKPVJ8u7 -L9H2ggeYekk|>yJ4`JeP!$-hg44A9*;44rV!n+gK=Ps=wP4hl$Vf^xj6ZJSO~8>KY~IDps4dE#pI@;BoS7xGYb%$tMG7U{qaUow+0ARTi4Pn8LsE{F>cMo{0KgIq?=QrPC -R%+Lrca_Oz%B3tOl+A$us1B(B=9M2E5yOlO4^dNdOtU5-onxqU2$P?&`3qa`{Ge9=Q_(vqqw1GU6u}w ->bz4XopFiz3tlt<@vvb_{{#`^>=N53uvzo8?dIrjA)S8lCY8}%=OCJavmS=kPC95wAeD};7HPGKWz+j -wM%WrMK6d7KN@n>G5Bep}U@!p2VDCbRL+C%m~2k_)RQNPX`spQg$w{+vcBY?myp#jMq*bl>c2jHUz%_ -(Li@Sma?96%`vc;K7&DXu}wb-=ZCITJ10+T3~OJFtN~tnc%;#<|KYbXGJdN58n>E;dVQeD}-wo{B=Wl -xTx^g1AqO2F70QAKaS8e4E~@5{O}@h(}h1&2(Bvr^x#i_prfZc!)1!jJC8Ss1-s?pcBNf$1oJL%xzQ1 -*wH32oRfHe{z^jEq2o-v-CIRtpb%w>PC`NWsTFr3FK`T{VQ;DYl5)%Q`&3I0M9vdDu+acfL2E3T~gy|tGdmG4Riqpn^?T`K& -js{{$nlGAIV+ -DPPQm2SJzU>`C&OL^E9i{x@%oKQeWb~@&`Gr`G+2z04b-YBxJ7H>C-w&4YzBW@p~o@&$eJUyJR?rV!} -_UMijAYeL>uG-(jo!dG9QjefK&uu>ZtluBWJXovizDC`ud^vFr`{7CF;~-)z?JZsJnQnX4ECiQ9-kQ@ -$#Gf3kJ_j{J1C?#Db&W1e!O8jxJ#ueK!KD;J*QXGXl=peoFPok()}vvKH|dDjaaI(z?D5n-wn%T2M*< -wDv!=d|SIzRP%E19YJEko%o}b^RiS8~@nAHUcBiMtI4h3W#r}nkRSSK}u`ap*o-5B$@Sjy|9*jVk$P; -HZ$QH$le=CB|Q3D9nCVeG-j_KWq%v-Ns%0p5eZg4^}}+#%wPtwQieCepVDF=U%qJ2X~CRCem3jF -0(hXsLxaD$sOVi9+4l6Wq=9_paS{!&;hx)HZ5Mx@`0ppgChM{q(H0&S>mdMGB;wGY$!8kunaG75{hD( -8)M7jj}(9CK_N6_I%)8A$HXw#deovdU-|qGfi{TgP!8jCN{)uOms3@8-RtRF$CGp -1XawZv;`t%*q-Gl+Xs-(C2-Rg?bzKGY1vAt3-X1})9&(tXHRv1}-Y-GUZmEzEhSOxkHV~p-v{?!HDvF -ZDCS5ffvhw=k3uBqSG^~6C*^4RL9OVi|up4v&Vn%8?vYdzD4Xl|eT<3KvT4p^HlX<7~djA$?qJ9YJ!k -D5&Q5^8-rr0cSPJKov~w@??1$?twl6`sj}ZJE@o3PO`EB~LS%G=E*`Ey}o`Yd5f(2P$Mji+}9LOsSARd(s -o0Mx2CxVJ%_<=Z4nm$u;C>2O0+wE)-3siZZ-)>~q8f~lVA16LvFt3xoWZ>uD%Q+iFe1L&s9lFC7A|bv8I*r}k36;5<*n(~l-X06;`mIK13x -#J}yq3?yYjUONAJ9U$b2DyEIkQ4681-)fauEUKLxTWGUxQHk0Vc1)lMB(kUA7xhXc6+F+#$mYLUzLd0 -_Io%%%q9}dteB>PLBQ$Y6t)}Ke`Y$yEtr8AS0;B0M6fp2qcH=6@ -^Sk6i{1nG}Z&$1>1l9H4_Y)oqVr6UR<=&|I5sK~c@&Ri_c!YKXES1!<4QT-&K#RKfVSueP-g*kOPA!e -nGGb}Ro*DeU2*C9s;TdFZv`a4v*BN|D`4n={h6O}0D0ZKaZ9d@7e)(Hi-qQo3;Q5d>Y4_-kVG7b%E8y -$dO)*NOmeDQ7R)~scQ4D%4kjy^O{sHju0KAG`c@~fPwXtem6VaHA!J@y|#)T7Qd+DGzzbIGTp#7G`i1NF=)2Scj2CyaE+>Z$to!Oa!o^+GM>=R~E+L&yE2(Y?J4{^qIKFY3Bl))4cLeu?`>&uNHHd)L;OfAsU@AH5;LK -YDX2f>*oVTa;A@Y|H$0AZLVLF>$kt8~7)v?0X@PyG5RYoa-cic>`p;avesiZWWd8i5Nh(cxgjvu_f4s -8(720x{zhaI&_vR%wHFnilrxD=`j>@b{htoB}oZ`&Hk*Bw*9R9YWYg_XItalzT`W)EwYxVCDrgD_PCZ -H9JF}XJ7b>R#r+hKS8(AGWNY`V~u8(# -e^%>*(8cjR8g9l38$AQ{VdgCe^j@*;5WoS_8Uo8B11&O2F9a|KFXi!N`-0zwUC+ -^G4h7)%--P|e5{~pF;HI_XIsSexAemkeX+FG3z-)*MDwAKkhTib#A-lC${uu?ZV=H -?TnJA{qSqcYAB~p9{G~0V*L!$vr_%ojXC=u``pb|Guc8IQT1LWe+zEO#cY?N4T7nZaJtz75&$eOyll4QMo*(|-Y!<3c{osnP(s}+FuI0S)#xZPti5c -|^?Au>XrDxWtgxEs_N-xC0ME;Gj*D*3&AUzH_6$4y4EK1h*B#TJA~V5i4}QK1ou2&|wzV16fN<-P*xM -Kj2XfS-*YjD!>v;nC`+CTy`3JjENgw?l)-%7F>>#M)F`ws!^u8_eeYJZUxjP?;xjP@YD(*{45z7kZxy -pnP`l8bMO~=);tw=?Xn+D6&yTdHo@gXM`8^Ny)gVZFMvN$!BL6V1j1EAVUPtIbfH0LYG67zkAF^cS+z -c|$m_|ZG&eJSqyTmr9RdgaUSpdQ%dEci`6w`i_9zn{E>C7O(R^1~s0ZMnpbd+3ui&o+>eQSqzX7GH12 -Z_%jVd&t-gfveg4S3%I6LTzJwT%!XKDo3rqc013k2j0-8h^!Bt&kHfQpwV02QR8Ei>QlETIzV579hS! -p*33uyJN%#DBSr*XY`LmxB#YQTHuB$}KP$kT%6wuGMm5^b*6G>QT_DS+xB|=!dA<=gg83#1XVL# -(OI8H>~HQSNFNc3F=`0&a3@Vi8w3ct%x+&;51@FF2#?Rl{}e_0?6NFzRHXtjVD?AkN%>!gra;(CsjW? -Ua|h}Emi*T#LTj|kCH$S@<1zqls;;)Fn|(iw||;0<^M}ur2Wlo1k`ZNxJXy&rAnF9x%&pF8L{{a^TX-0Cw|1e}K7 -bj(|N1AR7E=kYWY40(a>(p$F2-KCdP2E$$Y!B{op3~y=V&d^8NhcA>p$$ed#fMF`hIT~u=&e5gOsl1? -57U6diY_#e#Ir63OJ53$|zqvvvmDga44xHsxMNjY>bM!EMTzB)4)aYkCDJhD9*|1Bi;*Qa!_~ZAoL$T -3tvi@hts9w7<&R;eDL)Q!Q1J6f)gP#RCPlsCmK93EyOTA(NS62t@YTrNNYJA}0?*IfwfzioiX532DRb -xJz?)XuF8)PA}#W=E1PTNfm7fr}d9)uc=?*$lN_o-fNvz?8Fl08=sS;qB{F*e$et~~5BL%PaRtD${q -)mRemHLzb+|8v|A)UDuNI*q71%le{9-8S;uyhw{p^$hZ7&JRY-yTpmq6 -pG?wW^bMam=-bgBRZ_F@w^ebM)-+4xZ#Wk(XU9dIb_D*_`o-AocLzE^cTDehgj8$)PXP$h6d$TMfmsP -I3>n|g4g>Hi|$R3(o$#pNlhxdZ8>jZK-yLtN?810conCgz%XT87NAZM*o -4SM?nin)Ed-cd|_jn@rUlNZr)hX2WUnJ7h`X3pQrZ%1vX{u3*WB@&qQ{-C>5vACOJ=JeKzhYcB7+0@xy_}0Kp_21sPYtKoQ`f~}PwkG0Ew`Huv1M~iZ261 -Xh&|P4GGb3XWyGGEOQOrKW1`EYX4Ky;b9v&inwANn1u3yuO;=mvv6^~~SWRVdekAI!UKq2gASUCzpbZm{pHZZ7 -7QT2+ego`J;rO9X}#v#IAbHb@A9$`=f^j&TTXngi53XLyAH}BZ;*vUkd`l;#eH2ht7*bwV2{ip*Doap --RWH%Lao`dD+?M(*qYqprY6Uu`pDu2T5kxIZ>#j=nFBaDt}-w3RO!y3RRlfkgm3y5~5K3&XBMkAPMXB -#3)p#2#rUfI+27yQ{L1hm{LA+oYirS*!VK!oBwSfs@Kn65QvJ)R)`F?P-v;hBgky6UW0L{@M{UKGL*G -&dhgek4o0H7opO89ztsHa1fshAlDODaokD?83KN1+rGoU83ev$clD;;a6O4)~6#tcbt|0dRO+>087?H -|7Aj}=WFh-RZ3rKa2K<2!tKxPqR64~1n^k*<6l@tp}n_A*@ -#E=9IT_no*%CIpuxMx&AvqETJF)$|?FsGfby^#2-->ev<(y5>;@-x-Z+2W7fCG -1JAPQO)4Mgm}ryaUo*G=%G^=M5QW=o;r0wRH`x_mC8?1sr>P%R3w{SEGiWVSo_sK^pRvX7?r9lK{ETO -kD9qiR4SwNl0FQMeB?VrQY|_sB-M=*E*g?*5f4fAIq;-|A*ra_+BCW-JJF%OP@5F*C^bJQm0j0Iq#I+ --hB{X*q_9-?42GpLI{Q{H9?&Ab%hQk{Xa}%w|NVx-N--ea(nzr8WM?P*u -a?p{i29D^%6jW+PP97iJ!+s>}SJLRD=v|C>-%zc8N@s%pLYoKRIyna>GTh1lubP*n@f38AX)Fvmhwq2 -kR$TI%ydRgvaIb)BF^i0+MtshV=JFjW##5p)7!1CSz-0mives-$R_DITRtitaM~+bC5efua609;K=@i -pMK)K`b7ns+7K>^M4hk>TZfsbvs3=x`m=t74j%m1r((!FBYO|S)vg386O#ED(nbK07GN|hHT<_A*%Xu -)UPDwhnT)RvfAin -*n%BTB|LU+|oG5?l{FlmKL7l1uE^$K(EZs`&S{}8dE)tB8y4!)&;L}fL{W$K;%9jq@Uy -&)ax;C>ts$pTo9$tHL6X>Pwo^FhDfCqq^*49Ynds%JFW$?-d=M#w;5>5321qqVA1y+us9}I!^G{h8CO -F3~LP=x^X|vExcA*jftIf~$c?vS$0vZxn#pmObKg8!_*&mGg82k!xd_Md+od9pi!!I0m+J(c8e~s7}_ -Kw)Fjd07`r$jwuKGrDI*(LfiOlRO*3EuDPm-NLJpx-bfy1d7WzA9r(Oz0y(r&qk$i -BS5*n;Hy+m-2@XJh2F7BCd$mac#JfY=|8KsH4Ki%DT#r=bbD+m4re)KS7NQ8HerMM{+@3fX6*9dm@4`N;Fm_6*O(TvH(!Qg#z%%7v*U$^97>Yi;CB2VBM({3r{&^1Gm=e+| -h0=cU7?PlJe$1T9oF4&S(<{it@p;TL<^(Z4A!MI-i-Uzo{n^5dQ0-53@+?3 -*B-vYCWkOP`RYp3wt}UUk?ygDT~aK_DB8O -mKeI~Gy&3{1SKg$+a^WaVclmZT~&!mqg^_5GWVi`zvd62(*(zVXSR!`j5n2cd -Cr~ODA1c(lGe4B&hJM$H#AT6x<3e|B?;)&|T@#T7ckCMJWsws$}557bct_#yThDdRSDX1!444pC%LAN -P`5co_XaSv-8-m-HTAvIj-55EhFIvVIr)#g-}KA=wZ3QFHgm*q)s;#hS|y>7gN3XElYX#F3bI8h7|O1 -GAysjwml?$N&PUJfG%BWdb|G!un~f-EH#j~1ceXH0hE*98-QOGvr&25uQT^i_p)>yYpZz6BG@z)`hE25I;$ -wZ0Fn1DG`)*>!FxnD~I39o}7ZW)w^;b+g-wc5@3R-s@(E=LDTo3nt$0W{2;mIBzMK_?kzX9i9hr_AHq -AD%io8iJ8!w4wXhzoPH>G%Z3!)j(+>$HnYCaelu)`foNZ-lzdF;+*vY#Gg;S=SBJ>&R4_?DAr2j -iF<8e^zsQV(xUj@g>u5Nl}2u_NYcxtwL7L_}`cJHLORH>b`l|L*2ndpBP#FigRZ?nBC2ZW%Zt&7(1F4 -|P(>Z0@7y66pqXooRN?V>&FAR@;;h_v6!tb;af!b9Xk0$!$7K8#d(EjZa#7tKbnj*l?B{TS<=U*3oBc -WtE6{hq#GM5r8cowr$0^=?X{bjd8W7z?e)#h*dTHCAafT*mUTlz8AR*r;#wV+T2cqsoiFjeC8NerGw% -9wiy%Q6+)uUh&!Pxy?sVs`Rf%q031*bv7NHdvx>wu5m?8VqciOy%d++pi0&P&uDWpliNqcfLG4#(^n6>qN3z=__Jiu~9mViRV0e9>`7|KcXGn@}3B$&V -T2pf9laBk78ppd3=;=>})8atzb=uJl;~S;Q=ZwBaI(m*-fa6489ONVgp4Iv6Dc=|&0G8cx2gHP{Yd$g5~6HJ`~WBqA0jP*Z$8 -KsXYOWs!X8DQ7;VWrn?AcPSm^y*T2)?!=@b~{s^eT^;{B4vrgQ-HAxUqhpFS9bYdo#VcROB-p}`VO#{ -=#>FN?4`UVLYf6H1zcA0<_M)BmxS2Ub^#Suy>;025PgiLRNL4Vui!RwUO^S+VL?l#IA6ui;N7TlQ%V1 -i0e5<~z4$3Ik!0C*?l;9Lp}m(<(TpXlFM5HE;2;Va#1Q@f(O^fD3%h_qO^gsxhcHq&eT7P*rqWrOsAD -Fu8cu~CCG^Y^>=a}u$sBCppC58bCq^0r4UWNJYCvVvY?o~M&=T70{qd4?+Il>|^dpsDR$!Dc7xJg3+n -1<~-2HJ=sPIgx(__hDH+iPCf8R~?N`AuyxY0DVm)5#Aa5)cKJQN -8r#`1=E-2TGUtY7=0rXXI@HZ)nb0qO$mKlXtRxW1m&V$iVLrh2Q=3f)@%69U)TMvAlxTn@4 -wD(e=(xq0xl>-DaP0GjnXBUI#(sgVYu#Axaib%sxst^BKsPcmp-%Kn@cTtN$#j*VkX&vYe9VYQzPMO9CK}<3k;Pqje6`SDk!S8| -J2LsfL+n-A-QhU96$*~fwGX$&0+ZC}>>bxssS|aOB!MKqHR1u)IHzZV)F>sUm`WVw#psr-$^RRC$2lMVa|W23HvA8|~tBV{ExNaSE<{i?> -W6sD<^WDR_5995DsY>59Xr5O36n2Xo*44dJN$LHg3!!}uni;dS~O-^6V$bbfw} -AB0R68gk^ZEQzV}agO4Ng$9II*YxU_&3H{@`~(g~zp!6|l!r=?{Jtkmqgm=l)>m1Al#;et-vwK7jWF2 -RZXrNW&Aiw+G5W52-Z%>USu5V=c=c11gb*U35;K5NDDSjtEj>QWewAn^1!%wj-IM=0w>jePCj}*@#8h -@h8;N3t`0)iea>BrOLq-u@#PZdICPKs8O57_8YJ8Cf -BsKkBuRweXr^XWvchAS`ERGlCHy%V%;VF -6NZe#3Ghqp2RRGiZn)oEw})d|x4ddXi-iN)R>Q~KtRni9VT0O=+8wP13XtZCau9msC}on|H`-?+UhT- -cUSt1m`TJfTR@m;4^rUW4$*bL7(sxL -R;~I&FV#TN~UJ*`@d3G=M>Zewhl_Jzo$EH`a+tjQ@n(?!#g=RM+@yp5g|r?W4pmD -5l@fRej@eQn2bI_R=uE*eaWR`}9R}LT_vyUhSadoA?|!O~ne@dE;y6*%2sxzE8@A}C42s) -+a0oWJe(~($Qqx7K?YMICBF-{3xH;48zm+n&(OIlyEbeJy5Kn8Fv1~V`%KRry>3Ki)W0}NzG{eW1qz$mfIL^!>tDT1a+7^`o6ZI@r^PZ8tH+dLcQBd) -#68m8^hNTm?||_Y=H{KftG-50xs{+E8BPo-(ZrCgH(YsG)i3ZIXvr!yAIJ1I1+|3eAG`!(*vz3p-V$U --e&iN@s6;hJw!WBNiTmI!*2cAaYb6MG^|t<*D6ljGNxxa#wby{^@$lzX}A%;B#2QFge2FQmc5!|TuY6IBC!Bp!hhq@VTsF6oPl=~ -~Qjc^@v$mX0iYjB4%v=uOtcj>EgNyH8ME26&kBlzcB;#66|npv^yl(-_wc+CEliNo*^7>I~^hE~=i_()vIR*_)H7rVe!%1$aX&2==FiJA&z*_a~BD3d+zcwY{ -y-0fJy)-#IoN6jF>Gz#vPr}MCPonpQ%0)k_ag@HQHgQR~Ve?d>G&?+8nXOybQ;_s{4^SgWKKj>ns2-r -i$Jq@!YTR>`g>p!-8uk9Zh3{`1jfKz~tMvOSopD0iY&^E!S~6P)bv_j0$tW4)3>DI5;8BWJdc%1gu(3 -}@2~0|SJTQB3*-QH}~1{1R?MEQG)ryJQ1hu^oExXm99A3zd -s@cmXxto!($p@Vqo}Q+6oVMqE}wk9|Chuc87>+{5d1`z+(#COfiETLvV*dn_(&$$mVw`IJL{9y#s1BU -&pkeq6$~swK}>AcdeGzCrnHWaSWypr08D<0(hEYtSxjKt&wjeF$Hul>0j5wI94y$leWhovClL~B>{9( -_p)Ylq%ro=#t2+ig91Q{f3O^Pu&}vMy%xBGrG!qfuz=8hy9;Rrbk97t(LU4^vqB>pBx-?~&=-xT{e(- -WYr{}fz@D^2y`_0gPI1}I14lFI2-6zN@?b2TgD=J~Egv8^Vm2f}tAxHZi%m*=@#^WbR0qYehJ5H^+P! -ZfKeCeTtO38i{;?o!1G$Av)9=&XJ_IK+u4gIQ-nL+mQq)}gPDau{9Md)W4}|8AgctCfkEqWm+a3h1q3 -Ddb?0Fx@59a+!EB$RYzd@L0gaSbdO2o%p%XJpd<3jTpbJS_`;P*sXkdj&;+?=8&afVRwO%UJ{0Uyorf -q?g7s9#Z&;a|6A$*ztI1$(*Li}|9__cU#9=J9FCjsc5N^TTXLLGvJ^gt0JFs;=p?t7VkdbN -94@#0vC5~W2H&BO_pWp;PWe@lPd-73p( -+UdY-72ji-?dx6I;unS6_RWStGO16N3&i5I2C~KZ6k;Lq7rBOyv&|_lVLO(I=iQgvqBC<=Pf;f|O{(T ->@-Nv6uAsYT4D?OdQ%>;UO-*bTncn-t?ypd-DqQZKft#@I}D8oM&`p-kYAl$DX*#^**%>(A&Tg}q3Kn?mDN!dxosfs8Q?=GGOapUDz?`MlAaxO8={0vbZKx -LTG`-$omhd#WEMja$;rl7q?G7&|Z0RY<1mWUPQ7<^|Wj$e}066YBSlHU+a4_zn;i{w$=vpP}-G%NTlX -nLxygQ&TRhy;7L1rm0%q)FQ-{Ns^V{zEVB0IFPq1)9qMkGgOl#(DdB;pzKpNpFu-q2hmo3?qW4ag5Ir -0*HZfzVK}jqI3?ZKR&Jk>iks%`04$zr_+;zjhI{i*Q%F7}FJtEZ*;8kw~oezqr`rz7;O^(7Uk*T^${?yJ>wwJ?`T59*6ZF$r!VUD!*j!VM`yd)#!v)TEmNY4JWxB&2Xt$PFdDY?NM9*Z-F -5t;0@I}6&^4;NMKPYd+=9onkuwnAhXg^8!f8Csc7(W%wdS97y -?4CpFka{N}NX2mV@H94rVn}9}c8J6F@Qh2eiy!KQ(`D`f>~)T~EUZEmE={N?`_}#8_`ZH93;WuMurqs -NNi7qXl*5oQ1>&G~nw3?(vlg1wW(K7yE1Uw4Xjq>!%Mh`NatZ71+%~HZBMS4d`Qk9HmbHOV6S|>}x^A -I~K{}7lc+}U^EAzT^QOK&~7+t>xfp6P}tz%R!Oviz)tI`B(;K)+A659L5DP@gwwQDkPoXM;+Dh>){n`r1xwrrF=GXR$T= -qnZGmthEE%KDQWETVpH@$v(xJSxexSv#W%UDfk?&qjtbQuo>c_&z26+HOhRZz}5-A5##9#Je7 -IE^`W$Yab`3Ht1$=@(!k^Ct`Qsg>@WXh))5+heK#3sMRkahB_%Umy6$j>tz-?5f=GaP#lxs2i1zsN<) -Tn}By>llvRkDSf$y>x!ca2${1WQOCIBnu43-bS9xaC{D1j$}BVN0h@EjuL?!#&A3zCkHUR5plxs7Q}z -Nlj679aQpP8BQPDsbx4m%_ARSI6mYf?`JrDgy)ZUw!NOShv9f#T;9oW6qn^~496py@j7-lnw*(?H^70f1x*`$EYQfAY-SWS)gEy(?DeM=a4yl^k|`2J$nrGU+6%;qq&84fme%;qo5CK7 -B;GMguuO$^u^VK$}ACJt;4GMl@ZjQ}=#nN1e6$po9Xnax6GV*{I4nN0$-SqC;RGMlKyt_}z+A7hp`Eq -3)pU|Gg2eHXjBB(N-GmOtLnMt81{7^okJ>l1iD$9`8&aV8m&QbR37+zS}j^wuksmI^P-w2eX|m(1vbO2) -%+e6wjDpV;eMZ60E&8?Zf)G4Y3IP^5JERtZ?v0j#>9AW&lq@l5b-P-ipMI{UOkMV)r5sG~G*g|e=#!qx<1e -L=PH>Z))C2yRrr@w(cE!T7$q?EfHg{I*)Uo^(vqDyN-9o;sRv&8Ykg;AwgWFk%v}_IYLRR$X!a5NJhX -Nni#8D;UV!%?=OB4V3xQ>o*JjCVrUn2gusPG$4abDp -!T5(k2H{N1T<+wOb9=P6hdizi76vY{5DIM|&J~AN%egL2Ww?4q?9J6{4fO8|54>9u_XwH_49#Hr(6n3 -1@-tO3G9eRh*W#;q~rs@)<5#dhMvu$FjO#z@@cuY}3zl -xy>!_2559Os``DJOmF@H3VsddfQ=R+O4zMbt$dlZ9s$rL6FZP7E%*;x7giUeSs^773=a&Jh;rZ93DYh -qw!$MVx?7M-P+qI?72^_u&6X1*aEYB1%EWiBjP0^I$r68kZL&uQ`BwR>z*+bc>L7oNC3shZemsy;?_< -LsYwM;BfHdd{16|pQ4ycwBdOC4086TXB6cfJn0vE-VrGMj-E$6WmMjGdP(mBh&?ZM!z}wkmi5J93Z8P -N`Hel#mL*LkG?xv(w6c|F*NOc|H+59VV>ds7Z$EO#`e1s(uyke^)uF`RqEbRjCQuID@GJ$Zo~4VV4zJ -SqFsCISu4cW+Jn*0mye@zG32~Hbj_%A0Su!b*v;s6>~V!2X_+ikD -4s%TzZSZs|J-<5v|Yc)L{2K94~=Il@TIeKO6Ydxm`5G|hB3;wQr!}VJ3IvHe*BH~15T_0)Gyjxr&P1n -9XD^j+s%DkYdN^iwdo@mC793PRdRXJy|fS$`#(Tm_svuASy^X4GE0x7*3>2~V_RLe)`%sKpk!W?x -Uc7ul7RE?uP!l;jvP?)K^Fp}Zpx%+d>y*^2GA4=VIs{3!4`yQ8jH|j2rHv9GgtxaTTCJR(g5%iGPzzqzv3Wdv7*!)Hv#GF?2vjj&iz?>23GBZ!4g+`?jXCQ@6>OuUS4K&1r%a_Yz~syJwk#Ig-KDdSEYcx!;bq0qFEQdAWXd -BWzC_!@TzH6PYBWk94bI_~h|}@liXo{WuU*)mq-%gT6m^_B>XizUVmKk1h2|fJA0W+IsNrgS*$S&^hmR+LxCmhc5xC -kS+6j^?eNGpAgK6SqjkWcOF(kXOCffIGa!d@W-NWTv3z02&cqK=m>BE3M?{9pWt=ogko?>^uVKVP5R; -@;JfYqQRVrU{q#G_QF#X>aBw$fC8XY1xUp9f5e>%ere2n9mhe=jHFcXQJjGa;4o;Y{6sKAL_Az<$=ae -O*%}{+2hs~ZKF}G;+zkNx=SEyM~-~w6uIX^S>GP)^;o&ER;9pDi3DS!SM_i&$v9ng*8$8@1nT!P1~*~ -MnI>l$?iPjBG1K3?}`J4`>1=BjJ;MYn0#j(QTiQ~-rz@@POc9@$<*3xGsSkd7#!%SSUJ;`Www#TfC6C -u;nem>#}Jzrza^E^G|95ZvBQViK_{&Ju{T67m&DxuadgtZYiCH;=oL(0CSU^gHlEgklOeDOJu5$TO64 -JRCRa_jx8neUVYv;)nOo|pN);65GvhXs-U4j-nX$^`lSDT{mV{dxE>q6>egs|-Yq; -RSZJmsh?|y{PH+LxG11Gn7;BkvY=}XFSc(sA@V1G{#80Q6|Mgfyh`kDb1oFBv8be5fA#_ig0vvOpzpT -mD!Po~2I6|XB5+q@AixU4`svY!+NE|_%`Unw5BVl;wF^PX@y|*2tw(D=wAuV9 -m%Q%6>53n6ICoCA)--81F1@gt<>)bz|)S>k%N?)G;{i`)4_kk4?qvB1ATFwS}0t>q8_^{)>yo^idQ;3 -u!0@>jFO&4s4#?BK}cx04#Q)9v9RxIJ+u1gqv10^gS!Ah9e+I0zU(Og@?SS(?%hL_7mcYFi+ev>W -gOk0UYw=YouaGORw8igO<3la)`ejd`bSGu=oY|XZaYHZIoY{&B<1*w9np?)~xP}CVzI8v?Vm^JSHBaj -L$SR@qp@$D;n^jy8(7N`v=G_~Ix@tG{>J=Gq>sL6EO?RsoP{a(!<3I?(MyaC -Q@}m~_yKnvOK})PIdWeD#Q?1M}fNu8N)y1_hxxo)ucnXUX%o5x9+UFinv)c1}MCaKMWu|Ujpdfi{(+P@){`Cr{4u|P`AEslgL+k75rULDs(^oc?=_`viZg*2xwj8By< -tS<^>yFyWf|<)-Zk6%H#^RR)%*NKJH5}Uib&M^{KmCm}ti%wrOot~ttPQd -;!jVpNG!L_DEbNBJ*Q -p}ts&K&PyFAl2>Lg; -I^C5b=$l$7$9ACz+Vc3{tqA%q|Gz^K^d6=}OjQKkWC97JYemp@u5#?Uil85UtaC-sA5Ctj2%4!hM$zA -;3HqJ>iUI>2)dW2bZ@u8kAq+u2(Cf7Pnqp}CBql^(Pcifqsu+5dn_}oPrWm>;e^Rz15Qa{w7rJ3By$E -$8s3C_Th6Wg`mF*s?a_Er!4X7I$Cs0f`^mL{h8tuor?0?cXjN+=HhjP`>H*wX_J-KS=Zd^6AH&+df_p -Ulo4ZY^HqI9Mj`jMZo%r~SOI`=16tuz$4sfMOKsHz%z3)TGGicDZ?uy>*w`c8M%&~w~WL(`5^RZJH1E -2^QxepNMe(=S{#^tY;N=r2^&&>y>0L!VYvLswr3?zu4w=0zDjZ9sSUX*G5zsDSbpA_LDCb;CK@sgk4Br;jA!SReU%)ID3FWE0o3?z=Ez^q9 -=SnGfl^?*tqXocF?_Z4rsC>HgNH8fF{d;enRQU6~Dnmn^9{K%q3BC5!Av@_=vcV;)pGr^3@VTG>##~$ -Hbn>G0|T+=IDLkN4zN#aNs8dUI&`!p8r~$ztfN<{d}UzVWRxf?XJQfx}8mwU9S*cM=v$aD&+I7*uYor -fqZ7s2Z%?$a}UOUh2ADqys+*dwOUlQ{X{UwKKrPL>`@{$rO`1$WkaKT9gMef9@R-)MWx; -C^=NP(&bM0Ly))s&|7U^@`1OU!{q}YwKqrak~cj3mU0OE))o$V_Ry9W;lP;(y`ilc(~`_Xw}^a=DcgM -U^`Q333!=_^Gr8CFF7EGghC@Y_2`3BAxuefQn%#b{ir6&OF$dZZ=pUffxUA)25dK^gMLUgVm0wAHp9$ -5bbNl@Rr9>7A&uODdu_v3+1-h$=9_Cbbw!5e1W1uRjz~;Uv6JiPM)#8a$=nJX7k|NUvX9A}p22nUc0q -@FM=W@mZ%YL>-1r@j8vZ_zy^(yMY*!S&?-ZF|`;dl+IF#v!EAKC}Jwj)X>W*PERGC6X}COrVTY6{VSp -G_GD3u0nB_D%uWGIgr07#1Sg>+3g1U7BxF_uWOV=2(x`*-Q_?7**$G`5RHN1mHC-X!YVoX>sw(QeOrG -`h*1r?=sVu^{px`e<_y&Ah!4&(Iz8K#VQ>q$pXa3+Kdf1^_j@+zJEq;+yE0U1*A*8wyLYYgnwS``{17 -nG&WVuTNzhb|8_JalU?qDFM(d$`{cXoF7zt?S0%*v!pGrH0g92==>J{LN|>}t -tRPf^nAb{3G~`ldJnC`MLc?Ckew=^e2{^cZE+Wk-R?^<}sEbUG2jyJCA|EQRNNW`3WS?LSKce -U0f3?$IWlsxUEU1kB6lT()>8Q=qI0#Vz>N^g?EP#eKBrt==UYW(0AE0Vk_3n#I5)4co`{>ZgJv%;1eM -4%ikocx9sHefaY&%ArhftSflHv!tU^VIwT%Rq3e|pQzSlKezW`%geYA$2r?9#j6?0F7JZSm!(BeewX) -5d-aD-F#MX*Q=!-(BH`k561}~h6T89&(Gz+@^1gh&MhAmWEN)uw#4JW3eQN+dQvQVZ3s&LbAC~*|kdh -)UOWQnKqF(vf99yp)fpfB!A?XM~O)4zeTe>abBLPG&I%wvVu7d_M3K4&}WRpbjX{I%|p>doY?-$YNt* -WaXXOwythek(A4#TI=oOMm>3ySA!!L??Kxv)5BI>Ai -fl{GWFH}%{gU}C{Dz2k#k+0+7JGky^d63PhZ>66t(k1B=_BP$8b6s*%4xkM`r|`o -(lOvJUWO`NwZwgsr>zPLUjER{Zf;4W|D%>Ra#i-v|CB%-qA54y1)E4y}G%VD2<-7ytF*>DOnApwnlTz -+dkCw6l>C>VfKPwe{<)eADD(Fd#zCy%BoM#l{bHgP*ROgwb9gTkFsBeXm(NO4hLdm$^tFZAjsmp*~ms -bLj6w!=vnB+gdgq?<$)(XX3^+#;a>(|SV{DJPD+T>5>s5GFXqKoe%-KQD}5F5VfcOH`-Tq@|~q3da+_ -pW4TN}$EVoNQbfH8wd6bXh~zFi#t}r^lJ6RcMsHLk`^`NZyGN7X+!*eEJuB2-KFC_APo%cpFKdh&n54 -u>xXWgyH=dB;FfuGEbWrovAN+66rr}Vn8Oll*g!p`r;fgnvL_qR3cjO^i=bC7De34{&sAZwNEfq^_xxNtRDU5HRDM -zye^hE-;%(PRMlJIIEU$qXR1%mr#;R_7e;p;>P67T*O6yE~c5qL`?);Qc8 -saVc$8#;HI16T$GoUZOMezIpGHli-ysv~lp=ZyRf`}|XI$P~du~{D-$%yHXG4BThWJx;Q@YFBvDj(>Z -ukwQ1V2Xm??ql8>9{EL`v$)QH#Nh!DdM6F$2bW=rAj>1!R@HF4XKXCm=-J;f09YxM-aU4|cRaX~KV;) -Y_ryE9f$Xjn4V5)Wr;P{btJ9FpV-2X%p$k1}(4}zU`zS!kLAmWU>oD*@Rv^|s=9uJ!cz*qpL0@vfM)fvK94G1WoSa7kw|N0HsSJ15uGvC^nDFwVcZM#VmGhP4s5{=HI+*t5A_G~8o+qK5 -iKNtLrYJ9{+0Krzo8(yfmXE;I+A{r2Ci9?u{Z_()TLDqbCT7Ei-DF8Ex(KsVJ*l(rw9?mc@X>?nhL8^ -NIx@%(c6N*1}_|Vl^r{36nu5bAeQcj2$W&$fz$LAk7EcKFST3o}@hqDxgK6~o)lf3T81^Q^*^>Wa;Rh -YWT2!pR69f$d#9J3wjU|yJz*4T7ttE0EvGRw8&H!IA*v{2v{Mbn`;F;G57UFl)F`Y5I8kVfoXa<{JOP ->{Zeie)$r{j3#cp(XRer1&s!E3Cg*SnW{~ANB%X!gh?7mtmtUERO_PTx#eK+}k7_h43dnr5be?79KZ178)j_+|;Uwg9}? -*#hdV)r&KAFhUTqf3R+a{CY}L@oIWsZ@xL7kYSbCTUf3Mzk*JLqRff7iC7<{p=->>_es8f}GC3R~g{0 -|07D2v^CLjBYzW8li=F#sv;%bC$d!tk|Y4FabeFmPXRchHN;CFylB{{7qO;`D$5@eEpp`3FgP*V2^x@QWM!CZh#4i6 -F3?Z~uGRA1$&?Yi1gXjyc8%WE=>inprKDA)6VS+I{H}t)1Y@f5oavi_sx70>`y4|JE^#JyUgJSOdUE% ->#Z-zBR9(n{pDbnt$6f2ot)jOQ`i73Miww$;;rO7^kXz{g(Tt3)U)+4&(Sw8^C*L#z4~IjgFFnj$BAH -8ytmQ6|stXU3B!3gpAq;nklxsV>U~C6)<4B(4=rlT@?rBLUuYI}_FI#U#PP&in{zi3Xf{N}(-lkU2w+%l;7^(rT9+EniHhQAvT}3y^Jx0`abK# -fqDF@!FU>)~_b&SV4>zA?5m=6%zb)teL`1kwlDw38<}bQ#3*SP=0*s4D;|G^IZC&a0bovL;1-S-*r-t -g_}|cz1*=Xuam;aCFz~mA}5L8byiDs(PCg{F)=6aye=z{PUpX}x4D!5{5t%0jnPo}EQk9!tmLqs!|yr -t9?M`q4r4e><}ic9jU3+3;h#7>!eJeUGKVc321PMAh{Ku*3?Ah0MGnh2+{pcH<8UX3f8_88haYe#b9j -}*uxJLyb2yX3J2_m(;SLTT;qVZLXE^+f!xj#`$1xbfp@G9&I2_NRz~KT8(>cuHa5INHIo!kH%N$m6c! -t9-IQ*VN?RZ{a9ENik&0!peb2-fB@E#6#bNDicH5~qx!|yo^;Pugm!&^9v;ZWf44i0lT+{|Gehw3kx* -UwZAQ=;0tcl9e9gvu!)z6A`H9b>TNaRxj7%A1+JZE)xAo&7cq`(+726bTaH+cQW*_H^g%WuAt{lb_6B -8vH@mN#>JeGM$VhF+2{{Jcf)X7LwqKdp^01m`OaDO-$5QfWJv(HT+G2c=BBS()r&AVt_obKZDC=J^v% -rel)co&20!tqPd2-8He~Y5i8`>3h69||LrdO+ej{1(~13T@GXLyYC8|04Qx94G!h3{0bd@t;cq73**q -VrIS2d`mwy|C%_7;{oJdl^oJKOac{=!A4mJ*I?;cMo#M5TW%A@9A&99nIHD79e)O@h?*1Phdrmv=}ri -1aMLVONzOXnff{9*3f?2;kp=qs;-zl9KAIxTs7mt^qmZkGu8&vElHkLJ_eoJz~Z(pA$_<5%NT<59x{U -kQ^|ub!0WVyJZZjW8s_&w$8!_%Zq?j!sV>%?wGvE=JBWCTp5?^>V9=A#TfE)6P6QP0U5(JgZGq9gL0@ -jy%ZhOh-12Z>A%MR=m+>&%!!LOtq)E>e;&7?nt$}2sSe{hZ-SWObg@3xG_$Q595OH>oG2j2g73+GA~t -h*wa&KJk#x290R&!I_!Ba6VS_=o?~^hNOmw&Vr~w#oaf*$Rm6BOJcdDk=m*_tIE{y=*2~*R=j-Pm5ZF -y06x_W>NNCSqVZHnG?brXN0fvEt28RzBI&ApOw?vE>Icjv|n6XjOjnK&tS@|3B!#u=x@n-Zqa5X ->`YSrTuXoiu0eyyW={7B0H|j>Sus-nlF#b$MF4HDg6)*2-1ct8;Q~ciHnq$C|b4)^E6bqq-tLnW2r% -=)x7-;cVbNBlXpppSjTgMUp7^Lz|J{#|XJ%{Rs9a2HMLQXb)ngeI2G~0Srr(PVF*ioU5tdOej$vV5zj -XWW5pgH`q5iXdB6ac5i_HQ)%hf&=}g{Lia4{k_zFn$Oi5YTaO=YT`c}A+6uEEWGc-qOVa?UIcWUoj{R -^hO%>g2@R#ih!D1W*E@oOv7VB!T&7)@YxeDTCdB<{L?(8%r?6J^KE|lCr=*tl_z(4rgloH9GXYuAT|crX=;Tixg$5Ah`P`mwuQ7^QP5^{{RIJNTm`-6CD( -Q1hhHud@=QcR`P_Acv^&IA{$o=TyBOHINpVPxm<0_$K^=^|_Xo^!n|tv-HOQ1L+~xrvHQKjdP`Eqcw) -{t^xmcDg|I1owX#jZMMaz#uxn$#-muLtEpM+G{2E<^>^d(qhyS-y&7{~CpD_Z-`3w}& -^ltRjg43~FlW&i40Lp}Q5!ootGQue(yyVB2Zt#^Y`&#a9k|(-8~5m$Sq+)Q%|YDU*o~Qkxw(v+yK{3K -H}~M?`aot5<7OK-_vU5;H}~P@y#dVJSB;OG`>E;sGxJT{{5&@gP}B2cW`mj@HxK0Id>u2dQ|ZCYsyrL -V%^TGGar0Gf4&!Epn_GOCIgz$~JC#dN-oUz8K*u<>*NZ0+@MnOUZZA<<0K=JD+dR$Bt=>k_KSCzcc~& -m)0s36c}ESA4ua*Y8Tri7rlnl*&(*Fo0&Psk(~{wH~!NgA9#8bZ;v2tLP -CNU{>DW@TRb(J^WU;KaU&yQ^Okmr^)}n;CF4EX8w0`fTr#5Mt|)%K-2c;af5$t9PrQn -{aX3)F9iPFP3~?tTx4GE_U0QcZ(F%L+R(=#UBCQ-t%XI!_xxsC$#!W+>2L2XyYK!7c0TyKhju;u$fLU --d;Ez#Pd@eZGrxcKx#wSa@ehA|=}&*&`|MauZzxeX2ujPhsoEN_R?qcJm?|=C5r>5qX%RgVa`b(?wPcP7Py#RAV3p -D@J<^P{f|KGkq*0uZp3i{*ztG`=&=nrPCoB4Mg%=pZ%yDahB4(5A1n9Dkt@9SW`zk~UK4rY9Y*WLVD2 -QwZ`K$*H6KHMLlVO7@}7iQ(8=dPV6rixj)IcPsW&uX8&e5Ex_H0NaG+E-JDg*+Z`x1{EY#xyZ&jWsFP -=CHZ!nH`IcBtp!oE3DIU*RhcE$vlwx=F%0UQN#pNriHlwyz}$#$uGzUfzI=u{Cl?Mhj@8`L|0r~41|x -2%umYS2GZH^VKGSy^Fdw%p)M&2wWJz`=Wol0Pz1!f%*;%k&S#y?w$^65MxMi+ncLvQ{hM%t>+Wn8h?2 -1T8Y)}g$p#m;CXh*8Q63!vvU8gpJUvxR%{Ex=_FTImE60$X>c~mUwAv>bhU8ICcDn0;{<5;I=>{>^kZ -ZH%7_958X$~=UdA7Q9GCv0m=$-+DGo5)lbe#xP -I&`wSk;Z53BDG`ljWoI@mq*yjvN{tT2DOt7=3%_1vRylFZS-GZ6@mO&q9H9e-D=3qFxb?!cKnT^YZYz -#S)*zEc7V%iDx>+VT>T}ivq1x4F|(S3cvr7Z1q^BrDF|d6HVW!{7-D&u*5#>SnVpqm1s=)H>oT6TnOU -MW&z72IHKb;UR=Z)?Xh)uX^zy8n(bk+b!_>eX_@<4oFg5U)WzWr7ZOsu?|8415?W?nLQfVba)j^1ncR -6xJs{weRLo}Ubdh#`GV;NnWv%@@ZiCcXpyCuWoUz;UnBCF(ysdhK(8%x%$9%*^GpHAqfmXEbd_qeX1c -e<6bg1aAsBM-ASKY9Adn9l2G*x+G?R9*_?q7Ers>wn-dC}VbZ?pll(LwA;~Yr}O-lP>trzQTbOo@W?t -T?0K$R))d4+9s~Q`Pb814y?wRDHSpb&D82*W=fRR*44RuOK%WwU4vZh%+QG)%=hP+=1JObUp}9{&Ggv -MKWLXN-?|kA%J@l3aD2&IvH&V+{Sp|1aG=Rsl9#pGmTg_8_M=PEv+~3x;`r!Mw)ExX27VZKa1R~nksd -&LMEjE-bM>`><^HzeF>I~rei*P{gD<*;`9BEwNjGzFA1&!Kwx=yLB_tBVV0;H|BII*i*x`OOejdbcta -oJZk%8j|9MO}$VmHz&JrL3hAo?gjyLY{|)+33KBX_}=Mg41uU$h?q7n{akL;Rs6nh-w{60Ij8V}lyI) -d$r2mFsLiDc(uJz5Ph|v2NkLJ&5;QkL#8X>*wYukWJvr -!vzSO@j@tf=I@}nJ3$kXHTIue7L01pzN2__mt05PNo6GJl0`=YxMgS}s4pZc)cp5-AnO}_xrFI`XiC3 -hqJpq~594QuRK&uO6dCVD7~9?GJh+m;u6VRhP6LZ0M#={*YQo_+*qf0*>wyiK~52iW{MpVWhVqtR-Jm -UFqr$AkE29;NZPz94TBB>IpbD1Xq{fO3tm2k{;271=w|pA3k8mJHDBChfV2#a#;mY46d#}_?f=KF~hLS9NGWF(KT_eg)zH+>H2o18@YM$aaFH8Tkd!Dx*SJZ -Sp)lYU|t@KP^`wYeNZbo3^@$A*@(_6Hxd-7uhS*x2jyUd~EFK3mzwd -$fMU0Eidr2-+{yO(UkL(&?6L2m{|7UXDwv2>A_|3sgFV>PV;vU%C$og*=6h4KCM&=vdo_e1?D>wtr5; -q~L4)yLe$m4k5pXZ?_tEUq9$WgMe<(mja#p-cQSxr0GyL>KEHccYwWg(C7M+UdaHV&U=mR!QAod=S%u -M1?8&`uGNyr2mKx$Tdr2bXyyO-irB^qgS0Kn*>9>1& -?i?#^A?c&IRAW%kF8w*aBfgAZ*B7o(9yr9>TY!5eR7jKO}R0umg|LHm9R)&KK!Ct!+H{&2x|jeD#+Z) -P(zy@aWzo{QEwQVf8(0L(03`f>VN$g5AbCHSCE$6VeEu2d6{tfjZJXSr6^58|iMRydmqpse&R$H!)yMn9!f}z|&M1RKLM#pQ;XT2cp9#P% -w9m7Ss!s#JgH==*pzm2cDd5~^n{UTL!N7#{O-&R69s$@9A;9h>fvmh%#f75Gjk4BYaRR6`*g!DK@$gH -2;+pJrEI+k&)>c0v6zImLGES0V|K|8tMzuvdjr`*e?P4P_fh*W9Od%Qmx5`CBF5KV?>XJdq+FQj)JUO -x6AAy+mV -_=@T8L>7$9%sPcu)7v&sp5&-c8%w;v884y4Qh{0q)dJxo6H!`4Yd`s$Nys5|WruT$EGE}@uhNfR3L!* -BtLp48=woojsG9R9QXbbhD+Ln(aqnISKZcLk<4eWM#v|D0s_xsQJb_V}*zMaAUE9ct>Xxxu+mTG2L#d -tQAw!ln!A&Jn=v*N;DQmUA_kZcB%`zq_q+~sWLl{`=4?N*qGtBbTiQ5u)SZih*}8o$nKVQQ8*J=Z=jD -`!Qvm4{soK69*g2pylBlb4%qjTi0N32Pu6Aw7vPFE49F4v!%@w=GGMu9;3V!q;vg&95*`EKq!Iy47_y -wT}nbCg!F)vaQpzvaPqJu13GldCtML#ZF6;<(cTn7PF?U7p=*;3$xO#@tLW1@*ppx%b$^uRT%d?FI157IjuG_Pp~XBCYfpQ4?P5Hs%^U63i*4F;znFHA>WgP9MNizw`Ylvk?eU;B3 -x1@v7Q!d>Kf}c_BK{lJJenBdK;#)qXWMf$w{rclGaB5&$RP%GHJ2W6V|0!Z5SJrLd3(`uONN8boTHx= -b5a_9V=E??MZfP97`4$UqU4VQqz&?P?ACOYiMU7~fJ -NQR&t3!1QwI6cfcP|(!utPLHDY|KfYXUoco&vih$-vPC7{Vgds3#mrl#WUh==8q|5+qVzsXOU(LtoA% -=dY}4XTm*e0rCt*HCpm~4<6&f)iXmnvBqAOF -U(g`$DTdLjaqOBEuxY1}qsyz!ARTq*}=8`@;gHe8tN88>T<_yLx#_7;r2#I1G!E>~bboI-(v2$J>&Fy -yWgUGM`Q17-pb@KPYz_=4bI# -p)&Onx(TX}Bmwf^6$`y1{7jM>aWwknDe@8&M>dj}>C<2anmVGM`S97b|DoI?YLVI1l>Z1HCB0*Cb+{* -}Wz4$pE}%V7D^-4F1SrJ%{@_+{5ADGY9%-enm6e&)fdDx&In18s~N -%J|%?LcNnjyKD-`zdP#}=o|3|3-1MI9WrKefN9=Wt!xzXopMzb|;a8`J(VsOGg}yHVH3T6w=lW_fx4OD -a}dxTSaf -f?{PfR^4MzW|P~8Yb&Dz!9G5$NV<})b|JfgCQ)yZ{Z68yb)mhO%Q&VhNJ`h3BDw-Zvi-N0LV&!#{gVE -fce=7u)l%f27u!Sf}D5@_yJfukfnw2o!6e`>Oy)hBJIWz^8{ook6~HipF?1n>}i8^O;JfKx#X&j&wo0I!T?WxoorH45?!_9U8+Rn -e@B*#N(SuMzBJfVYhU*$40>fK}r`E`s>00iGMr^7&VQLt`LsV2@`YhD?HZ5C`Zr1@Z>C4&bt>PzJy=0 -lso8A%6t?Re)dK%Hoj$E{p>nfcjbla7`R5OF6*$IOYc-E*v3_aJrG@IR#)5e0AWz4B+$d)dRj4;9ubT -0^?e698` -nm_HZNg0dj|DH-H>ux|nQ!2&|I0bU0X7q>9}$VG&_a67A)KLYf;gOy7Qa6Nqc!T&aZFWtfX?*(XusaP -G@X9B#inB@&&&=M9FVf<1;0suDwJi3(GR|9Il$Z$hT8xtsnC~$pP1!@+_{{E-2t#69b`AaF8~~EW%-W*xXQ};4dB#Fm^T3)2k>q9Y5^|?7@x&(6 -To8}uK}365@-uJ!dCbQ)CE}uv|7bzxDjCWDqdcIuV#Z6@KX-Zv>JF4>ki<%t64pM0q}SZ8$JA%28+-o^Mc1>oVkSXy{?<KUfc}*E#Qp+dv0OzhXH(I3-iAR;3fF70sj%;p6&3B&`>yDVt!%(_Syk_33w -R5**kds0(^1@3;PVfsilmzaR49L#oEGdfJYvNHVb|bE_#IbQ2-}C3UfoSpA7J>-HbjWz{|TCZLb2{{} -^vO0PlVrU($p+0{F_~EDx^&{ONHPXA8ifC-^u5uznAV2Vu}ttbagw{%O`X)&u5Fx&?msR2=F5Sv;U0e-Zd0X-3xpRcpO0cURKw804jTV|MM5XUk2X7*PdSiI={l= -PXc)KRiFdd*8&WEjr9Xz0GDyx1~BY(@Q?NYKid!UO2EGWxZwaH{UU%q0M8wSdI$Wk0PlDM_#SW@z-Qh -D9tIrY{f8jmfbRrI4zqCtq2Cc^9{_Om5k}{HfRRU`j9?!J(5Djk0dNDr_u<j90S(e$ -VkA0VaP4bq(<#jH-jMfJX!L{|L$gcmTk;AF()-0XBTZc=!UqH@;wP;Z1;ZzGQWj3~<6%z|&wK15o;k_ -g4TXe$Du8GQh9kYXmj#}O8CKM0@X_6XnPIKmG(j_`YqBMiLEa6CIdlH&;Ha2(-!j^n -xU-5f`FkmCs7<2bG(;+bH?5#qU5%p1ZiZjW#a#}V%0IKo#rj_@SM@to#&9RL6HV|R`{>HgqwV9{=b?y -cqH9^p2&o#y7xht5;>fIZGnpxesFaJ9I+Mo<|%$;W*~@RW~}l`w#~cNnSZ7>|bDloUF#r*p$j?D4nGt -!muuw{>B^s|)+*yRhHig?)7w_H|v@%bnQcI&2Hr>z;ys1&H?rzwF)kjE0IasyXag4S8PIW-k8|v_G#l -^YB-}eMp_{o>DGe%@4 -S`cJ0QE=;5MLbM=%`(+tj4VAg@*B7_C=+O=qwPu0}aP&1t#>)^kO%GC_&zIJ1y?g9i>D!F;>DP0SO&& -asS%v;cZ&8duxQ&rr2mAPkw{Z%wW{8zypf8b{ei=UaZo2mbrnp2g`qM62j9@A%LNMBK;Q#F`1u*3XaJ -$3m!n3~X?=6~ze?KM|xnlKJDx6WN+1`k)WA$~A>y?HF;=G`@?pa>aAA6?x`1wAgZwE(=Dqx+?JY_O%~ -6!_SI3Yj^hf2wr7jyco#Y2KJK(QV^D=1i^*DBn`=nSU#=zyP<;6FyBoVI55upN9K$4|i)9-xm -0ke4>HR6FwjK^zaQ(kk5lSum2jKM@L7K#Kc50Z{9qzVZ(-YJYH5-MqYgJMe^#auTmcW;DZmymtTJA;& -U4w4**%wws)To*2LYmyOq~7-XUAwx%f`Ts$8-Kswm?fMcML>a~W{Lsx9CBwCr_2wmkmjVnw-4dNr}z?X>^cv}qH0(s3*K!-gs3mCdoFa76;yu_~VI%r%ln?Q!Idf=T41d^35ZU^&^h%SI -~iokeP&UPXq#t&kCi6f&k#A-5h;$hhMQnS4?qOHL>x{;Wb~om0r1^9sp>Z~kWrS^Tv^(&`nm{DMLX3J -OR`NeQ{{zWc~S4?RR4e)wUsd-ra#XU`t;```bb*6-fEd&z6By+-@8H{X1dymxpn*?LJKuQe*<=+UF(* -s){e#EBE+^y$;&?Af#A+_`h)lTSV&7r(3{?|rY3Pe1*X@?^Ey93|&Lu2m-4cMS`CsUL$WU(@btXJ+LyOjILYsz29S>?3apOO#=KM2A{Lii~V-U8uohwv* -P{5mC=Y=`i>A^e{pd^vSKYA -!3Nk-uH#UMAqoObVT2|4uwjA<|rQO*PtrEUySKAA%lm=r7D+((qgzYwL#Eqn-s$F;af2tO6VCqj5!(} -20YvL3>3hwzU<_&-DV3J8A&!hh)&9)yo1Xv#JaHp+Y!Zw9{Ml^VD|=Y -;uP6};)gGWw>jZ`RzOE22a4BuALTd^X+ILb1@6GV-8NMgOzryfI44=yID;Rzq!*6H!&lsNPOgql-=NS -Hq6TStH(1S;aA`2rGGnd>-L6k8q_nDOX#Qa=kApH%F85OByM^t|8^tK61i8#qcc|-oWtv7=94Lk7 -xK<48NG+S26s0hTp~Tdl>#W!(Y_FSJOD~B*Q<&@IDORgyCB-d-i{K0It`ZvQ3-MV>e -*DaBzaEpP54>uVNVMc2U+|anOua9;;A~MuyF`CRKh6i4)TKM~iSnncfiL}Vp^(l|K)>f_55gPgWcsJ| -@cpl$ki7;5K4eHeM;Ol&YpLRnNE5jR%05(~yPt~bYPrWX1Y~<_BNJcovw?6sTI(2$<5jXgY8+^P0Z!+ -Vm5!e4(r$G-5wVur}1H)Sl#>ie4OSr}Ai2>?))UErs#~**ZeoxGR>ye`I_?{(hu!0S^ZVHc#uvj9)Bf -U!8z}K5L7vqOTnl0gx=E$y3dp@b%fa}ftTJ}`b@l9e1$u(;*ZqWEA#&6v@l35%XZiy5`h#4gR8Goy}b -tq7<@ED$5yVj{whi`CRx3+)O8X6GJ6ZGQiPj~GqP^j;`ZmLzKR#45raEm#Tx!l!D5YeEkrXGe)M&mP; -YgGv}SR<{G5#bt^&cAFBVQL#@3V6h=MlbCGcoktbIAN-s5+OHm9i`CR#Vip=Nw_3J0{k>r?e&fMd@q!49RY`C&$j7eEPEnLZ6l -B{$n;GBZ}T=&DIM4eG{h9o{NT)0qKx^$_sY}qm;BO^muv0{bD71pg=C-T7$Km1VS3J3GvQm-7d4pqt2nZXO4^RZ4AIue7G^N?$5aMpL1ZM#q&kbdKZq6&*YW#6G;DPvBrXzXij$XZTQtw=n!bh9A -xFGZ}sv!>?ud&l&!1ocl9R`7=-X|C*=B_b_{Udivm4PrRtv)1z@fKmh)@tEac8r-!Fcb1(OrHJ@fZtL -N+2xN+0Q0kz$0HG6{3J$)KBZOZr7cJ~VK5Aav&`!@FrXiy`dHs4V5i6@?}@6*^X;ISHhPitRzd;2vHs -8;Q9KZa@cmzqy_dem(m@L08KHL6#s@_4gawLBkr)U$bm$Ewx97Xj{GPgZrS>KlO8pn4UI|2L0Hb@=SD -hBc~J_h`h1l^8t^&K?_d9~O&j_L1o$^){CWQ7{{D^p{eAoeef7EFnO`3tajHzIg=Z)g^#=f -zm9ihuans#{`|)7Em%2ZM7Zs7Q_|2Nu$Z0-VIJo5jfmzNiqijG$RHmP2{I(r -uJCr0tQZoJ}~J%#w$faLhYYL&HdAFts3;48u-)Zcsg^5xHS+}V2h^5yS;`st_h9DB}Ox^(FrA5UJte* -H49OV1uZetgYmpM5sR&CSg}I5;>!qsc=X2S1?zAMS^LkTnAHUz#*&(vXkfRm}>*dJG&m(9oV6e}3xl; -luNI9ZfrS?4a}K&lAT}I&|m|HmK<2$&x3Fg$))IOY712_Df#lL -Rdx@~69o;~21XPyD?Jj`aZAI803{I6ZRMx4u}HEh_h9rG~t!i5WzmzU?{^PW9>=$mi85qPqE1n&5J>e -MMZcI=oq2Tihb&LfVjbJu_V`R8KRyIVYF(Qm)~Mpv(1y>{^6!7bl?_uWaJ>sQ>zbiT#*){b#V1?>Ie& --oPQa(3V^J7|y{GLaqlOFHCd*^jT$I+ulg$z!g -J}u6HBW!|gSR4xr3q^-*Kpr?kZm=D|eDTE>0)NPm?Vlir-+z}+o2iY@Z}IoHah+}Wnft;2=FOYLVMyq -IP*6}4=rKC*|LUu+XzSLk#4;z&nS~95|B%P$pMNeoa4#q*5VmsU$Pw`wG{6Ty-(V+@5pdi16H&}jBE# -22Z9gXp_=L!RCsC_BqJF!Hvd$6RxOuaXZKeJ_G1uY0ef##bMvWS^;~YMf{UzifXaK(8Irs{CAPeA*&y -f9z6DLH+XYd^S2ENb(&;q={f5_POBa!(KQRsf6&U=VDd`Z-%fN97lYR)t?`H0B(L!#8UM<4bddQE8dwJHzaV;sX^1~ebn`aRkbOjf+nuF}3gQ3DFTX&S=eB9n -#+P+r254Y8{DBtW0zCqbrO^StgXfSD^alD5zsqYjflzP8zsE6^hOh%fA&hG$rlB3{O~Mfs|JXvJM%%R -KFe`{Z#|)DGANtSnh|pbSBJ~`6g)E>~&<#lkY#e$9y@y@MBkTh0+0#V5m<9{$f7in*4PEvUb^J=DA&_ -mM#V#Vh9ruqv`{K05jT^UPIi$kQ;9E+g1G-+8eu4kEj<^6g_!)hRI?FZ+8onipU>d@ihTKb3k@hq9C1 -v6N>#x5O$C(lxG{`RLC`~4%(Q)h6E!wwb8LgaTp(UdYv~X0YKw`l6M9+Ok)b|uoBxv|XvuDs9Hq9Qlw7*U)@Mk&CZQHi3FZ=Tuz#n?zN(b~FdhJSwqy@STJv#a2dU|g{FlCPGLhmMq -2pZ5{`hn;LjfOraiFz_l-M`V(7|)9}_hL4a|S!=L@V(0{hIROCajIZ2141vDTYl%-q1AAWMfR3qiQ)|s-HhLuc1##j~q#LGl4Ut}7Jh@L;E+B -1BT<=9<&1`X1lL4&ks_$0rrVwhiC@rRuuzHpx9?4Ut*S2{|w5lIW$rYRk1?W9hW^J-^$KZ$7=--T{mx -j=`1Q)#d=4Fk_J4LW?b`Wrjxq!I!!P0podFHVc_c0P -4zX6|Ww?fTi<|-N!<3G+F}WkHVH&cThSf~Nw#B39`sE)g((#ACkow=LQ>P} -7h3v3b;0@e>yQBraAO0Te8u(4bD#SLl?bCv23)8S6xfA71>P!x%!PTDs%|0nD&Pa1dgwd>66HOm#qA7 -#TG-uoZI&pZfph2Hww9;~nvhZhrFZ7@BNd?}(3rFAtykQT(Tgn7JUY{Q${(?s6|HrdJ=p&|KGt;nsGS -k4e0DA@vHjReG8V&P7!|*VgIgDu-Vy5K5;erM<_@qv{9HZSQrRzU@24WcU2=vvfSCidtC*(!4>t#}&o -!q%|hxWc3O*^M|plwr`2DXKDOv4A7J;Ntuj0>S-7xK^7`wqlf&T<^i+fnNef_$_N}8`GdfTn)3B9k5cbS8z$Zz2PG_I=&KLu|oe)ZIF%79 -q!y8P)KUt5a#+U`7@JU8pjHZ)1Oa1TMxpNcX0Q_dnn&qUym5%agxpU_>o!+^GKACHx9n*tot7g -w@b@seMqhYB=!@pRMQb!nRcC1PRn#eIYKKJCl?7WD-UnqZU^&oSOziEB@_O-;v$L|KNut(THa&oeuL9 -bu;k_nEmDc3btY3*=+jx)!AJ-?~hvr8*C{{jAxefRF&AqRT@{r5%gg{Yrew8RY30h5gt(lTm`G#Cj-|PC=L)<2_Srmc -xe)SEtOw9w&wEylquamndLwhZ)1GCHu~@U``D_dK!hhSgZM8Vp`MbowWy_ZBhYT4KKX~xqi|j{*9Bei -l_3qu9o_+RNdh^XUY2m_!v|zykfe+UAz!x+i79dVYIw``-ipH);2Z@pp5K@oJ -6%dxlS1k`xK_@45a84i26j85ubh`op@O#_Rre8#ZikuniMmqokxHVw)hG_vq0>-X{0Gk%}%yQ7%^ZPFL{N<+Q$OD&FM0q-P?6_d@;>BVg1aoV)OerZT6crUk4I4J3X -3d%r=hgxj-~rpvchDfaD=p~IDLFQL>sQNOp>6*R<@l`T7>LjI$tK$L)=)b3#YVb$<3=INyTbbqixw@S -88c>FOG`_mY15|N7%*S}jT|{r@SWF{LYF`T>;iNGH_+iq3-%xohoSd~(XbWlM_?U^?~o%wcA%w{#^nE -c0zA6k{T~3c?WQLrB%FNhwbylXrlrh9Jt_U(O{hcjT$AAR(ZpaJ~Bz9HL(pdmUsntJx^DRgVa -iWLHX@ESbFXW$Jxfd9ew$W5U?$hBbyumRWv#)S`N+<)hBW_WmbgtFXi`T6-pidyrjT2cHluRi~gd_~8 -45%{odiE|4)RV?g*hq!BhDEwQJW>PEL-9fggPEfq(o+2QL;eH*&-R##bJ!b>zy+>Oe#< --9XQdj@Ebez7z-8T#!ZpUa(t;!Q*k63{MSA<~w}pP|`7S%|gDyeWagG=O++}@)^V((q1?~cW{rZ<gKoJukGu)z&>2}jV4wM3<;s=+8{D1v>*Jz6htcOY<;es+Jw08(gD% -_;+JG-~3%(PyNZdI;UG;$a{s;Vl3)`2_e~#ZX*I=th`YdFzyHa8!nywg8W1~?zrg=N4 --k(KJHcbDIl+6#LGJHD4mgL;hp&Yn<28k-YcS0Z8+U;}bhkV`FH7I`z6jUlz6JOU8lmf~N56B9P{guA -T=jg&xTE3qnSc8q?wAA5Jv@}x@lpy0mmL!(+*Yd6?}iBzPAQ7vihATv5BXSGJGyB{wX~y`b~I?mDcX_LqZ^JTf4AeUJV3+qJyo?{MJXp&#C{+5&$-`pSuf%o6&2 -V0z_J?V^5EPzkYm0jCMKpY=lf>%`FIQ)kDVdsep1otDaQV`m}vYNqV9*(8XNY4QB!#4W1<(oCOULg;8 -GqAptXLLDpfl0+HN%4bTRxH=bSg#-~PaU=|}cUM>#*Yac(=Db71_2p*443&NnU(J*(EquosFtUuSJE_ -rNuxJwFo}^2Pi=l{0?>_Tia4XR*GXE}u1#`{!ED|2y|<>~jm*Z+?h@Q8Puo5p^!qSFk6Jz1V}-iS}Nt -P_5SsetO!pY0(Q7EQkUQ$epl$KzxK>M=XM`MlOpy1=sKyZD7&edLe2&sCQyd8}%LR$)m=0ulcbC;5BG -GuNQ{P^*z?Pm=}38cp}$4$eZ*H^|Ary)p{RlW$Uk$wsldWU3Sq2F~9!V2iIpFz!Su;OXrT#d()%DK4z --62l&b*BI^aUZu;yQweBmQAJFR~>O-hA2J`V?NuBgUOyTuwH0QZd`sXkpf8@Zx1@UL=;yAGfhW%jd$; -g+&U-OB=!=qpNoA&{ -rsrCqJI3mi#~>GH5%0VP-B8V`hTSQ2RT3TAAPOVwf+uV&~mlA&VJTN!(97iYe)ekylKdI5B? -8lqtjYwO8PPeG~8k4g2e;HDzn{KGd189}0a$jS01l=~{hh(jc`joE9r^5c;_HZtdH3e%D&CYYiCk-#O -DD>RqT6VNVl#<5C~7FOK>OY7?TyrOiKia91%`H=RCq`Ld+^k(14yJv;iHcixGDEn>Zo{2X)x7w`bb{h -6_%Zispn_EqiL-u7~>z5ts*eFU|_lo+#<2WWa9joPhV|6yCNzWQoB`~TsxK7e%r)^^|td=GeWz33a-{ -Z=1QC&2!6uC~9vLT3}GGtJTJWK%R8(1bqnyf1P;{!2;wm+LdwA##3wokd@}1E20yGb-HmfvB;dRyTuj -NY-kNy7mYEhxzBsnG^l)yYEJ!HiUc}c*u3Nd~OhUfEVybxQ2%MZO*HAZK5=N)ahEO{vSE^q)C(FXU?1 -%2R_W3H&6II_+7+4_eEIvu=Ls!lopv&JKx~q?4@9N{yii$dIFc`$XD{9lRv9a{V8*hm99dMGqK>A9^2f -VpfbdBNR -qS2JwGViXBls_$Z3I7DV=@}XZMp=SF8_WJJj|H{=W)h7KJ{{{H^dt5 -+|9o80%4^`y^;O`&n2#1yEa2n*9sMB!vDelOrJh|IL5=CJYqh44r&W8z4Vf(F~cr!FB*6t_jr*nq -o#tqooRUIKIV4L@4APj-;+Q*8Z%~$@C|rYRqiW7Uf?z8hEFamEL?S;bBp;g5BnP7|4YL|=D=&#tf6Jg -mWjP$_(tqG$(rH=&MoH0JTm@5mq4$~!C`x{E(siq#QtE_9RkEZ -3zm#??kY&Y3Q{j_rB%73j|wW@^QaX1{8d5lq6Sy|$HzUPt5%*=@N^z`l7+1Wy#GKN9UY}1GI@TjNl+_ -@9`thacKQ>=g2VTV{7Ltk@v4f50@f;lyjEJwc%jyc -H92sH{$t+vlOsfqbB)(vORuYAU@eZd7uJ4Q_hH=`#OqGvFvzKZH~bbF)-cEy*Zq667(?GK-ct8;u#bQ ->8~6t~AqL53P`ADwF7hqpd&u>WYa-Xhdc*O~Y%vDb9NifY$nS+0UJ!L|_%QI{RDK3+Oz9%llgQnXw`1 -LawbSI7ZeosSPZF(WzARn3ROEwrMg(;=$P)VEnv3t0%OJH5qw)*jXF^cHVftEvP?CnmHrT~?N -c70W7~hcKfHHvZP99M9?7;yga19P{63yqq0g|5~eT;pKW^+WZZvFdfAO1Xlc<0bz{J3|o?TGD^t;nWi -R?F;@**{ZpcM}Vkg7hQlMd`)q)iOLX{4#g|?VjzC?VatH9he=I9h%)edvo@Y?BZ;XoS>YfocTG6a -?*2F=j7((9*CjT-#<_o~^(Jz7^T7*op<`+%r8gy)*qX12cm%y9++P -oH;BrAu}m6C9|w7=Fj$L`@d`d0Z>Z=1QY-O00;p4c~(=jc!Jc4cm4Z*nh -WX>)XJX<{#RbZKlZaCyajYkST}oobCD$ZZmt6 -YW-?;$3TynlFv524&jHk+4K6HLl!I;mGKs -&WS3^E{m(pgNG()AZ9KZOax-de`LHxqedLaMjDaJXdux`jON@-Au>yLlw_T_3fW^S*Y)Md3l)?m+EI( -t8erB@uZy1vs`0P>2Gy8xlWsvrn=u+3E-n1*ZE3%H%aGBut!gJtE#NFf?zF}SIOlp*$RphI&za%R1L} -pqu@hXw15}q^QxSrwML`9*I%2Yx-igb(%Ibpeb?Mp$$X`QS94XR$rO9}6ztR>EM~L&u?B&pZQ-c~vRS -&qV-c%*-()oo)-JUOqbjeFrb(j;MRijFoN;qMPwOFdfxZue`fAZ+`7o%G+i_OR7tJs@qOyDYLqf0fnI -w~|bd0Tx>vB;|0LY@s%XwPRmw%QSe7S0xIbbym?q+#4pWs(Nv>J*vjLO^NFVx+ -2x{2ni8JeBzXp`HcgAB2NslXlB~e$6RJ8-sv3WNJbXukFbw`Tn|}*qISejpm=Z88dO$6TPm8kQt`U^M -$_lb@S^*~sFUOZ@^NyZH<8hJ9((xEdt-gb9^Z=?qW}ur`h-Him;b2M`Cl2f>@oFQM$p-Z#4+j`zO -Z+dTxpRT|4&h^p`((aGuUX}o^+VsKhN4+ksX?H(TO?SB|QSmpVPw$jf2`-APn-SPfGx9}fFU;q3S{6G -5X)$sK%KcD>TcV{E1_I~^Due*odT3BuHbMzJdYu5ZZD}FA@=Cpp16#`|yi(y%vp7YD?58rg&!sx5lAE -2j42irTlZlUP(NOkTA1m1_gjCbCC{P5RKv6IspiSg4f(ct-uwXHu4H@^Pezkc}B@1OnSzeYd*@vGC5( -=#L!fLU3YrWZ6&>3Eh@*D#>`XUx-Pz_WaHb(Q`eV8I{>xJt|m%b?*cOE -3T>7kLBKXYplKF6MPKIN3aNiU=IyMEQO)i_>Bn4Yt~{X%66#NV<^IRFVOE9o+*3??`Gz;i7=SghdS?q2b^P7chz0INrz+}R+KW(fkl!yo6^?mQX**VotC99pVD8s -{acAS8%aS|GGoaeKoS&WbC1ak_h@ugTT2{@FYpMxjrP4y_KzitF#kz!h6o6TgEd29J|n*5Ck1VY#?U> -#Tl&2Op;N5)llmwXdtrYW6f~hO<^HXAe)=<B)%(AobWcCJgf6S!MrV2DwP`G?r)mvW!0c0x!BbggZC$f^PZj|KjluF23rO4ID?CIcLTI -4W`2vjrW!#{lg7!eUs71pAnHmlb_Z&K!|ZcSS{K*z3oN3Qvb;Ne&kJ%K;P$@v2|#R6P49rypxU)`TdSJ|6D -_kae<)3;w8YiNqIlQM$B1BVW1`hw_%|FgPYfapa5sJ0hy{(n4tr<|0-paw -hm3Y|Y`-M9ora-L^kPE?C&8q*%c`Mks}i!WXd`RnU9n{Q$Td|IY;5xQWK{5H9-dEE(GGQ7hzp{jtt^7 -~*NMlYzZ%0)h9;yEx<1=h|pgyS2OsEO;z6|5%I<0dZBCNC$0M-A0pi=!H<4w)a2(|yK*1p`;(% -d{CQ(H+5;u}?ixAGIQyAHDwOu&m%(TFz7DKWDf>vzX`Tj=KO_+#_Rj&0m=2=u4Xp2~V1j@uQl@>C$?f -Wxm+`=+rTmDn0SdPuCgesd>`1Iq<`B{^?Ql=qdrKUt{4AtWHQL{fCC0UOGJ) -PP4V(0o4t7ok!V!ZiWZ^ab~9K4`ZrQlC}M*Fv0%elvf!B?ze)T!RC`wu^RpHll^}=a$>ch@gn4+C+@N -j^OJA0=Hy)Q-VBE=7a7z-9P(ra>T-M9LrXu2lwU?iL`6O!n7Lxp$DevI&>vh#DzZG*4jXmIB15{f -$2)nZ#~T#fEm{wsiNou3Yb!*`{qcTI)hj_x!F*p8`Ly6?g+hk0P%-`)~89;b)0@-%|HUYQFlc1>0nh; -u@CZU|y+pkM_l7=XD5bNM7DTIrX+!%+AGFwV@^Pc|1XlS9cI5E!({JMOoa>NCo-VND4;h=_FYIrBIFX -fI}?`n-(oPsq&0BggriH7?94)sy&JTepq5KEevpWwl$u7D?82xzWTUoDA5dwQ#BD#MtrFE8{e2d># -YS9n+QR6_Q^0SlC};@jkr|pVb04QbmUWy54vP=A?Z;K|uoZ9sg#n=sy$nMpgvv{E86NsD;TU?uh>k4K -}JrdIJ0TkqJFA1_O+~5f~yvbI3^WRH)bFY+(G9);OQ3c3eG98YfF)6+YwJo^3 -NQ+)Mzvo%ak-IvJWm?1iwP!vwRY$WFwKB9e?+mVVlFA&){*9_9UTCPEDC@04suz? -SSE%lFhU!~W->k|&V;uQmqrIB#-GvK)-{N2nG=Nu6bqf?X~A#XX+i*n8V1=$!B6a;Mf#m5HO5&jE-td -WA(cbUA=0;GnpY{jhKdSbVmx=e5LAmB0#DBCF+N3*6onTtBxjYtVid6FG@z<0{A?kn8X&sr1{5rx^(^ ->~kyB(DYT3i-jDqZ4_n1sI>0J~$2mD>$q#W2%G}S$6D@||lx8Cd~JLva(y$O470k{X@%QGHLF4`~8$h -`&N>ir-{avq)fm+yn@~+;)?_I5fdM(szv%vd=c(^woogW0@PjBdQzdAjuUvL!+c6iW!n26VtnNvS={= -2>ZE4p;zPa!Qa2z{qWuR-S)>1J8yRn$75J+ib+c9+MG4N{2t>r^RmDoz#>hj=pRXH05Q2r3N+;~hyA*ONLx^{$>DEMxh7)kn2 -A9foTow2gJ{+vLJ%ELtwl=@wRlvm3U;k_3dtR0ef(m6Si+k&|h%1m?SmDPSmDF`&x==xN6o&jWn_wQe -5E2Lg-2%P|z62e;DmZZ4c}CSZk9=AgRbvDc#R)6k(iBK_Zv4c{5ac{}|Y%0{*3-m@&T`p(w(|;UaLEk{qDkea;-3n6r=a@1=mP8O5Z;YaRFm{jRj -nUk_MK`rpK5RGkOlX56sw#H>5Lqdwzq#2*zkFV4F2qrK_SGMxb;01g!49vJ`gBd6$b3}h)ieQc$b~Dz -0C=^H-r={h~^QkXF<^*1%B%jh3ABSOYFghHS!4h;AjPOpAS=$w4c+n|=GcHP0yuTU+uPxOK*i!;M(FF -z62^dVvTl5n!;UfeX@{FyfsG>I)>tBWg)v!#@V_2FS6q52;EH!(z(O4lWwJ8!~kIPk+IKJ`~|76Vv3BEF@iNc3JU2zBU#N5$1y$fndhS9ipo^ecEDumg5cj%)yq!FQCb!*U{!M*c`OB0&y$9k0$eBU@@#PtugwZ)x7}VGx&I7@?OMQdD?Z9W{wT*TB -$>mi61Z{;9DH-G?L<(5sdcLQ!V{RFX;YzXqUyu%UaC9@zQKdSdxy(Qdc)*f1|cC;COd66fV9yLa> -@yWCLi~!fB@5rd?Y=O}8Rq_4ja^Jb+ls7}KeZ?GV);5tiUh0?;zT^AHq9zU&}K^VgO9Gov+F5 --_X)N8EX}7-oVfu{k>ti(J{}fVGe9Gan(5#{JSJC&#Z=Ik+)i0l2VRBmJ_O*Us$(PN$4D0kaE~AhA0Z -s$$+;4=W8llOnAW40Zxn(s;YLylkrdTemG|H)W$R8^f&Hjn^a25Vuve2DWEmf|o1$Vn2f211mFNG2c_ -o-*@5UfavSvJvP>g||1vaNKq3~gh&(LatKka-b&kiiKI2F-+~MYuv2x-$$Wd2QM|GUMQtf9{JAw!}6sHlwz`NT_xgm}>EOELcPP)UqxfD*|*D -pEW25Z}Di>czn{WfD6O}t*cE!EX8XEa0#ph4f~GpA3FP_y|5C(5vn&K?>144Ao&sKbl7cKkt&TY1N7q -Dr0kx(uVJDar^?uOSMxbbtbiUNH2%O=WPQ9TVNB?J%q2!-F6gKLf}JcX^n38&Wf+=Mg76kkZCS-Z@PW -J_f!gvy1Q1QqipiG&U6}{*gMva)=x4iStS*#KL_5_K)P{B*}T|;; -RCe87K%God&m1J^{OPg5Ie7KHz=0bf)kzC=>*Txa+_R*LOHLq?sXw@I)PAw4T*h0T-um8Dm7`L=n4j1QH^5#F_*skV9v_QQt^J?u_CV3UsDKrDI~J-K4s`q(vzIv11qUu -I>kc7x*%kX?>m+|p6)uI@En}AN@%4T_qhCY9@UxRhJsM0CPmd3CS=p{D;v$C -wFktt%$=UCEhlwSc*LdjVyaDt3c^8nM>-!4$99#23iX$;H^BM*U(6AeVj;Oq*aO5Eb&m<1g~P@|04t* -#p?QuI49aCHnFW+R9)qZycb$0-QQjAP3AJsAdA8>OkpDFP6;d!C1~N!3(n#lY0(5-{=az-(ae8GxCg( -1AX%cNi84;Ms_hQCmr-Vv^M3c6W(86`U1JoS&bC%g -ic-Pk<02lE3P%VJ)twy0GfgshX>8D5}JfkuTzT-20PrL+8_d(+>_Nbici7;RhcTiT;xeb1KEcR#@N=L -Fu=~rj__Xup&g$H!Um?i2y17@QVdAsLPEcH{EYR$Tt0L;o1w+6rC6GO8cdxQlM*whxTb}8h-|tNXL|A -Z?W`+48;gS-Y@fixX%hR5$IF(HVr$psV|G*(}1(3a8M!w#T5MHW3=Z!U -g1#&ZYFj{o%0%*6h?^9<8&m{JmldPr_qSAAN-5~QL$Z8V+9@2E+-qgu1=1{aW4O{2p&(-@=4c+rJ(Ax -8R -DmM%@jsKBgpFT#I(AAsWXE{wM)IiWEqg=(K;BY{A=wB*aA1t*(*f9y4hUe|*6wVNRoG`K&=-TT7hKA_ -4E3{+`k1n3mEsXlL?HF>5XiH76=?RPFi@;HBJz=balxtuKK+@;Y~h{F^39t!!(g-g0zO0p&0oe$_8KV?>f>s8_7oJ`SS3@j>^64=@1N -vmHN)C5i%bD|0~X -e|x&$1s=wylS(jjA8)*O$18tE5Ihs>H}r%u~l8pUQA|(wm92qJfkad)E9v1E%YYsiU=u!0WKigX;rp* -qF8nOR>>w0`rOsa&}Pv{gZo|C704$|6XvfSm=qFG~=$8G^V2{Rg9t?!YyMwzP<%x#@Shvo_PjR9EQAa -+$Ix*h4A2hvS$>058}X;^4(@nLK8U2kPyARE#%k(H{N->yYtuGZ^m!8kKS4>81)nn8{;?BO -XxA7Selr_=BDbVGih1+2>74Js@fUfXkwPM=qrFmF%Syqx310L&zET|r)m1#6h&li_bc!)@mt*O1vxcw -9)O*@-^PZ|5QO&=Jl#}JtulPW@!&msWr3)$qaqMET>9jE>m4M<+=`%3^u!~NS>rmZO+V5Ol@RYTXjT$&I}F%AcA*^4bcUT< -(RHP)-$5k{n!!@tLG1Fyuse%wq3k&4ALxHo<~eS~xfFPC(rD8GPkTpQJpJl{!(|seqNAOOj^(h1a=BL -L^U=mRhm~nIZ!kdmcBn$3=4o}6%;^k?Q++(1-?N%E#!S2j%_&i(7%h`>I)bE8r7#vdO)9nB2c}h436I -$js4#Bpkm(#Y#P2H%dWA>2rUqIqypDQt;L;g~dIzDaj+GLZ8&pBo0z?oHvr1sIa$d;DtW1pof1nC>ltZ;ua}7UvZe1BG1qyNYC-TB_9 -Lhp+O)E`}9*AG=q@`-9^HPT&EI4lOZ~LOHAbxVVi28doyKSQ?fH_r(dmiQrrvyYa(cUd_97Z#y|1Goyn}D@69D1wv%%Lr@BnpO_u}t%5C6P> -v^y}5M42JA?4_KLZ7Ym}`v8g{aSbalo&~qcDKYEjL{ki8`5AD3`6Q$TzyyMBj_|De{Be7^Dou|KvYVp -D^I6466^u&p^vRwS)AkHH#)KKFoyf`Q2_=0q7ID#p;f3M=OvPPF` -}`*K>PGU&S5w>>rPWsiCCs2c=Zq^PFRsWrz3e(%iM%j#+O7n;_iqw2YVdH) -4GK)Y=gk*(2XIU3eI%6NgRBvC7gtr3N(zH3{6OckI$)94Uv~`<8U0haKl&-3FXvrhCw7s2;g -F)eSwEQMn4_II}5Z66$bvwggc2mw5m5T6e*(}_wvv_HfL1;gd$>v}Qch(|gL&|=5CO&Ob94TX1-W!_R -mr_ZD_NeZ!p{iD6VaqUv0>BAD~D3}sj7(!Wvb#PUR{@1p&E}^1$JoIi3ps{Ea{wo~7s|Ro-P$r)A;8; -IERQ|cB8)vx9fhhd2lUmd!PECWmFz2A&QS=pUm2$u=2y49^$IpNGfjQ)dA39;1ePRm^S2qT -x-)&?I(fpS<-fnXQ?Q#Jx$XItqVJ(XU_yY_Iq>{V -rlqs0RZ0+(=RR=|K#h8$%9fGfa)Q)eW+#29iY$uCP_fU6l6!C+6x*0>g1f*w;S{_q1RCqMk4MAEdvV0 ->UAN$;TLOSrzi9xCwyaJHdtIpHY#3GIyk@B>GPBQS4$vpPJ83G0xHbGkhhU7#;f8PcI?YulJGjJ*5Wt --r)pdZ#*B1@v_oP*=Dt9WpmlS#TB${a$T91uvlJi$LM54rKE-6ot!2#zAcIdtwqN$=AH$$=6(Sehage -+RADTbAzsh0alN!jGrjxL}AA~*0&rFZKj -4IUNLa}Xi2^W*_Dyg`ZeBXhn|pHr!wy*BC(QUqbSHg;_xwTSUxdw*{E)CrQvAl&ktZVX?t1l)9#muD> -&<%R&_dfaJc_JyF16@58Ll|$J-yi8B2~f+dUCRC&~Jco9q8QK6??J#Or7Hb9|;BJ|8?Etamr{{GS$BO -!tYg_h4_Za`5-#xBDOb7*yf<{60FJp2lCFPQPw1*CPHLs{=xj`a;PX(Dg#jTA!V;%NZNgv0^?$3)MKq -u$9{{(Fc%%s3-${yk9(NVsUf;!B^ChH09ew^RDlL%bIMX+G{6lJUtJGf!& -vGfACXKe<)9%f8Mm+Db8AuCk1DKl%Z4oYwZSGM<;ssOdjdYV~}<4|AG-1>hG@yCNj><1%;eaO~N8O;g -fMR1WId$RbR4UOi2OFv+FciJ-&q>u=4MeRl2%Dc&nt0U6-VGNS>VD2Kv6h<+B{dDH7Kr;QiIfKThibR -vi339GFWe9diVBQQ0J;xidev0}Q6fn)_gu!jzn{x&-PLp0v@-=FC&MErRFexA-iLFJpLdz$SIQjvf|v -{!6JXJasq==3CIBPA-s83XveZD9a#v8E+1_*IPUUTcpT!aG=walmy$>lql7^O=)3}d9QoZ?+ps~@_bz -~l4J85s%dp8)M@yvJ`E}6jzRnVib{1-K1JoyQAu}(_zGZ$XQnR~Qw!oH9*eZ5t@doRY1FrLWvqJ|@d+TTVY)OenUoE1f2n0lK$QX9+R^X~T9&!e^_y*5`p5OLy@jlJ%@Xo|uKD>F9;R< -JGtS$9Li76n?dM2f0&qZ%qz>b+=n8rj~cCT$)qKs}%2N#$M$p+I)xE_7Qu%S>N1h@xm0l-C1X{Ts2be -B#@rye$y%e$-uDn$ZadG9#f}CwOj}ZofoPg7>1*zLzjGYL)pI%SDSSciC8?1T8CkO52X8jnnP=kKW1EK+aRE -xP-Hdsi)PxiV$)s7vbeuIRSe8f%8*m~`Bo7Xkl(bN>q+e6@>>6#7CzS`Pc8)t(%+qFz;x?>gK3byVnq -l-uNR+k$|6Aa%cG*)b%V~U&T+eDD5{&aONWC_U&FSPnA473;FXC;=_14D5?d>fPVY)trsg4T3#{7PsZ -HNw5gDGrUS)`ZIq%raf2*79c2K*<5Vqyc8fCrPhE#`AxUs|<@E6V-OQ4g-vzkxkHdnR$86-UT(Zu}N` -t*?7~!KK%q)em%}h4L$VM9B8_N5x_6pnJoPz-V)-XjX1^Ke}WwcRJ>QpW7G7i+6s6&0E$(`Txe!7J2VBSJ(5mC9lO4D4Px2h -EDGu@aI%3bS^?%RWM8AyJ-BCH6Md2O6E7Ud5?uu^Al+8`zz4komeP^NR)PBKs2nC<>1|26}7ypJ0{&U -s+=bu(d>hNR%fbSm3D}~7d5>(+D!9kZ{>nCx@x%2W}Tt`DG*M8okZ%Si@}>md}(MZ7I>&VAf*bm -w|8)9bGDGZAQe!kiR&g4EZi7CT@vhLXS>*Y{IVN64%KLn$UbCz;R_wbj}a8=w1*$-JTr55FC;q(--Xt -nVbDWTb<>OqL`K*s)kex;)|mI%9nh~wOj81bn#6Q4U!23ZTy4~gb4+A&uB31Gu5wG@xp8!3UB8l}ExI -16$~+0u8`=(Tiw?!B?>U~Yzi&_J@^xK@({>jOtSdz6v0)mQ7e-;HYzvet7>&r@^c1!D2+eDZZo$0@+{ -%X8u+05HjiH1Q+Jd>#X`1`J6?}ii$%$~D(<9_on5Fc!>EM(UeGQX5rxV$21q#4bfdCE(tTYBtMFoPJ1 -8It2?vy$P2xXiTe(>ak%1~Zc)SRP}7S!1?-MRimk(3&MYt0F*Iz6vHshQC>^q3PSx?Y}MG*(^Bvmyj~ -*tUmL4>^bX{}r<;Y4M*L;!d)J@Iz@}fhItO+9`ngxgCejBD{Y`=mT)H8( -TP5STbFV@gdD!M7{M08cK8@fhskz=sP||49YMtumF{jKGCGmwZ_`In6LZxS{Lxo;K@#T`N89*h4C&ou -kJX6HcAHa(UL}R{VGs6`fR1SB`p^;E8HO*LH8M{OJPSvofeOqUztoe?k|?RPC7x`w<0`4& -H*{``V4xekH0>vhb^tqd*B~2p$n6RE88&^jF!DAv9cB%v3?_)rFdc4nkYy|vVZzecC(I-1MBkCIk>Cl -{AA*NQGl6JD_=A{#|vYo>Y7rpgb3ya0kBjis;bU2V0Hi(83IFo$f8Xi(5_u>eULKK%e3yC3n!axnrfD -2xXuD?G$R)WEJi|vVP+?6sBozve(J=*<;$|?*8^R2@baDQP&Y4RC^|7(p7at9vw2j8_`?eEHP5QMp -$OZQ((Xu5GvDyZ+6J2C9Su$2$3EY?a8Qiq_UdZ=1^b_doe8iC~pg;8*`bMoJ0d;x$<~&WtQ6 -SItS|`TIHpKe923#_J=KXSge~4(RAq5F2KpE35(Y=tTEp$2t&I)n= -52PJ&C_X?#AS84fxkA6lj@&I@$A(X6b=WV@_Pjgui`0Zb5L2qDj4es*LD^c)AVjkr;uo?&b(u@ZZKE0 -HSO~97F~#rUPnOeN9HWn4=<}^er2x|?aP)Bf_mhrcti)B@p6J>PU#-7W#)HsUr$qgsn!g=Q(*dGKr(I -KLQ#8i$d67E%_s^5<*?Y<0NjDnYr*a6ZihBS25wx2t|1sQIi>LtUTdj3>R^^i&V@R1cz~ot3q -8x?JcXqPv0E?*8rOULS*jjWr0aot%>f$pp=Vh0W9=Ya78TIkqn`VC6ff&hrLbrPGgS{@1!?2&t!Mk!)e*c4AZV -B;WXoQv!@^Rk<~jd`wp1wULJ&L00CvF}xvj|_zmkVlydG)KZyXM9< -#@LDnb&UWO#P*>OMvdDqWQd&eG(0%n@N6`goH1=r{Y1huiW|Cj>mkfh5Mrs;M9&`rYVs-k?T3qRtyhw -4G_1DFH;-#kPlxlRO`i8Qi2xqkCeOP5R^X+-6|;pp{Wbm#vFKH66#hm$=3>kIV(G3er;KD+?H;tu^}p -F1FD&p#1n5J@ZtuzP&X?7U6j(a4OFqQzx~i_WQvv)vDyt!3>>~wTTea;Aj0PRBq)5sDmjt1l9Xf!7cX&5PS|S(W-%z;6z0C#KHAya!{l)Y6}LFo(J_;!318BOo2oO6q|F)!3g)XN1z^Dl -%N0g+0DwIz3>tv&%F#^+4Shn=QGj+kkc_?e=Fsl)B_Je0QQtDAK4xiVNd2>Sc~h7P;laQ*Rst&3N!;F -wEbmm%Y210)2bSU##wxn2jnjcwR@fJ%2c@9yB)}}Z_1?bZ^y!@D%F0+u$efrOE17PI-q0455;W>m^6K -p>rC0ya$gfVZn{=r?hUZCoatZ3XP4eqb7tD%k4)&z~zF6eB8}`L*+0#*dsuYW3i4*MERyUoTTtV-gxz -76l{;0CXBPOhHd82oYJw;IBFX3?=oF^43j2}Pj9kY&zE8t)8pZECBU&3K{4F4a5ZT#$k)V22>7qX~;r -1|*auOIfm{}A>@n$9SL?`RW~#oyt#mz$g2T@QYzG!j&*6ab -ORZzZ`dykQ^P1jm+ApSvnL0qKySu>b4{BovaJjME>}Ezw#QBdSDdR7Z+?gccl2}K*wGrZ;JMds&P-bK -xEAXGlS|Y5@dvo$Qsvx5RhLfq6o=fQyg_5?^b4+TTuwmeD}B4*W>p -w~jx~laOgQD-llRjc9Ca?(LkX*`#K^#HvQK-bP}jPKO4)2N>&dXkZOGJfup*Yh0cu&Lrw#apH)JK3ok -V{6Y6H(Tk0P!^n7qL=NAGp<=&Kui{k2`!Wv3e`1r5ZT!SkRU>~%eR2Ej(4scSy(Y4F+WY4GeBD!Vt!y -c|ds)}6*vyd(?bP?$;8Pn$@QdW7X8D2erIS{M-@rTD1k$N~MZDN*9VZtJ=F{^ -(;6HT11nNDexa-oOZ4b~4?DihyV};bZy4q@RhiCMC$R`p+9oOr`oF65>1jOJ1IL4{aXq?YtfT<=y_D-6QI!KSd}1_~q>R; -1u1HW4bM2e7L*w@$d*8iM}c)|JXP^9sYE7`uz0y&-f2N_~onx@P6;;{q`|5d9-`%R{zJzKYluUe)`ML -r$154zt^cKvP;07+RF*G~`^nmVGDxY1?W=Z459C~La%0b)Du8A2jUUrm4UpWv4uZK>%x? -U@W8Y8V`Phme{AtT7I>rCIO4GEX*Nl6@v;+0MH0Zx%CjUmx7minu-kaM7RQ~1ShwY`+M*IVi8Xo_PCP6q;({)0 -T>??Q#W^Q;&{rNmuJq}tQ(Q3zxgtECh|W1XO`gQM?Mffs^IyZ!o?DY)x4a3AfZ*!mKKw84uclO_nVOVZny?!D#G87|gU_2 -p)4{m}HH0BB+W6LIy2hXlN<-5%cKHEQl@#5k>Ml)KBON!j8F>?;KWz_8FLN$KNA!A|;qRM!3m8j#!?b -zF_(d|^v3=Ve20V5)@=4b9u_qw&#)y(MDDCcsGu$|E44m -nXVVP7A@MQ&M%JJ1zYOpj@)Ht|D`F@VEqg5rp`$7uHDQh8t2~D_7V`q=YvX>kBk9Pd>dZ>qf1uQ^}07 -1b_r7x=Z)t=A}i^7*Cx~%iAK7)#>Q%M_{9GzCZM%-BxsO7&bp-&~$VF(M=*(SuyXN>~3DDfg4)OxqJQ -_KQ7$ZKAUAP$rewHbSs`hwi03fw8L|0MIU2I3biQrAJ4>(X#wJgGL&GF5KTod`Fw&_s2LD?C0XNXt7q|x~G4@z^NOpowRa6Z$vL-n7BVnfdtt%(@$el -9Oec+%LAB7ZZ&)zjQxruyK*n&KV^~K<@5;3^4MRj{ERiVsF+< -c;azk$qwKreAwni=8%1&_*x@u79S7sU0Ju^{Zi!!vA+~Hvd~K;drbSOVNah?4@McTv8uPONp*D4Mu)f -mNylvJ~wN002c+shE8QW?@l&72VuAnMdFRy?4fDb@7!r=6fifK{BL?Mdmwns3W&2(1lk6^qfF`vPJ+AH!P3mP0E30@xUVo{z4>2m9F@X_EgtZt3Ih)iM}NA^8pcjV -v2^m>FJ!}x9C2bkaT-AnhL)NQ$XL%Wr|~kL~lskT2O;gMiI=kQHTN05%h3CcUGsM< -Wn+GG=tjQ&adMyZJD7!EUKJx;1dM*RIe!yTB=YsX7d%0#*;)qT2P67JsXbABu75BMA9Q>mQ?ds(IP>; -C80p03)LdOM78CfMJM7A1q-2g7IK3JI}+96JXU>oj=r;lN@DFM()af1?A#RB&zICq>Kn;Ji{?$(tSB= -=9U6#(x8*H5RmkI_^6F00Bxf~SiqZ8inBMkqcW3|b8{R#QbvklrhV6!nf=^|1rVftoL+2mJiTwfwUv7 -)DhfZg`b`^(4jGkuO_KG_&>I=YLTU>NU9CZ2H67!@=+`a*Uk4va3KnL>iN}AsbtTYX%k}lN2PQ -76Cj26^=UOiN5vm_sVhyKe>f+^H{X*ttPhSN09W|%)N+#*K=21E>1UntREgEzxhDq(S(u9wCc&$->SY -H~=|wBWH01#Z;<2M@H93wUb#U=J3<_)g4Jw9Q*UI6OQ8m|0IjJ1xRNu%;M_r_3*AsCtTd0J;gZyurax -`p%d#&(+0luAr_lm-@^&KB0+x$T`-9-2Ll-X9+;J5CbM3kWT~Q*INFasPdf*mA*;w)yO>+#AAS?R_nV -jX-|#8c9mJxe1A@PF5;0~x~>DeC*)igZ-s#BGj7g@XU@4Zs?_GZuW#f>WI#izvqw+(-n#NTdpw&Iw9V -)i6qQ21jLi)9OU9*hSW7O)QlUQW{aBq_8V$(KaU_mMdgvGu13bwnsRriQ?K^wqz0DhYO1F9A7h?{5Us -3kS2j>9Ft3Fl8GyYgSymiN%j>@Pp=O2$9b1q%pDz{X5>%^cwX0WQjo25ey9su -NthKAUJl`ldeMJKX+lkKT6Oc@Ga|wQZ!Iv9;O)^tt3z>he?E5l!#TY{B2I3nk-=mWfeN_O6(trrj?FE -dh{~?3&)}DG7(_YUyi92-eBoa0Q2^A)%RzLaKxceEbw(j-XRh_wX$VKiFK@zVz*p)nlPr4c#w9}eq%8 -#J<{9@x83~u*(!w~oKI1)pCNunejQ!l{iwx2-OHnTpA{*le5rHyhTSzU429|ktmDJMb1>`Ul6|Y~keY#nGtcC>? -NDG9mWz=w@%&5Y>;oxoRksXjEZyRyylwNVtE=kbVlB^ -gqHL(XwXuPeZ^}uT$L2Nnk4gPp%M^f6(${-i+iW{vIM%B=Nya|&b~X;2Z%c>BhpODLi -gmG{aPPB9%(RUQdF&l4+Eu=Nsv`<7WD+L)*}J!E -~N9c=ciH4!?R0t2Bjnoux_9OJ;~OMu3Mzg1zOUXlx!KVCrehX?IbcHkQkBIP3`eecMl)2oRbdr`SQ%} -;6=M?j2^K*s?@&RxgNi9OG!T54vW=ObbwC%+O<`>(2P14`J%ouNaz~y_>91Ckq57@DLlDdXkd!R`Y-ZN{@x$+K!TFLA}aLcYK|J&)-drk{Y-WMtT#oS}RaVN#4ZdXgY}Im}s6eBK`PeEJg?JJ>j8*94p={lvBSuqf-(iqqZRJ!$XeD;FxMwZ+Y>S9y8HCJB%iFe#p6Fm*26Kg?>*I!f`P_RPqQlUDK -!fach!woaTz>-8O>%slOp0ak4NN$~cWa7(R-I@%(3vynIVCI6i#OH$X7}6ekMA&C`e6TP?|A?4?}2ftASlbAAjC*!8cx2Bcq4 -SGZohkvwXj&fReMoLEX1P9-L)I@1r`32&fSZx)Q|LT4xPkg!%_Gzbkv41^MmVmF|WZc>{t4tKU)px^_ -Z!2oG -KXQbj=PY?IHtJyEsqd~2hdxe=nEWd2CfUI~b&V#vdbTEp-c2=un3f-AJcw&qILQV2n7a)k)r^7;91MmD%{+1<2Qt*>0H(-2~U^KFXtuRe(qrdN?{8 -wzLdxi$VEQQj~Vg2pMxGWoMj6(%Yuw20EKTk`{Ms<}?iqqA099Ok@zl!|7x&&UWf>SQUo)sxgM>P3FC -WVAXph?g@byxjY&;@eLbbz6yy@fE}i%hVQT5vl7_FX#Zl?R!&hf9Y7?jZ(I^UvUX9YEw+#rnXIQX45? -GW#iMse$f8J?p{70`jQtbRD*YpWwBgmzSWCKeXbN`BXCZc)V{sW)4K(%FsXi%ue=hk{L>dPbiuoo*>w -}%F82$x+Sd_W-Fk#9+2ODnb%4-e|`PBuf9QxEu)7$rJV(;7bZ&XEsx~xlsE4{@Ome$TyBaQ!$9@Ry!9 -b?Og5gU>W~-uIEzvo9Z;-Vnm4PeU4oW}Wx_-vT%1hNl6!hg$4A-bh)8F2T%zP2#ASfY%7jkJ+twvECCMQ8roq-2v>;jL9hq@xMNyk*M+|r<6keF-J -@p}G)E}q>aRNm17yoYQM4PK*Z0AXX@+}%{=*ZOe8T4zOsf)yQ{M^gUIN1#NTIL~>y2e1iSyq%4z-?~R -a#s8pVCBIjQFI#7Ogz~ -Ba*c+k?D86$2KrCvAYoQa^2Jn;+&>aO3V>6&UlYEtmcUTL(*|0_qau;;N1iczgkW(l$Z0b`i#N%E&Wp -<{z9Z-^nKY(bn7~QfQw(y%6wpCg2pWF~(#sfszRQY*M9W)YB-(M%J@8Uw&>sN%Hws5=39?30arWNLH`Eia%ek7sY1_ZsU94%ksA7>yK0@8K$;tX -XKdt|VD#CM0h&b7gq8y -(q^`4;(nfyiX=5SD?)M&%eCJA_oUM#L@9%NO;Un1 -ydm2z#J?Wuny+Q4M=aA+8>Kd5_v*Tc(LFZ`6|s*-mK)|#hg2s@GBj#?@lC^YHo*jQY~o+%@a>d2o7KW -6@(k1k!71tTvJ#v;GYVxdJbAn9Yn|is|N@Iseysy_0=lRXGWG#p{&)ye6VH$AwWffW?)=I=@86=k^0s -a3$WNYbStha4+HT4ws5lbhAyZHNyLTbP;XP7a6TVMa(9D9mAwAcT!^DciG6r;1-iAqZfbh3zx>=8*$x -9Se*{Md+lR~rU%OlotUjyPpVh0F!cUE*8Z+(;89+{VKpdB~Ep=q*C1^8z6K)w38ZT)oV1(ab?IV`!ft -`6ok4-3YKXgO_fyejHrm-Y&gu#v4b_XDuUuzybd9~#P%B%(7OXxQps0+1nU{0|JqxeG|Ae=Z5&daia# -XXthsGR31Xcpg7Ofh8|pESxPhgW6eu~4TvdInMJsTVb<1T?R%DD17aPrAdJRqz(=Pp@8n_QhvkzW)5P -FSf$huiyo6+JC=#_4@S}uQp$Q{^gs`zWCzJmz(gCR=UtOUKI~xB)JexK%e0BpCxy7_M>sz*IICaD_wI -{u`*F&3gcp$R8twBb)oLMf?VN=i5fHaYDL3hnAglONoI0CsfJ^CHdgu1#B9MwS|&N1YQ7WW#A@ktZfS -CV3Co7jSj$-Q$^e{?t28i^LI#*r@^s33PVktr@V9I@%VV3mS*jB2tnHmgDp}jKsg!8LsXQuO#Me*fZEZ%Aa!QA^2gW(am`DnXdS!*^4-zS`)|h3!SV55j>mY#QmFG&_JWZFjSgWRjh@P}X;(k#Ywm32{{v7<0|XQR000O -8`*~JVr=>FEei;A&*;@br9smFUaA|NaUv_0~WN&gWWNCABY-wUIc4cyNX>V>WaCzN4Yj@kWlHc_!5W6 -`fV~UYvr)jit(lqP#W>4$-#BTTQ%Bn6!LNXhQR7pyj27|%8FyKDxKI -yU`4Wnc}WUFG<{R~Sw`<;FEHeGJAXns{N`>n$Uz5bvJzn-&;E6&a~#Z{WH^K@2R`x$3nrmH0MizrPz= -y}dL`}*D6lhgAPued3gpM>n~x2G5H-+l4j#kcRzu@KsI_V)HV?-t9LFL+W=Zr5QUrIqM!B_1sR*i`BA7(>U)2>0${m5e$x>^M#)jQJ}%}2!T$tAUJV -4pF^Q~&3LCX%hCn&1Ry5M^CE**lVY>vxoog1qF7Zg{VeB`jQ_NXGMZMWGnx2tJeja#HnL9BY~jbz&pi -BIpkcX8EBwYUV49ibc3Aaeea+{7u+ch*g7(Jea-LdqRn*?$yF^m5#OWl<(hNJ!c`*@mx5k|g5{PFo4M -`~vs=uZ(dooG<1)ofuPNx&Zex9=}Q0?#ic*W__84?<>1Q%CP&fuS)uvOJfEtH3gge{i%JwX;JPG9ttX2@a{uhq$*gHz`n^>Q=*RLRKO>&e!+)HwHxF ->X#uRD4HnH9@-)>t?9s7!tGBAqx`1klOyDO;S}+;{WWnQ%Hr(UPVvm_InvT|l|5PqgeH5_<4a7yWiaf -B_v&3AmV*yLP>%%2{>u?ef&OrbZxXIx(7;*8(qx^C!#`Eo?X&BoQWx`FgV;~Wm6ESCUCpGR1*1-6_Q0 -Mo9Dm2MeQ7nf~pN46WdrQ(gUYgCHW_-po4x8>NC?0>2hCJRE(f~kR`T5mkqM;@V1a~*E!b)3P6k}2uU -uIi-?mfoIJ&J;m!VIPJm7#8XnyH6)J#9E!HW);ozlpM&;Wf+X3L5Tbmt%zz9e4jOQ_%a5}b`Jto- -&poRWw%VF;ftar)k2~lC4p~qd4agE+vSi0Q8@adKi`(iWpKwDEbA5BADd%9 -lw1zD97t_#ZYnKHsTMpS8c1=)eG3!-pn^#7gm&dJ*Kw<=lR|Di-%}waX+nXD$?$d0aO*go>{wgk9rkO -Fvuh^?&uLAe+VK_c&Z~mwp{8zPvg;L0JfmoWv8&;&H&E>}h&k{gut?>{UaJk}w$Y&|ANk+z+sp@p|s7 ->t&_HSuwY@MxY9yA$k9g|66I>@DMs;sg}%8ijPc@WJuKmiGbDaER~-piQC4AY@L84Y2SRZSKuIG57!{&91Ma;A# -t;YcT$$KLQ45#mPY8lzrfQ$)ZA3NEOotW)00j!OWr?d#=Pj)b+WS|^pRT+u0f*;u-X+g^owENB5b+nP -!;s0bu^zM>0xPA_-dQt|59C6^L^oMuFTzBH8d?(!cvxOJ991(MX*FW!9|Ob&;dWvQaFNY^=<~?NZ -G`QamQvWmgp0V(~i7UAx^F1#H?8uhz3k_!fdlyTROPvXDg978{!1_(3s|V>NB@9Lz9f;%XS8Gq1Z0Xm -<5wq^T?&^~Uf|S3YP0*8OQUM6F|gT%Uz#`N#V9rT<07!z1ND{#>~I`M=TaC#y!Gj4w6%|9Js{f -4^%A({tFt@AOq4XMet@+BrKpJ38u3zCU??{_fk;$@>$uNjb$)BUsPO`eFUrz6=i@IrPWA^x(Ji#IbGv -c6uA!W`Tp{j`R9X8!NM*x4le370NF79`f~rARO2uzx(r>?*CqfV?;-Lb6x|GPskr8IxP8ef|G;Qu$5G -9+WreDX;8tFDq1v=LT*ua*S7^u$@;HZQA4@gB*=M|u9k?*!V`}Yn`E`9>;{mb_SzxEwno7uT7t0g@?{ -(qU`<<6dV_HZPAAGa1b{J1A>Z(A)a{RPbM-3p_{8h~wG)f|o$#snS|B+7i3B)`V9w}TdS_@@GnFB){3=r-Vj3azk375pwCP^#fUD+0D7EUi_|RYab{Vq`n&tFc_& -Z3IISJkb>pl$;3QDxO7wQZ_l-_OBQo69wr!^OxWUxy)auQCNc`O?NOZL3l4%2<~@K#2c4QR|Sj^fNX| -cMO#C7Q*zk60Vl(r;6;E-R&$mjifa^Jl!wtA=F?)E(6oV4eYtY*TCR-OG`B_A5ZQV@Q&T&EgVK_Gn5d)F~gp{gXk2709*m|u|B0~UBe@kxC`oRDlQ>?!>`gbtmfAD2HN^#;0sh2Mw($w_Mp=1?dfg3p8n?djk-gBV7^al^+E03J1XB -VzWvW{hrq(H_T-DED``M~;p^CHthCP-Q4%dy3szjGNY*J#)4;B3sWwH-*F0tmjDB4CYZNJ}CI`6%D`2 -9pXj$4<@(|}<*Oh7RLZW~tS9^t2Ck&sM9}LuSX5#~8?M~h`;+%4Ur#RHe05GoAtz!*?HiYE<%yT1OH>J+GL1x) -B&?zH{Qaav%6^)}(TzAt%zw+%dfP<###t?-!qlC;-AbE>OQ%70y@7A-dnh5-p4z}p@#*!qh++9mQv(j -3`g;hK8QZ;j<=A{nS1ouj>nj2NedKLf4Y8+arIzO96AXu`ERXART@HbilZ2uocd= -7Br__BD{9*0~hGRh$(eBo+`%@S?YaI3h-)#?~lt7lH7UrGiwWKL9Sf3ddhby91C^eFtQW(P3xUHE)Q= -7Us~nN$rRSaZ*dGGUjHm=PqzManzJv9cwM?p}p3SWKE6et7BGHHs%aPEQmGl$23ap5lYuc=$wcS%#3d -120f7pOzB$=A4Dq^m8k_^zW+A1wJRdFMeZ^60@UvSY_Td{r8!H-E9nJ-$=q8t4DIu) -d$H-rurH`*#iXTZFqjsWWRSmR-lN2HPwU5pC7@_iBut+5#Nbv#;O0J^AM3^aA}sn|j;Y0g5&94!Yw{E -C*ad{MGh@p@~Hb)@2z5Sgw+w$Q!^i>xYpywl6~mL1k9YyX@D2?lbQcV^KZ$<6{au*osZ808V$j6R8R- -pij6PsY~1To;a7zCjzZDq$Dg12q$0#tB76=(At(#>-A;+gieJd#DRp^K&tG~1}?MtdMl_0f`f$||1xl -YC8BxlRKsXRTtpb3&Q?i|?j?G+P0TK*wja;@TYq`wJC{>}>BsO=!o|Q=o-cWfiA+$@4g@2lRDBCi^dO -*h>X=zj@0laCV2=(_=x1RhH)U-+d&&!6uCj&C_gZG$CgRGp(Zfcvn8wxjlFy|wXwY`i;aUCQPk2 -C1U39;so>P9I>bN})M=SeFs{ssz_hJ`W7c1~Di~f}l6^%~I4{Ih_wUNs#29OBrDcS*1%K^)yG)T&sH= -*6H4_!mLc+4f!K!Fc#b!r6J-*s(Il{24u9QVhrNpb_ZD|aF{c_kiRDue1!qzj)CA}knh_vK6>N3E63QIBr>rJBHG1ml8ekVB|YssEA6nL&f_f{(>~}^W2|{;Fbhj) -;4!ji_G6bLvv$^V#i2Hs3TnMNt$^J7s3`ol65TvLBV)&ZGwixrWlCa1`n1C`ya^mIwnz$vJb?O+CaO~ -&uY$uDMFjlO&6l)rQodEY5DCbOaR0yuEacA^UC#>@jXGkcQdw0S1NL8HW6n;5k@AfLqa?9xYrU@M_@9 -H$>Qh(3MK~!PYC{kE}mi*G{)BftvSbVtbUMaW7axiEsp?dr;;S$#F^g=b$NAAN%svY2J6e?Jye4^r9q -aY5w{skf{0nVYzoG`2=3#J)oISNb$NSLj-In%mC5reU=s8xp#E%g_ReEp5ip;HtHlz!5TN1;$zO(dS4 -7UU&0BaG^m;E`b~O0xIl{*@2C<@DHaP4LnEmX~dvW+!9+kBOrBR)s#0R4xnJdqTbR5piUll3z1Z^RiN -Ucqm9UeVDV)kc!uXj{}zs3bxdO^rV+u<``xoDNGp$Qqxj82Q4C -yj;27IvG6}|5RLMp1?3!P(Y%s-wrvTM91j7>LWTk|mg$g?-i`W(tuB9TtJX`LD+Usz%tIifZ##%C2sG -7eM=R+BOK?!)HA=eR9MF;19V6nAf-q<)$)bE_8u0t!uC>yoEG|}-d(zCdE9#dkhZ-(L4oA+%t0!UyVF -m|kDa^XKhRqa)u&;X+2Y~%cT4)bie6j~H@u7zglK;$B08X@m2i2-V00tnjTOmSnb8p%m>>(C=z6TF7i -v@6$;z9x_!wgbLW9t7D$iHF86RvOxUB-(*c8EFV0jMwnfDHZo3daf4q6K$**l -F8h}Qb2cDTx-iFXcKE!Hkp+17;2qv%gPzwH4W7Rck%vCkKY!6OupADPKE>nf?6o$y*Bt#V@L@7!0~pl -z4qbYf_FWj|h1dHilSyiWcpt~F#T1=O@3ov(E7)}pVXpf!Ednn{<@&j{wYHkPH|h_^2At9nwR*4$AO& -S2iZs$iTFWW{D;HK8^mrbHyvt`ZOj5&D#;Zau+XLls==Gj?y%&2DI_eo=D -aVby#peq{rb^m=xm=i^Pf(i24YtvX?ZxvPVmao9uULd!-b@$-k!(K%P%#6gqx`hn|(@F@mf$F96e+9b -MM)wyRbz}#TrS*o4@S8s%<3XWQ$nq^z_tXg+CYflW;4;OB)Tm*EV&yh2NK6h76!H7?##JEFhBLP+g@m -VU6fQ5@xD}JdPGo`y9F=3)aR_ -zcgL6y8weOkjOCP4?v6o7Sc|C%MNtkZgi_cx3U#^_?P)H1<^XtG$;vG9iHQ}u!~;2=?~7DEmAQqbvJ( -T*aOC&BtlyK{7Fb!9VV{0aYxu%DJnZ(L4+ev(A(n}jSYB_Osn%N?HyJX!?cR19tcbl@r5()~(-T0dP| -%0mmlgV6!x}mVZRkLA6Lr|fST;D+2qL986za{qc~q3`b~Ky*oZZz7)PRq}da|KF5X#Jky4+wN`_FnH< -&JhrIkCfU0n=MR;~pDa64gUnh}oJyMP7%nk-&EWE_DpD2Q5S&Ae1r#QOs!&^z|0qpvq8eHULTigmuKP -1zjZdf_9309Q2vp^FG}z_kjWc^H##W?HU3p0KqzVRohrW{Pf}j3c8%_EpPVJ?a3%Jx7EVWJ}7ZtvymE -{Y$%$Y=Df0}?rW)Md7D!W#``{#@TK{gLUa;MymK9O&06Qx4DGyuWA!Kk-t@_}~ -_%cnfk6#Gr*HruWY2XsL=6*C_MEFv5QLGv)=`pFQM`F@=DhSnPl;8})(|Wk$Z!X|J%J2sdQs32~jQB}v;?0V!e_nTdlfC4XOD#>4X=RNdZ5nwl3lqBdP@8w{XZ -V#n+j?7F)CH}je^Exn^eA&(;`}(@{b37XIBPj4k8`V)G)~)u4dya)?e&Dwhz=!bV>HuFb1y5;ed?K}y -Ynk>^9uET-uxIKDe&`?Z$JUH-B=~lh_MGloo@~)780w -l3Hs1b-5f -rK_QMYN&Mv^m`>lIJfu&1%vRwXj}U5rrW*<EpSZ4^BYv|1yleckq`lvAP%7`Q3=hX+70tLx>2L_$E&;-g;`GpsCC{+0cXZ0pb#~+18|GRVK3{#ir@qhx41AYAb1)D`Y4)fN|;wu_ap07CD?;rI%_5~jH#$ewig}6zD0AnqU5rp%?uW|%xp8| -91wyjV$M)sz@CmV}qg{#G4L!ZN7_T$`oi<56bLFHa3B_oLj1eoSeV?$H_M*=T --QiijjbiZwiN6{KRculiPZVR{rcrJCJ8^mT;Y#qgO7KYdQ|D`a+>3@DOM$VJUgsgr-Ysw=PTXn#b1sG -lwD~imD?^ev*+Kj;d6n@mLyJ)n}7!H+kfz{|K;Y^GS@b -X59HdP)h>@6aWAK2mt$eR#O}p4#qdI0001B0RS5S003}la4%nWWo~3|axY|Qb98KJVlQ_yGA?C!W$e9 -ucoappINbA*q?1h210)!}BLo)>YIF#a9fE_&BtZ!d49t+l1icH{adcfabOWvg5<8=5+EI5e-rsufdao -?Hi@WY!_O5)m3d}H=Nfb38i&;U9ZdPmTtOkk6KtlR=PIb>Dpzhu0eV+Gu|9N>xcURS^Q&p!^f#|QXjuB@)hm7XZp`1%@S`6z-1Ecy@89#l13%*P| -7CrCgYZE9y$|G<-|5c(;g8m>zac9tqd>>;<43vm#h=_WKk@ggJs0NBgZB*&y*@vOzSa5PrSCW9m%)4C -e||Z?h{E1I-%sP;*5QZr_b`30xcB=$#CK(#T~3BsYDi%=f9KL&iMVdYXvj2VFwCnEcvg!myBQu<`0Lc -Ef!{`kNns2$J@IDt8YuDp{RIOgJDMpV!Y1Fz7yW1HXPAW)rGa_Q^$kRQ*1!~{!|MdYO#KJ6-|)p1eFX -Kq+wkr1H}LDX@bG@`PkM#0t>(*N@&UyS4eRdV?*WjxdfRV+ztH7yP_E+!4UFlb6gFr$KmQtB=!S-k4c -~`v+E!>YvjW~D-+-@S{rx`zK;R}y1B%aOxDxKRZ*SrM|L_0Fzg2Rt$G7k^2czaL?Q}45-!6XkVkQt0C -Itrg`MxSh@u;~~khEJ%;3xYIW-^SQ@mZnUea~W6f%P+Lr;-kkR)DmH>S`ljN4#Y$;=9Hm-ttX|H~a&{ -&An!pVRo6B#f$R!DieVRbI3<+5m~ky9H;rQ$@t6f#KILVMlGE+1V3g -y*Due7~3&X+kqG9E2Il7j%S?rzb@%jz|Rf5i8o9gpT0VGo6u+-Q`{7{Uo!ye^!hJ7J`SWb$dnSP^J6^ -6-E(0Y9TJda_XmYgJInAWsFTGcB#6&2Usq@bRO8HA=+hG{8W)FFlV;ITVFGD#nK@8P=y@(X82oF$7=qJ;2G1L!5oQ7)2eYTK&kb5fR -I}6Q_gkBe^$7pA7p|jtg0>#z<@YAE^l6DIcajsn5W2)}9zq$h?7r%v1@98C#HFP8>TJRIL -!meFa%iOD@frhlplM2`3iDEzYDm*McOB8RHOu6LisAkJ|Zj$I^(V*MF5GMpyZE1=^Bet$U#7_hZu7xO -WNCnQ;NwpP=V;`VNlkrmD{xHdgMMeHxF`&*D!vg-{5CFh{~nruB4+pFd$ge+;RknrwWD&$fvO{j->Is -HfTdV9SNG7PlvQaOYey{_nu9T>SIz%K^KdJV%;7XIbN?+UNBY7U5JHr1C2ZE^Y2-D>VUjWQN --Yk_rxkOD{*=r!L0Xt|3N46ADMV+E}!?uwAQG$*g+v_MYZY6^$r*k4}ffy8-U7x~#TN=AtsRLylkSp -b}eIp(^&USKz`SLLBnYHkjs*`q?KMq=;F*p@5b&>a|-|(;()xXv6-s&JyolQ)ujdJBB2U;vEJ`o#Ck)tRg-ncq5qacH_4^06AtE;Dgbf= -PpI!AJMfEw6)cES?f_E{T{J_C&*UfjSr=J6PUCdJ0>Hx>u<#CK$dYSWv5l37VsAqIA>g<8Rw7OkrgAP -20y*dQZRkZoR|$0qK9t8DNCV-E1x!HPf3xbND8UMD-%GUWd6WVcF=~Vy2%e4L`*m>P(|Yk%MjqrRaR* -W{;h97NRLEFgk}7Y33r&UZ9k(7wt2JYaolbXr -`On8S>@-9c%S4q-uDeYHda_VuT9%IiK(E*y=aZ6Z6E3dhwy@Gx@v(0+5rf6v1MO-U5}wHeG`?{Fu{gm -`5=yw4rY%A~T^F4O;kAc3Pw&m}ctM=yPCE+SoY|I#$g!LpRrp3uf|Wf4-99pDBjSHOpOOfjQBK(ZH#= -s?Gy8Qwo9`jLJAHuI`(PllCE`CX^53EarhZKDUS#P>SK1JS*)x3ouGnG=OhEM2f^HKZOBXNAQ_$c-Kp -o+uF3Ll)MjNxyu#`oI#RwQIgyMaWz2^i?I#V$Zy!YHsp7*%@tS@#6$JGKV<7^?@8T)z6Gxc-K6B*RHuSLL2H8sI~_^vN`246{DTArO?8O -lr3g->ZW&hk|O65s_Fd5D(nH?Gw>D?gDT!!(G*vquC%vs~@llE_j;vuN;rSzm__2D*R_E|2sKzKI)H6 -eq2KinV`{);@*SzJvn$x)3ok>=y$-cW#Z_OPP>VrXdd&K>ux`nny6ui@Fd}*F~<*r3|XY;Y@G>`*QpA -nL?r8x+Yke@1N-+SztUN=_s+iI^#=L@X!pT7 -cKi1M&Xxm4YUb@2MCAl2h>laJ_dEpn4tA00}7%0fbr3oHJ -|WW+==>u=XP?W5l_BG=roE=@g8ADD^}d*gVXNfqi8@j5QAs+AUuO>~h3Z5=pp*Lnq$VP(R1XkXkk=E4 -49NWx>!c!bBYeqiN`bG5L2bkeDl-{R^+t=73T$bHcBGLt2*rR>K+(YbUN5h*C~Up`b$&c_1$ThO{_U$ -TvGO3dbui!Y6KF`>uTAs0zE>gW(yRv*@;KRa>Y!N9b||$5Bem(t&ysU9vF7l>&D5IUA^T<{Lwd+2czX -z$g3mSdyMjLLv+!9k3#pPNn+{Q`HLBbpX9?g87hPVaZu9wU{ -np62XVe8XoJrO53&oZi&AtYX!ws-LCoIk80O&Jkal<-JZj{)y=@~WrvVzH+9^iO_K}8jVre&)v$l?(; -=?cUQ@tdMA2l-|7set9{rNr@vRlzK=qi@{4LZI?TtAGuS4oPMgDI+|3%YE#1wMB;t-oa9EjC&hF7Pb& -WbrA$BNuN#E#b7>CDnEY&hS39xD%owA}%$al4{RNja}jwX85)bHQYf~7cfjo8vl0)EK+a5Y{UssQkwd -Xh7Y3>C`xM23MnPdE<8ksX6;8NuZvpr=?IwVe%AyYD)C6C^Kc+3zx;Miaka>S|)MkvI -fWXLRFc9<~{tc0UxMk7G474$W4iPOrp9M=>;5YLmz%*jci_}U~G9ze0}vEL#3bQ8%hD&SLbR>jCK6EYP5gvr87 -hBql4WT0SDzacwvZZJ8^p+~$m#Zxxa6T?P2QA9JqD7t1qE7CB;cG1l(-uL!PGY?}`3SAo(ZaAY!LQ3` -G73Yw>xBK8N9%7*$(A_+z#`cR2{d!0W7$0BKE* -_*v-G0h!b|~zu~*2n%y{#7D{%a8Z$Fi3%qH6V_TWO+jesG;5jjFY`7&@p3at{VY+z&sYb-d>=AxylsO -KgSsCXdPot6yQRz_z=^=(#7#(sK4Du{eIx*4n$|JN{%dwhXbwdvM{X9Ao11-n7DItE7K@}LePqB~`tj -={cAkNOYNdUOHMs5qP?**GiTZnrI6wSoY>jkd2qNGR#T~n@RaY&=C@&IJR;aXFZ9IqV-DX)){SJbh>K -j_RumKSnH2h!uIw34;Sq5waPCJPwHjaDG8R67i^YO>gv!|*wv=UheZ$W0FR&vAJwMC~wH$OBPg5!+SU -$-%)AXU?Xp#UYiqxcy-B#!EGWaDWew0&L#L{k}10u)<=2E|nVl*|{YqNY*^lc2p!K)B2LGCr0CI^F^EVr3d?2Yy4)$+~c%daI^bn6Ad<8T_lZ%nIu~zg%`&ab=MFW_+?;)#I -^U9T8B2Ep(x#ngt%7CbN{2n@0f)C}8Yy2ItIiUYvez;0`Q;Z1uqYN@$jIcx_!$2 -w2=OBw+@z4*{jy#wDwMFRq@ptFiS!LywmzC8$F|=fz9bnj1~hTC?Ba_K+8}AqXPd9^oDv!d~;jM8rZS -fi0{w$7aKqoF?DN%3Xzjk!t$~j+&q?r^6Rh4d#=qgC!}`fi3`D7(# -my5TFxgWN7mk-=sJ{4FQ9d_krs>lu3#9s`vY1K08rE!_-MW3OJ-AA6qaOmSI=7luto!$T=hQgG@sSSD -DBvWb>j1f@Pmtppg}XEhFnK`G?Rl^~5-Q@fJhv>-z;sZQ2m4>z12thC_vpyI$BR3`j-p*~GL>-XvG%(Sos5Gmj`?N`#>;JNr$};X2fbV8lI%Bkpl4@(o}C+ZK8@mt1d6c -1$NnJ4Ts<1K>yLx($PY>rmc*gWVdi>UQf(>B)g4_U602wljX1J8OcCJyLD=`QMh*<^T^I<&s6M*NTe0 -O0`QXvb%kVp9<4*9{G<^uthr9P6$Muy|_0i2}BaQo1qr!t((%pcA>gkYS4Tu)K3+r2A$oS5OArhSZZz -O*w6DGdt~cvYIV<+3@~0=^HgWIG7cbJZc=gsPWCAKW@368O;03FO(v#*;8Q?tE~)LQm&&(|aLzJ`D0Ss$L6L^2=oAn2Z>HmW>L@RINGop- -tDx3(DLc-!dQc<4?8CFfl$NEFt$|Q43}YwPY=PNeh&fa-LTQ^o`((9xmNVOSg?6{3I~JRFEb%J=b^0w -U{eUnGUQr&DK#bWh0oCjL%L8bFwdsHU?(S&r$tTX=+o(cyStDtHF)9*#5e>&?5reBaXs{dQGYTYVh=kOWgh=ETLM~uRI*r2#qsRL49*?OW-qY74JE~W5W)X|yn8#t?9KyQnVegKhU0_joK -gTZg~kfVJX#nNo5Jqm31YGy(KC4b07#TRI)OmTZ$#Lj3^GAdk+r9`n5sWD0D2|A&zkT>GRf8=-uZNV? -N%T~?2qk}4!RA(j{aV`T_&nO}2#G8Vu{fF}D!cJTglPpGU<&!tvDSVC#xWXw^!?Q5|Ose-PuZ|WWzO6 -z(=8(@blAQEY_{@wdt$7e5oD~o9@Xp@!j;*6)FJW-OJSGaXET%+k}1)$8Z&I^D-E6IXLA0r&@<%$uWc?g?H!?>Ew?X;Wv -;S*kIssuqmIVFngdo2b{+Yd@5*j?TX2tQM26T@nKuYhM1~K&h8SSM@V&%YXF{9!$Y}2cJ|7woGw+Lbu -A}TapVH6*Av-!7Qn^E9tPnp2tOAddiTIoL}r0_iI6o=KT|_ -n7Df`grX_=@Ju%|p=e5hzT`)(Z7I+NS$47X^DKO-xj)A)*Z@F4)&giz4KAVX#9hxGy7>Gg?rE1I0H}B -X!yt?gacdjmz+{iX3)F8bV2$hw&Bv{+oI2ouG^!jasrI(UG`VdzrufY+l8G2mT-EEzMOLJc>Sx7|M5PSw?fvmV+8P -qm&ky*L=4{-L9&IVQ45tRFXlnYk8j?led~hloxUuC+K--B{RMS9PIU7^QJ6jY$x5Q^MQ6Rt?HLn#idn -!iV1h^#6|f$?yZUOd-x(T-oVci`;)!$!^Sv2` -r=f*HiMoQCM&gWLav{%wH{u4SqLt)`F|cXLntE3Ry97fC+XGJx|B$>dj5@ns^(W10q?;e@`UYyt4}E0F}xWNh%kZ1OS5(bQctyneQo;xGW&po7WGkM^J3^V}9i(8aq3E)o>za -hMs3!^Bc$q3U`pMR&?=ZTZdxI>`K~+T;MDV{xLF0r8PymU40*EvbH+8QvB?*WELCd;uFO(pyMT=}=^AD)x0IOVD%Stn(wNu4At%(VWWFkj -L$u=YI;ZU-S8&!Tmtn97=+Ui4Jo3+NQ#LBgxMh&Tanww0-mWa>eB*XZZ1_M3t;RkhT!*%HXhtb+}5)2 -ew(zHoBrAUEp2ui`T_|fyeLd4oUx8!v^?ZoFe)lT8e(^-0?-I_s=3 -up13ZiR4l$M7xo)jRMwq#fe_TUF_N)YQMS6d;5)}=Q&$@v*MuFY&gT5Xp$x8a^9sFfDI(N|Yu_f-$Cw -Y|B9Ec{4YJ_WVu@KrG5dHhE5nzpCqxoq~+_es@aB(0jeLEE4;)vDcBu6@xRFI&ik|439@&r0@;t;249 -6DKx)!ea5l^$q8OReu*RjN6>;&sq%~W`1+05qmDB9NiqVYtW-_R#*(NwGon=PL;0IgK-ng!VniB#zfS -x7NzgQdC12qT>xG#vE*TDAM}w|;Vl)_B{v;>O?C&fPAx{~F}b|JD&1dT4Z>exx7*{T>~fQH*!G%BT2E -9V4)RlHKFJvmI{Vh`#(WXEx^JD7g`YKRaY_1bwaXLu^?4_cE)vTdSzEE0&f%$TPLn2LY(tT=M;m -KkOE+6-%GJ233oEAHK<7spBTDKXdXb(5F_wObqejF&4NNV#rPdRSrs4Tln!ljVSNP?d0FIXt_F$BJqk -^?fl^KTD5#Y_X6cA78-aR^f<-&J{)SY8Ig9+9t^xEaJ!rZwDOn#ET=m+5Y9G8-nvT1@HQ!RnCMOiw~I@;keI$ttexO@l -J+$8T%LmIS^3xu6qLQ9^6F$VnzV1d2q2A3l>=gXju7L^~Y1yALpq*oT-@U&SKyz-g&b?^B4PmrL?4)Qvg($n?FAsuVT|+2ivlV?LEL6G@L68<9WQyM8%n(fWOW -xZbEae(Zf^>CWWR-OhB -BE6%d_cyoB%;P4$h+$E}71o<*4#Qe;)a8Fqhv~plr%4S8f;<}B<(>X>e&g4nbAr1ISB3EgO3;Sv3J7aF`B)cb&5c -LmblCy2F~3#U@-|S2xxm-L+^bZ7E?7_XAf^o!DQ+Tq!uF%ZC*jQmM5{UH5ajiP$_8aq8rzl+wb6W<%r -){QyAuF!XFEN@GX>)LG(fc6{QPix*S})T?vEgFjjFFK9p7J=DXx~2yScE1UIm9s;h=k*B1_hVDx( -d{|f4_jeLDjfY_*7m#jCIe*bsk`tIb@t%cb!im`o7xr<8@mml`s1dmvs*~>o5+j6K -_$)}H08^1LE9F>0|-Mbp8+l6Q^X;&Fh#!eZmIf|RCzbV3yb7C*Gbi#(aLp?p}c^YxpG!JkQ3~9IqNul -m}s!1`fTGQx%w;+RQBE_m5*ap=UL@_g8K$iYN9K1bsRI)&FkFis_q(P5)e~v>_YpWXZ;L0g*sk@rd?j -Gak4W}r&QV5DOIksg$N6@8D)Zlm-YJYS1%8SMuTc`Xu=rHfupWn2~XbI2z6)D`7EO59)uV58}X>c1u2 -;C)r1%N6t%cLiAnBD0)#NwRu|cY;~#C}i7vIcGYP47idmSV?kdpJv!UCLq3xeV5yUp3n>g#+i>&tno- -`>qnea`6;UBHIYXQ()tY;Y%8ryQDZ5!GiX$Rt#wLNkNa6wtL!sh}+{w#7A5|oatO+GcZ9S|fsV-XrR7 -Gg&Lsc{%RbSv)1A-fCxZ{SThAK3}ji9oSX4?tgl?&ToH$dn;X8lWRDdJd_@*g`o@j$zhL -O0H$T93vk2Ae&0*hF48sfd8&XgH(%;kD(8`6#K#IiH}e(@Z3(M%`&qpr7GzjvfvD|?f{f@9OCiV0Cl$ -8j+z#hgNtDU@6rAFI+QkDNXGv&0I78KuqBR?tfBx4fpMTvz>2wA=zT3TPYb=Rg%)a|uoiM?p+9P&axL -_{7OK)h|3?emRf^q&5szR5kn63BdT+?ve7HvsO*o|qVy;=m9^*mk*1R`H>Uy^_e0)UUh -)w-@7hbLyHrlI+MNk+i+C#|0Ft_~9XFl9^;ZMpC8xepRb*${ed{mKUW0E7snNbw?^tF-_nLGv?-IK2u -8nx)VswY}-F*^Idz|*I#P{xF*m0+QRW#791spU$yY#eJiV%!kjKg9#4g-+&r3lFISz_sN`hhD(1-o!$ -Q53syG(=M{$_^_?!>4rPw?}vzMJIdqRaXDUCPT-keBBUU&+gOZXk`ki*tXdZmq -s0?)GSIklut3qvpYgK9R%fX}QmSo_m-&a*Y%Y1uM;B+}t3z$$M4kL=52?d)LOQgAADF@L`lArieY#z| -oUMOTVi-(oYPQSs`{2jh(N@@;8Q6qboS?pgzIU!Ua|?rQ~}T&mWjQ>btvXWt=P?c+k8^z+f}`^T{tu5 -DyB$(fkw`24spqZ#Ae{*Wlqg)OnaMVL+yy2hM!$6lhcW7VGf0Y_ZM+zzBKnR6^Dx>;VA4{r17Gnktm) -o`d;)4(2dEyx-`_x|Yg_gL4pS&i?}C1r_wb94g7NDbl0(4G^L_r{R;eb;)HveOcdtD=AQk#S2a0<^^; -${|nnmeLcS_kK1`vK6;shSyC~nT4-wL4lK@bd$ -Am7}xK*SudXpVHj0QQnSJ^I2M7r=At^fMvhD1Kzl30P -r|3Zan>fX6PSg6&)P1kwIT@VPGojXxbwHLnSH=VfP>jwmde{s9W-b#I`PB`T2g~-ELEsvMXs?--Bh3D -0M*kp*~l5s!vS@ZGG>=`_b^_Ww0z^`-Tzu1GgmeH@~I;g&4KhJ|1r1Fbxp&gdW*2hvHitx#BM)PNCSx?KMp^Z-9oBxe39k!10fD&Ax -ZMGoSL-pIl6^ohrYBL~NssZ%#t`hC<8$Ah_3y)N?XM8wP;XqUs%HnX&im9|;JA6YZQ!EM&x{`R*oqY{ -AUTq>p(zI6mM?#MB;S{-)^gTY6pA?q2a7gt+ -j=oJ9XHj4gEU;p95%H03-5SLVQv1nJ*xM+uzRZH`&`aPb>^OT)~}LnC{PCLEhJn;_N6hh!JB;NH`7Mw -FyryLnbKC1!W_si+ZJ7mf(`H8{#1r)Cg}f*$bK64avflJEP#(XIUvx#Q=kH*+ha~FW*}_fvzwNt -PP6WKECtaj;|9V;J{P0%F$xg1K9-6EQ_@<0K|ipqN0b#B8KZ$7K=WTHlsc}Z-||!V47OxrvZ*upy?k+ -K(&mh&rPHt%|7)c-j7TLtA12^P%IE_bomLPH*4oMcef&6ivIRqk82Aeww -)beulqF-t41VYCdswQZ1F-+KV=I7WwbKgnqc$CkZ|FV(RTaAEt!Ko-wN8h^8uf$PJi(7XnF@KAA#8Hq -YgXKHcJeTo!tu0RD@l(JuB{-Ow}WqfWl#LUH7hFn%mZ4 -}d6V#F1fPJg$Q|%%h3QG0dALJLBTr8!pLhNnfGf{L7ZXEmV|~b($m}kkAK1$d)7IPs2EE#BlQo8fFWX -9BfL*vm`&olAS$bsHhZg2x<7VStXZHhVuZ~tj6fKk8W~B!M5t9l;c`FxDDncz{@RBB5r?30g`p^Xxa*iY`J|JU^7(OpDy#4x&Z+%BAx&r=;U_7eIYFEvdwJM7_21BiZ`%5VWZ% -MdJ^{xV*e(ZsTjCBg-{%uL2X7QiaGS;Zu6WObRPiK>TCR4KnMDaL;00xNMLu3b$|0u3E>AmsS&#(^+B -2C3EEDp~y{_O8@;mf((nyw(SOPJWEr6i(J&X69PF*cJIO=!x}4T+ZqWoZ(t3Ao6A0V}l_f51oHroJ9} -*l9%W)NXglaGc$IOES#k_u0Be}$rZj*ZJfAB>YJ!q*7@9WX**SC2P!rgxxi<*FPVa))4&DZM@uV<6#h -Li1k233b8zA*V|VA#w`KR#$#~icbW|~((rB%hmzG)UC5w}VXoo^*N+~iyW7dwPc~e_kH>6=gOiREK(F -g)74<$@%>lPN%K2hnYO;&pX9nHy#S0BfUPs2F3`fg%G8*ykAL1mw(yXMMluttRZ<{>SYn_Pvt$f{2hi -%5!DT;I*0GsS6$i-@IN=#wRk_g^9&oOq)t7-66@gMK;#tPrFW`mcheCqIf-74EXQFmP~e;H9yF&}bmI -))jTKxTxIxP@D%E^XallPZJ*+21O4joCrs(YBehWf>QvW@)BLhIz8%Zht)vOauE`+YNx1y^#4({Kyk( -UG7@wG8|e|;{l+sD5&lP|3(&32CqIH!gNVA4JmGutBL}t6RS=rsBAZ@^ZrqQ#NM;4Kn;v=TGJsX10L@ -MWQ1?^2wsbW9=GWx7W$d4R@(DgW!U1S^p$dBN5{Sn+@FO0g#NC!rB>Y=_4M27jv-#X~w9Q -hG!Xz3Ot%aoFzW1X-zj{>)-`!R{U+CfhrZ%Yz!yGF!uavD@36Doo09|%;kc9RvL`jw?{T^0FM;8T%VTE;KY)b);UrFln>7UOP*r+(G^7{RZk;KON=Vwoa -;?qLY0MKUaSIIGuFN1|>hZYIud;5M0Hsa4jca8T6AY2*{h!)qX*391ofF#aH28ZL{8ZL7<= -BUGQ>s^b~Z}B(&(P(|fYxVgD{>HyyF84RSIhmRIx|5l>Tl;l?W9NSYKOGwNH?AZXK7zo4nL-Wu0=}dL -ii`RiQ^1`XU|abjl^N@8{F%^B_Gj-{3BoE3zzvC1Em6?ZJBt -=noMS>yr8XcB{QhGeWN}|=m~7uG#*STJk7wTJ3vc8@C$gZNSI45bO8lI`Sw2ELjF#_9rivU3r|@csnP6B5Dxd@=U{~el_f9N( -nObjS$R>g${=OS%km22OQdCH`_Q&&UV1?52dfi@E|hAqJs(O%4u4Rtwul#0eu{Wugr6vf#qJsMiSWgg -aCfT7OpjV9DF@rL7Bd45b3UKrH@e-+NimRT)=@ma3CuhjO5IV-wzA4Zn_B6`(=k|F+?yz_q;Z%}*Xg9 -->4n*77_VKEI8CA-hR-G2K1?$F)$kd+NSkEYBXTWYFi2i5@F~8He5Td_dbC4qM|X3LcIh6nay9zSr9h ->mRCQCHx{39Wb!pU({0FI8mGIma*Me^Nbe;8K)RihM4-3mZ7BzN~>9`6KRLTT=4M1hlMnB;>(9QWE=Ho2Z;#HxZKU%3T?|)mzlZ!hQ(60l7nl0Vz*TcS -<|6s>dol%n>-ZXJ5^pq$Gv}KwdCxTR`mcwOd)rF0L+ok5a$O;b_7;z1hdGEcWi8B?Zqun*@hWp{s%ZN -$U`w>n3-!=Wm_xrU!q1%}Vj!A=|!;>RP>Dl3>rcv;j>v{+#hc|t2mct6PqdGM -*smyezxQnl3{ZZJtsJb*~rXW_DN4P*yNIGvQepI9d$chFB(C&mM2oOBY$&j*)6E>0@Xo`b@AW%gW|Gr -cvi*jyz?_@C@plG1b`gKwax*zZ@Xtxb(1Olzjh1o6|OP~9Ia -Pc;uPOZj%=o`zE|ueFe&?UTV5P=^3@JMW;b^fcNG@MTaTJm~#!1+blxE2n8~cVHupO3 -DrcR9=o0I{UB#ppjgllhGE1aPRPr9hog1TT;tnk5D_n0amTTiE;Dx2~c=L_8r{*Oh?npAqy^=ksTAb{ -jYO}B@6~Sge*edLw>@;yvk1&+uH@RsY`Ll+(7wF(4^UAtt)Rz5#z?K={qLKTm!6kF*#m0CB|3Ch$)%B7IIV -Fyqf*%*)W44^uR-SrIhH@QpPMQWBpsX{31kTY;VQBH`8WN~~f`KL@15JW4&1ahjQjvkOjY@jG%xGZHD -In5fKSM>B*3X|(XK#ZU|KqNtyAg=$= -rHSi*>l!Pr_ampC*}~eVQCU5i?9jn}G+N|%_*72H5fIY}5q(}e0x73)hs%r1Vz+^B(~bx`ty4k{m`%% -cG;_L=#_|C-Q9b&B5hQn!*}&_Xmz(_TLgMt3auqrMZnEs5cPSn`rIN4^ER42)^)Fqxb5+Z_Nu_tJYbZ -U#viIGj5O=?;tFN-dI(mjI2PS>=)V+t^Za0B!2Po|{&n)?;7fF`E=4CjDH`|M(}aPC6vYD+?^>6Banh-)jr)C@)}JLc+x6i>N#`WQ(O+XH)n6*aA=@yz&L{q -PgLw!M}(fnRrX#iPkhRY7k=vVL)yVceAE2zO3+iUHsS-jv4vX^S=ee85-PY2nPkMjlk2Kn8%lTg3g!p -12uk!KflgH%BG=K3u87cu*eS_$nfBkIDze^r%ve?I -=Tue{Kh8#iHP;(pIC_UZqrrTRzQvC_nl1+ydrCcRGL-6p2#?io$u-8M$6A3XCjpmI0Dd<#`Fy#o2F`42K$l6n_k0(sTwb|)ct<)ehF1@SSpoF_++oe9rBogS&X=Pw6oCCr^R&@`nWa?OIlO~utuRwpu -waEH=_1Xw;KG7h!HUkfxl(lw+lRJ3ZaX8NM--H7=nJWWUI}HVHZ3zZDL-zjFgJs4k>Vd%tz-nt=%%a~ -`pTv6F~vnyHyhP#rA06w2#Cd@q~l~@hEMiub>injeUuCYi{dq!YMD4>;vVzi=~vq4YHl2 -NiZp?Oy7NF%UG|W-R%nVUs6v&e#)uQ8+6!^?$^|HiIic4S%_J0OxRQVycP?_vDV$Xx!xXCh=_g(^p%T -Os&iR-0qiR-P2t`@ZsWGDLlXo=X^2R%3McK|PMcEM@?71WuseI?35oK=TS@CUxYG7oHpt^k&^NM%ARZ -zYBPC`)q=Zd7D8gN(~e5$yGW%$W-Za%maIsIjN9RQ5R6fqk_mYSNlCt#q=;_%3QavBU9K1+NEgccb17 -DK91CVq!r`LwMVgdcZ+QL8O3IYRyocbJ{5QCy_f2%LE9b4J3VS%@ -p4Ik?BR;F-ePy6eUG>dhQ?;3&7hX>ZH{rWA*)DN|hch0?kGEk&IW?`OlrTqgxE+TN!59gFca?$Ip3qZ -R}{--shb3}SPkO+O#g-mWneh$kr!>rTh5lcOTZ=c`0w;^!5%@dbrpJ_iC^U_Yd$??dp|0lbMWh@a-zZ -%nriQ-hC{ -An^0pEGBk+0`Kh0@b>F?vngImftZnE?7spdhGU{o1}{?^#PC%^M!k5j~HVtl?Zsb1V}oF|M2DX(V)8pVEyz;TXEug(P6jlT&hec^G)BU5X&@Cy}m!>y+9rwgdM% -+9*VniHn?g>09hR4Ge^HyMh?`f{CT;L-xX2qlVQOZq-N#kmnl2~iBBX|h!yQK#cr9*wRJ8&$3p(Z_U# -7$U)#Z~0k2M`>j3|NG83Un}|lsm+XO%u}ec=-@538Z$)5hN1y`2dB=*ScO_Gb}xWg>o$y%qVe%%w5lB -<5^6M6rb;%t%;dNZUX~qJH~2$4I5;hH#2(4YUv8=^iZBCDRw!BUnXs?~4qc0czK7t<-J;@2p -CkqUNN1IFgmLdWoiK5LzQO(G#1GKGvgSg8K4QnZeqzX=Fuq=8iB@5cBZkpDsK)${(?FCS;*bI>bK3<4 -dD7bdg`;O^2~g1c`6pp7NDQ!+)VP@~lzG$$Cy5e*>zj3gJ~XyaO*#%=x& -?^Xn5MfbmI8E*q8tJ4ZWR-G?KIJA72#*Pmn$i0!2yn8WK*iybM{C$Lj~2Ad)to7((W6tMg>kq!q&$kE -Yx)#9Z(Y)wtQcnB-{10fHF!5E0DBt+A#*9+tH9)RXmYU9-#n>2#W>8COS=g%~xOMyr>axD)HQxfOv|o3lC8}gR9j(;LKJr#I+ -pB=h!PNiy*eN+X7!pi*sg8&s+kf(RU0k49A7tDcAmE65umev12Mg!otwDq^fUS=Bu_st -;Ok_Do?iAqGyVk^BWr9YZosTdupsovo9qytRSfRSSC6`{lWRD2@^`ioGb*7|tm>6n3^D8k+{e26Sv=7=_NO()2*!Wo0g^Lh=JQtTlH$cI-k)C -9K}_?dpAm#Fl2PbBpAS?uw;$ea5UcPcCa1tU0b8{QBoyrxnVVTu%z)$oWN$6JG<&)#&z^(L -p*C;wa{}#Vp+Rt)~Za9GM3L21yq}{6TdtSEkRnt~pWM%>?Z}F1cu)acO)UMd_TYc<#^nVSHSqOVSq^) -or4UJt0Yq@eEY`+fnA4xE>DD=-QgjL^q#X^|HP5!JOC{Z}FWVGOKSojKs5({6?-+I|ST6ABXpT39XC# -#!Sa^yKxZLYTr4&e?)jg%@?uu_G2%rKLg<^}X5`gP(Kt4S!39XX}>DgQc&&n>+gU)iNI@s(D}IG)Dar -Oq8gOn5@t_wi30`dvT?A3P8FUAuOojcd6X^{-`jfozn+-V!U!&LKQaI3{%P$O@Rgk#{O06O8+$x#;aVFWu?JH7+ngKMWs(9!npCM+)(fs=UvrAvXa2kYh8%^D`&d;hRYou-KKofulF2_LK-D{}-Os#&Lr-XVu0>KTqXHtAjKbCa(#% ->1~{R{Iqvn?V124Ludg)@b8TSq|Uc828(VE?;Fce!@uJy`$BI{=zify)HKSM=Or!j&?G{Q9(tk}M}F$ -?}lJajICT$+1l^dO?n*8ff;df?!L8Ul#0eRiP=2jO`DJjETj?*MNO)xFRu4$p -$ukmXJ}Ski*>oP=h1*q%faLrV4gQ7&vzkI-`AwqrdJbJ#V?nut!`V$Bc;h-%Bw6<-2cQ -YIQeuO+)makO@P^;Y0*ppuZDCE|8XX_GgN`@GYi!;n5NhrsjWuPc`Q4obNeH%3i8u3jhqwIG+f*Tjk4 -OZBUN=;S`8IY02J49hQFC!OZr07P(Gj1G?Xs<7hp2C-KH`)!l9X5ON?P?&++Dk=|XCSa(7S1~dZNa3ThC`1mEXaN}LKL -4!JkbJ!N>L4FmjBMViyV_`a9892k2a)(1M0G>r&yiIFl){azYp4K=uZM3CI7Ljxi6C^;=DoaJogLd0V -KCR8jAjL3)HI_}UL1XLIAS?(+)iCCIS=$+pw`7{m+p|yUzm{@%&SX5E)H-eM6x<)Nz$CH@Szan;fFEG -y#{UM{-vcHG3ipl-ZPj)y-r2F;v3+%Vro8VvNfqy0R8t)A -U9;`Jbcj!OvyRl9r8z>WP`L%Qj&DK)V;RzQm@_j79p)SS0j$oY2FXS)8KJjJMA&4oJ(;`%scG -`IK2vQHeZyn?@43X^44Ai}2)cJ9Ut-IVnnY`2=5kNAXPn-EuXry{;S?Sv=3^Jth}{hgab%k8&0`)qyo -K_Tt%oq$=f2F?o>xh-oPD5v3WYi6HBuVxuMEfr%a)Y(y--GuwC`kdgk4WNei^0TYlTn>Wn ->f)?OU**rqf2S$rTvdevFH@vQGQzelkuN(h0dI@8X{qwjYSK!hyy?v`D%mC5c5~qd#C$WXUhApAS7CI -22RyDwA{bw;AN^Z%`K~3Zt(8^J}#mkwh4O6&lx3yLcI?uXD-(v0JtiT7U_;b2Vphr2a!uUa_m2spo_(LaWd2>^>+9W%zVz;R< -@-k!&;m@)*@nu!~oGLz_EIEK)WMZ~T<{n766m;%K!zA_r^-t+nra;|5(j!_1(DrIq?l=^aw5nfr=J>6 -6`Nb{pcOCpqgFmZC9K!P@tNN+@>{X^moxSM4rR2f~^_aH62VHhDNwuIcS^orxnOR5akIZ@LAvJq=yN# -a%QmO!d39=NIy!e%+_yfYi^--thcfArm%yfkoK4<~Al`<&+IIoleia^$*{I%o<`x7m^eSe~bpYOvK(l -H-(YDc5e;DatzIjvD=AFiebqWnZTs~beu;E7q)c`Y4V()6qB_Bi-CxbXLjSW98GdfR${-pYnJ^mCzo#n!LG>D!S5ivUeXUrubt@P^7Yd=v4E*fo_5Emh((I56@_s$zE%N+t+teOOcvao94^NV -0(wc$5jn$j6BHf9-e4+e*#eqM6bnYekTS?uzl`$avjvRam+l@zom1i|afNC}A2vDuE2tA*q-3;B5DtC -(AGe&PUM{m-a9l+cjhSsG<^ViZLIaqEiXv%MDt-jQ>{!&$IwJK~M2sd#pdyIHl$F+8MLMfFJKemU -tz=OIsOq6@%0lKMU+>|ZG&6^6vcnTL-gS$P4og+52iffq;fhsOgiCX~99(4q_wPWM)EQ8VO1ybyuN!L -s>kMhU8G~#D7b?<0zt)`yMdIj-!90 -8Pj5*xVbm2YFQu&=&|I~skM*OF)fBgXxcz+) -6~Nbc*umahJ<{~i_9FJ%Akb;m8je{*AEh;YyglRfrp8N5D=S=0rTG-hg|NwtS}t= -Fn}3-k*+wPFmgKNa$P&K^MG3IZY?Ltccc5}nRDncEI%6__P}OfO2PDIM^51x`d~6r4AkUZI8q*3YK_q -iFsFAT%7gd!WTql3~Js2eZ9A6bq=*Rc!E3tSVbe{f$b;1O)Zf`=$vwP_RTMfv5uu2+P{U_~88mICw;B -sbfAJMLlaPY57jl*LPRv-_`ZRD=F{;L_?V=upvL2gDt3x^pcxR+jo1Lc=A_V9B`8oM>h;vKcLBlPx+w -)plNlS>Qq_q@hlR!FW^#~$aPzaXYvDuEUei;I4_+yMjr@Mc!M-6*hlxbvt{`2-@l0~v+k%Pxe_Zckf< -Lde%D)1SB|OF=oSP29O~<$Z5A)2ryJhHdR+{}c4SH>>QLvqE+itROi!u8y%^yY3Ayu`4gIu5$TDd{4? -(-d%^kD{I#7ngFOdcy}G%TBq{g6}J}5RrypqUodym^}%_YWXm6=r3Dtewr=YmL7ZKs-CI}wN0uyM^pC -YTO&q{#>$gYf7uVTB>tq^ke$b3{5UlcYi@d^8vYZuCXwxF@<*!&_9#=B;4;jn4gO$q6(tVay%U!E$YH -QXUH8W~wgcx6+*ycg+chOc{m1{|5Bw-?vKVcf}$tQ?k4nA+?kn -^=3Cdl-La9edP=u{#$q%wUxMlvGYQW)wd*i5J+0YtKKg7Chy899IDU -go$Fz++YE1&eX0J1l9$iUkieFL-LdG?t$QwJUpI-zhm$>>jW6yX*((mt(Ary6-GeYT4@y9Ok$KGKb}M -$Trvtvp$z!zuu&sI{VECdqy{6lAq+ZhLTc++9F_2z2Y++nuK@n0!e0*jS>Vr%Z76@dM=F1!2i^qU1m3 -;y?uB$Kf4^_b|MNga4PkHvy=sX#dBD9TmNpR&M1@aVb(!TtHAR0xBeiqM)e}%0(drgn -L~|1-+2s6_@vI(bUqk-O3iVO$9ZVveaxb+qCrc5S5gsR_Oen&vVYX91zU<_I|(L-~am_c<#)Z^?9CoX -6BihGc%r;^${Lb9v5|PNDw`Rz(%kTE;Kw&*}rQUFf`g#>NOX_u@*;&9eUVOThHp!*`d{183fdnLDNfT -pRA^Qzh8=cFMLLvUcHp*)yw2pwM+t0CRIzgOb~dPpq~JOVFZf?dvrX!aEOQFN9Nuc=H4rYq=B0jrlzc -Zq>x++3k@Nc8NyMZnlk&*!*K>Eo91H{p?2D9$#{lf6T#~QWd!>O4iS92l*^_a@wy$Et;lReW=k?#lIc -&TKbd}H`jP2NrZ1VEWO~*v8GDF!6_8_bcWx?;Y%faYYtL$8t7TT{@3Z8Qhl$>NIbP_eT>sfY0Izs=!UypW^!%-U-IXT_wm*rkLTe9*M|L@$Sd(;jZ^ -81@9L9ef`k3d~Le_l%oo#2)Af*TZbQKyrO=-LaOPI(547$i%7uXRIoZP;;PMJX}i7nb6Zzl3~F5(|HX -(*ztZ@s5t(iY@(9jVIUjS;%}&E3Qb^6BGI$+u35!u|xAMtdwhrh5{VQ;w(cVkT0#YRa}^+i#ha#c&`Q~Evz+O2^Ru3H0de23_(b8FzF(z>?>;%YmOk_qQRC -!DiwOy==3v^FSk0qSvc$GOr~@p`EmfHlcac+pfno{lFsib;pN)fr=k<3^#LrN4B&ig-I?MBW|boO3bk -_ql@%$oB>EVdXP!P$ujq%&WtGw@umW1I!(JeJk4}C{HcE;>tr_#G`-_(teS$s50b&`j(g&I>BuBOiZl -wmUc7m7`~XC2+FRJq5C=I!BU>u9b0NW>ALZ-q`3BthrU?NZ@CD2sJoWNR3RlT-5OOlK{> -?-mtFQ;Nx3TQo?z*zqH&~&E{=s7Za7qi*;tzTn{Y=$VWvl*^zS^x|!a=Jx%lAWTJHEa%59%6HtQp#q6Qq1NkWe%HT6g!*am8oprqgdHYQzo! -EMH$UzmJ-iqt`fthO&P%EOr;N-vz2Zd3R|FIEA8RB1fxW8U$++ -TYJNL2U|m6wXiiD);?^tzKW!0uDLv_y7-29Su~f0s!LFq*A&fVI=f8o9o8qSnRM9#r}VP4T%yn$*;{$XB2HJ95}mzH5(!!(yRs!OXduLRA-S9NI@<~2%lIqzT>bC}l{&E=%(5)kG!UU -T_SbqNXc%GF%TRG08DFPrA_qUvG^^O~u-tX5s3!@Oo|E{jx`ptDGcOJ+WaDK_S)lSTJIT<|D&_qvD@srZ+x^bX<-J9%kr46ah+Vl -8dtD-6`JQl_sVZbxvE;^yU@?uQpA^_Q5~^2-{UTn#*fvgcze|ET#V;$jtcO^;rH5g(BV$^{%-c}gjDD -Lsc!a8jZfNcDp&mbN4ncvlVkJ#sUCKJ8dW%UdNw|3|IkSq*{)@6%MKrRl)3G?u#;zGS}k%XS}B>+L?A -`_;=)cWeAqUmJ2CM?9v_hF9;E*crN51I!ODe(`JZdxpHnj9{B*|sR61CyoI4-MTfS!b(s)V6dd_U(Y` -)N4y5YDoKht(>iST}#KT_rI5xsXw??2r8HBjIC6+}JxB5eI~zNb7$6-~zYItEI6Gwk~^Ip5n9!TY{=+ -tzB0eU~vGMzU?cu2_KGOvEQAf+(hcx*f(*`1(pumVz1;4&V7y(?VVNSm%YkQczgJ5<`YB -Qo$sEH3@EU#Lb%ol*HWJ`<#RpAY-WR5Y1vi&~Uy=1X5T^QAAF-{#-7^txJ2Dp7LILY+v{#CD@7vewlZJiwlX+lTiF@R())}JD>p4Ls~gM4KBsOh14 -D^xiwzvzXW(&_i^mnGv9WA>osDHfhiV(k_VdOv1h3f6`?X;8ta!y`cdK}cH=nT~%RrZ?2-ec=TsvJwOSdJb%Pgqy -CR)xRKFcp97s}scA%Jv{wSeE^UD9w>Z%W4@`JnyR$;9v2$%Wkg -tC)pQyg!i@ITzJ1*Z+k1pE_W?%p(}025y!L(&(f|7DQuTU>4hA@t`b#RuVN|v%1Dyx1c0rt>D6I-qx!hwLs5V -Noor+!RocDy=5dk$HK0E>ch|@QZecPN{f=>-bZxgd&U-+P^DRodHH0em>s!X!QD;GT7hns` -t5`iF{IRI*Mc%3HAgj~id)4 -jWas0OYKk7NZVcSaCvK3U*1&M!Cl)LCiS -ghukx~8hHWOchTXc>L#%`-Xz8qgExsmoG`89t6 -EFEYZz29(96YcPTHdK30uXS>~Nu6-7NMcHj7<$4!P(|uC8e0UMH4=n*vd~2CP+gJFektxr$fNFZK_?A -ImM3nfB&|zfHDJQk}Szl0(^!ZA38VabmpHY=Cn!yX;Xnoc;be)f$%Pu_eW8FXBbn8_pD0cZqFi90V~EjT_FkfmUodJB@}H8_t+&Y$xO75{(^GCf0EYc$VZ7gr3)GFfDbSHvv74!3d`i)ASKh{Z&1}>_Pz5O(r|&h}?xq*QUhd%49cL^wY9XpS&iY -XJMI69$#GYc@j25)8_+yTRt_eC-Mf1yC(7f5~#>-sw&X=2QtyG4bFSlTm*%01jHl%uGWeegX9JLK*u0 -Zu|F4M`=s#C4aWz0QYIP}eBEOgrDGNV-Jo6CT7sr{_l#Yk?Pj62IfxVp0}#9!N4rV55@XBk+b?ktNr% -iGHIDpgXfDiU1PC-E1=9~6cL*~Un&{Z9Vf;}&G8w8Zj;F&{^nd+~nz&w(s~hc8xk<)&|Kou;qSC$^A6 -ds%qzfhw(>m17&~A@Ljwb!v438~adEU>{H;wYa)fw!!%p+FM|yA2!4^SKrKPA@a97+-nDN!hxFBv4T_ -dvv+b{th~wCyKFMwskGv~V0e?fYwByc8L^z(p!qr1aA3}a+F0(9+WexoE7)k(UA1~r;@gl_KE}J{exmjh!GStDX-m(;ygiP)rh)+Yu}u$!!^e?G(^1D3>IaL9 -ed5#ZLE{K_eCBHSqCV>C2U+-g3m8HRFTH~XYB!7&0z5{6_=i?SH)JFZrAkwb1U^hWyau0vPBDC=kPA>Ee+}F13!l91)oVz=TD$-~7quGMP-FH3yIYj8dj9 -VT%^%qauSU+6bJ5#KPbnB?X@>oep{+!aGWo-y=-r%EMKZ_l9*nD!!GBMigg10-%Q^yFKshLh8AwPE1dGv5- -QZg{E8V(HW>FGG(X4{jXh-P&*Q?#pf_T;DP_M(!QFG@xquWYS+zr>Zpxh3j{ZH#j5Z -5CfRM;}*`2pPU2KD?RhYlzoRwxO8V=4)IPQ;!4>rW(@w6gsC6{tcqv&^i7*zyG2)c0btLD+Nn%1gBzM -;u@;?E>Vx|b9(FBS1u=(wo|u{c&N7(FB%pS;J*LRuO({&D9weX5%gR5tGmss@K-l`7iYZxg~Rwn{3hi -Vl_bTxCi!PeNqM@7(mo#@;%T;-GTrg5MUt|edr1EzWuT9CvFS?a3LfX9eLM?IMT>E?^~gs^k}X-m`$L -CP$7rTpO%IkQI6Y)f*^@YlxpG_$zfDSV*gvDYz^+UDyQCG=i6`rN|!UmW*@2pmIpE}`-c) -AsTNhX8D`#sk<;a} -~<@ndbj`3FA>5gV=0Q3pLEbQKsI`W+l<4kcu-TO~LyinjsL)b@W=e4KhR8zM3@VxK&9DjVIZ<1WA4`_ -LTT|(`VEWs&+ojbMO0+T8WdXLZ;kCuR6O>Grk24A{Vjv%U6w6`$otq?rA7&?0>8VeWymh|7h@E% -<8W?_de|P*;xM@F3giYM49C=EVaVOdg}S?``2E^Q55;)nN$`VkS22gkBHO!xe3WTx3y-fnK2Y&@5dLl -4HUt06rsL8}S~=O-V~^WrB%%JMm-u_^nYRtr%b@J_KTBDL+m)21y49g<2q?RQL#lQCcAX?UYfE_52s4 -oNt43Cs^C1@Yt=GA+)O&mY(weD6HAv1g)S%^+1)on!nlid6jzFoZqzYMDiev-JdGMFuEKf+%&S2!4c@ -*+Vplg7%iYhNg95Jr?5V!paD(dIm2<$K6w>1&z6dbQ#96 -Fr*DHGzKWA7a)E+#6Pn@Jz{GgA>d?00=ZI*Q*R2nTNyEhFG%p8vJl<;r3HqalCP?9l@$xFdSuvQE!Z? -yFkbPxmYtFvy&F0*Ec>G2$gmr0Qf|MX!6U;SSfKBmytn4sYxP2LzR5=!jGGsn@xCRIIMuzWBi7G%ZzP -}VTiKjmJWC^6k{3B)DCUgxb)LCv{N$#Hop*O9gIt1G`cCFwn~MALFnhv_gz;PY{B8FnW!Q_n4dk+|Ly}@4yYYbIAbPJk;JDJ=xoeRnAfO=J-Bg6dxg^E(q -PB;G67n!4bm4{C37vkS{)F62MUQhrdtRtX=*<^u5_+*tLXTaM(0Pkzp=|Lr!O6_F>q95_+x$ar#>FzeVY_ -VVhb+Nm*u9~h9q?2JpB)f7rltL6HITPyOEU^(ym$r|?Y#%OOl$oh09be3->+}_Op^|*$(9gs*B -7^!I#^2#^PGvY$kCM)GtDT#vxIY@bH`Nb@4V(KvR<-X5hEECbnU%773J$D_@pZ;mIZLqUY})D>*$MMk -^?gF1o(hUN&9P%^-zIn9KoUL&Zc=9N>Kw+mRQm;gc;P4KQG5YRW|ec-g*0;C=iCF+<8b8=vX3Vh92-h -u4p$uh{P1^&k2cwFKX>7*vx-8sq!7^o{uln`aX7D87=@eRn`a86u+tk247juPcVMQhFz)vg>4{yg+$yub$1=!TVHX -Mr`@Au!bv&USMNiD@D3Er>;n -}m3vmh2N!~W_MP?JlQKa2Z&8v49$uv=V7ON+^l*~EmkQP&E7&NWk+mI~AAJ{2VVn6QmWEU?!Ikw8=co --9pgFW5s;~aB+g*}8YRFHV^=TXE7c58bjE4*lgTzKbLLYYzo;G?RfUp5>RliJ8Z}JgQ3UCmpznW7d$0+p!M@XVl7fTvsO9QcL|f2DR4e -d9}{w^L8>#JM(YcWAPLYU%AKP)fijq9c31ZRQYB3X|IwwCsj!}=s3~7{DfX!dmuQ;3g+OzRZpX;pl^U -2SAY$WO_ZPJ1N(H-nX(ayxJki85el3;9p4m`D?YY<+BdlArUEyDNPqm8@e_m}3w{#ta}R!UOG_{rJ=A -`v_(NMi=gxA)(`KslD2?U#rLq1)?dKiem~8EwJA*$j|IXd!Thh8T)-%q2(Q(|v=Do0WIkC&LfU*;O7<@XaB9d5^*Rc&pYfF@2VILRyLN`hf{4`kB|9-%b>&o2SEiQt++Xt(AM` -pkF#!;;Tf>x!NfhKuXo+9DV1D~%RzHD9~%Q7V-I$$Y--(%1D3cK%Lp>ceK@Aefu+rE6Jo*?BRz%z3a_+J=St*cv&?JuQw8e_33oG$@tq`Y$v6jhw9aD -CwA1m$0iad9~^^y~+b{=v~1`^Ia&M*rR)oe@rdIv2Kw_;C7u*Ywb$`_aU@0Pu|4itoWRnxD%vC@}{W<9&?vCmwGBN{^OezxGSJ+>ao2I{xh^C30c -U&jK#tYoD-TWrIX6c--oGH@tzsQn_0}V{e=3sl194Th>=`(zatIL7~ikdkgYf!Cq>vwjCuwPCi_ -RkbgB0+oY-wQ}bD+48F`ngk3f*zJxYB*i?iwMoN3XCHpF?QuuHbeBR0%_DJ=4tLx)B3WKPMY|r+5r -0l9WA4*+uVlqH1=ms8FWQx|nkn$3V+)&mkqv2>noR``LN;C?&$=7>=s$nXDc2b9cFpM -o}NVU@8L1M#dj8Co%;u}|!YHh|y(=QRfJJbvJt=cR}%FIIb0Jf -VPmyb_>FG-n#cjf7sHFvl|-BKQ&i7Kk^C>%T!owIl4H_F^v?%h>)=ibV9H3bc)VuZanz2=5k|9q1YYe ->B^()3cie^RpZCsM#|O+dve9;F@y?uxHe#gST$mSJDz`v;y)$`l@7Cn~4ba{1wKfpISFyH#QmuG4bEr -^vn~d@q8s^)&7@RCsr5$>{GfS{jQhfjFnrI7_tSySNtUS80pHwy`<>fmLmPGcCk@k_EnDXH9&c+*=qV -CuUM3I-YlQ@XfU}VkHs3QJFQvb(2f>49rUKj`k_g_L0l=Zt5{z#QE8c3p~Tl=6O1I(O7U#0#`6!#i1} -QK&|ge(BXy5T(%!TxaEFZ=!rZUEj;?_&`#7Sh+3i_kw -A&Dck&e6Ke)8YyTysejk#lC>r$)hdlhE_F?x>(&~-gLsyo>RCU~0GWr}oFuPSLbtSJnG`*(msnG3J9q -s2Fmnk)zcAm&P+!Z9+r&d;<@r@H)wba80wXeo_WuUaB+UYjdRND_0MFzN+C7ZU)@-%HPQy04rU!3RC|Pw8p3K -mcnAF_;w#Y&tbwYFr$+w6QpWdgs9Q{`PztCc2{rH&Wz49Tr6B<`CG_DkK%1I}UD<_*YuAJ$lapla-C6CF_!g8R6&43 -nGkXy>1UzhSVgK_rt#rtis&co&3;R1;pjrkS$luq|AE%1-CpK+Wp*#cb#-k#?tAYr3Id`_O -tj@&coISd-VnniM3xCb9c&wU2kGFKk!iCyCa-C^#C_4ypaQxE47jsYCk)+0`rA+rW2B>nuVRUHxK)Ld -Q)|xz`djuH3s*qFCL`ta?@Why@k@&rhg>5BcDGKrG}vu+S-t@`fP)#7+;a6J7ZeDf!hqy!^)@k4LfTy -QSwca6O)wJS91kZ4(03pkn#@F(xw6ohYM1y&SAgLFe^{y<0Dh#8cQ~WE7qnA_}=b?-rD?zdK?5#IfP5 -~yd8w$dtBrFMkFRDWxAEdw;YjJGQp206#RL2wykC81V2+T@9T;84SUbFt;DVJ4Ye*gF5YJ<##Qgs_!Q -+!zC`_jd&vYTKdfR>D#qzdt#cLKKY2o;rGBEIW56cr$S&Kx6f3^f#0%=NI?uF!7jAK?#(Gb@?+B{)Ls -@mTeS>) -FzjwW?Yk9u4~b5SDwnOxl-=Y%vx8d~vy46BpYmeVn^G!BTU -(9QB#h8S+$iWKuFpb>|BtPPuNG?n#P!lIxTVTYK_LN>=`1)s=Kl>U#auP5PExzS#%8W3Bll-XERljQ4 -M{-xiN{caGoI$XD;$V#asLog1HA84FmCSR$@R_rG95ixXNA#foDO47gDYt)z-I6$yU_GR7mo9@gKg^l>Xonrv;K*CshXwdI*@xZX_ -C~mh=E8}c{vz?h32oXdEuf``YCGX^WE^?PP8%tW|2)_7JVT>{Ed=`rJ|U7YQ0)6rvqLFM?=-F$6Y(e-J! -Hu$|x&g3AQWeifn(K`(+q1j7l&6J!$D2o@8pCU}M5U4l;tP7+iR{6^sYn-HxCx)Ssz7)&sVApJLd{Y$ -hFY$Di0aGaouz?<+k6Lcl$OE8FFBtaU1jbItU1_Dv7^7BEWX)eLq3s>@bY_LUqIm9BCC0fJ>Z(Bsb{T -7#B;ky>mxVPoXNc7C;a%ntFEW^YTyncq@Yaya?@?vIY*b?$`CRy|Ig_tVx#Uzm<@`RQCx{EZCMSp}pj -Yek@Ork#uCPs@53P%GU`m+*Fxn!CNa_HAioC1#VoZ~REfwO7smd@!;<*-wTntZaQ(r-SeGLd -*bi(JwujXd(r7L(~Mn?iu{>6}7MTJXnyOp^IDVXHK$;i*tdO4NNI6`_Rap~kSE_tL?>!oyc!q41K@XMeeEUs{l)T|!4zJ-p4FFM-@lL_q%`d>=GVuSUoMvv(w$D>^C* -^`)2OfJ=ykCsMLoUVe*ym#PSq-AGUh!riBjKCa(XFViGNQKde!pIq1>z*#%g`e(frX0PG@PXKi%4L1Z -={}p+uol_d~J5@_vPq(HXf~Pqw4_&z(ox*(%Y -P!|MTU8patdBv4^0{{W5JHtfl-X2%x7ETg$WT7lo^C4rPNoogiuqT_Z;+wAPUkXEYmeIFA -l*=x8{H|l6#!0Ki3tfj=cFe-JD~FJYG3)WqEF5A5nV8J5kl7sWM=b)^G^`-A1>DKRX^@9LwTRYa7Lth -@-TldIOXk{7Ec}sC0+~0%oy_)R4*l4|8qV>LEaDb2t3I@l{E-e#<}5Ndk$E4P50VKraxs~hxX2t5#O4Ei0Yl} -+s&Vq7)i7EU{%R%-p3=Arc0pWg7E&)O&H&f4GYtT(8CW|zw>?N6|K4|eaNxe#wz1ICKIYDRM{R`o8_P -&~s4i|KXT?$mS#-6fICT&;iB-6C0P?c9#ziQ_h5mdlQIL(MyvHM=av5&WGbFuEN~{u5bGso{AX_ue%w -D!w|7I$kKC?lJPXJyCHO%-GhvLwi7oe1lsu*OYA5AavJJ6w3G>&+R} ->oJ7XW_@2tNsQKyn=xOWe>FMb4vAeGS)T#aRSvb<@x!oL3j~SL(^oaQQ9%*SkW^pD7RP2!i(bh@UsS~ -YSxWjX%yX<#PvgN>%=^JYMvS?X#II>HlYjH;TC}|Wh8tV8Zqv5iO*gkU-_oIDK&Q@Ky54$Q;O*VI_Xz6QE4X)`zWqW% -`-g=O7#LxR926BDGkD0**tk1}#SgzTVZ_M9QKOTR$Beye-1rH1r`(e|aZ;K!ee#ry%zLw@X6NKi%geX -fr_Y!<>%RME&zV~w{;xmgEB?B={;u@9@^2{hFjxC4s^_nEh6xYhh!>r+bt+-m;Zuko)&0{*Lie?&k2TGF5K&FFW{Bx`ine!o_FYv``)!nbUQ!g=!@Ma2&+SX -i>ixwv%6(q+s4@y`bzdiaqQD<568dd*{xuU+@V`VCJ$_4G5(KKJ~_7hZg6)61{C`r76#TVH?U&9~m(w -tYug`OaOt-`TTw-~M;sJMjJo2M-IMu -_4S#Vn>!;nw^sUC5g1N@H3NJIY9~ep_v)UOMM`uHUq)q2wdUAuk@dJGV{D+#a%9ySKkD<8!^lUSz -4$p)v|QnSo4d3iZ`=8SCfuvB~Yq$yU!o5=JIPR+=&rkQOy=A2w>w%Iz~NJotOm$e{?o1@Jpw{GpA -;oQ*$X$8ij|(Pleju5GK!>nr2SV%b7}{IlAb03&xv~MD0+*bqtw8glT+^%b7^{xWvai&2G(`m61K!oX -!Yc;h%4{*$mzy-4yX|qf=c2mdvlXzs8FVvL|eP*SL= -tME|$;9YgwHtXn*=i#eXV}b>a?-5koOE-pzV_6e=hW2kaSu*2kIadjau01L2~W(-T&j3#YHF!aT2rT{ -l8s6R93r-K=}zU?1-ASt)`_X)n3a)j?Qiasm0xcfGp1zNtogaAldR^{belEL+@*&-Kd;BcjO-rP?CD+ -fh;`(lMplp$1wz)E@m6tVdYDRV{7a|o8#p*WAo@29`sf_C+RUgvf#LL>Uox*d)d19cz;@& -7j@@l#>GHg>I2KlzsJcIMKWoIZerd1155ASrk)cR#it5(=FD;K%Z-)zqZTBAn|?iO-ox?MVUF{f(SLs -Dv`Tn`>yZs|g_Wo6~eK%$6U7o98PG|Y#3h19^U9YFo4vwj@#RXWU%G*f@SwVkD -we_0)WQ+vx*~-;t9@=nzm9Z2r_UiN&-&x!LKpp?&#H+{uJn8EE_MCKe{} -(1-J^!V4)gR9|yQRT&p1SYq`80N1J)h4OUESY*0qH+j2;&LM9#Oy|aJ7BTdkE -2zlD_bZ2}Uj9rL&K;SN)|VHCR(A?CZyV?B#UYEW*ps+ -HmZ(z#6&lIt}`rSVjgCI?dPT8K9M_r|!+pnI^f --&&jez+48btsB5)RDT>Ja{EW%jS_-3bY7h{2yA5Wvw9x_O`8Dzcc~PWdxV~m;=}xZkIcfGR>tNDlhNn -(N*e5+kK+~?5w#gpx_AFb*pjkHSsGQ`CG;7q9)I9N!Mx!enjZD&zcA_n9DDe-vzPr0N_TzyDX#nop_% -SjqEswDY_ZXIwnx^rMQs@Q_8wgiZ$`gu+)>Lz=miR~!m6emPzugfta(K)zebnR>D6f&ELQH2O6G`z%7 -BOy#wmkZeC3^dF2rBA{T+AYI=&q>g4|}RxH5AoQ!Nz?ZiR%q1Mz~{zOZ>$9DB|tPGU{ -c~qjKX5}L019fC7QXJv%wIxZ2Noq_|>+HGkNTvb5uD#Zj-Vx8y*;5#)w3wNbthtCw6appjBzV`bw?}M -#v~{9=@?>jX0x=%~^`nRZHBosPHezj7B9$WJ66!I`nmXNjh5NVevDxI5nw4>1UC+quw8Y$u?5G?&rTZ -PFYvfid&ylvQ(b=SerisJE$t` -8gnsa$LF!54!RzGJA1qo!bd8cV~xN@Hv)Y&~`-uVliM#Ynu*C8x?gwYi*50t!n^fju%RL5O}R$2()ej%k->#n{_ItDkAf -l7P5&~JQK65)?BevYenFZEIv|e21ysS_G-ORTLR+cM#DRyPazh7ABNV(len$ros&1%iWNhvF6qf`gR| -`UQ;^LddwM!)@>nmYYMGmwXLXfEGEy{fEh#lG1H+VL@gZY2(y9t%q8cvk&UCfp;@oTI#So7AW>so5e3 -R?%-2e0Z|MtQ6Ft|wh<<>i*@m|(BQs}0-(xE;6^~ZI)(z%wu``?A=f1dvn59A43=XBZe&`hi;;M*%)3 -5M;4II$P={XRmS@AuWrMw;1J^J`-8yFrT!)1;;AuesrSuj@2``z@M#AMHI-dymrI6SO!ZHTOi#OxDsL -tL2@gxla{>SI_Z{555u0({T0H!WU{d&(qBLn!iKyFVfOkPJpF^$F%o#8a^Ae{GQg_w`%x(p!t8Q`M2^ ->d7<-6=ef}of6~1n|34ePA^-o$M{194nMGU+{Cgk&1|QYm+~D(XM(n@oX?Rgu^>295^nVubzw`OOE(6 -;0|F|e5c#(eP?&-UMI=Fl0&zgUW=Kj>zs=4k-)tvYEah?&f%q?W|846#%{@8kR1L@AYteO_h+C67AeOvkM+7T{E?$y9sgB`o^#chm~TWMy76m^c!ppD! -CHdV1S<$0Bv?#PL@=8mmmrN`JV64%FoK~3(F7KPaDotm-ULAew-ao}ZGzhg+7b8?ocogUAUH&@kD!d;b%IR -<&k(F7SV6FiU?D*v!Ayc&f_n&t5m*R%YvH>Rv?K5%xcG%dCo6LSa^!K4l-Napn{BX>nnZUwW>R<5H|8(!(T?`pAL?kCCi;Rp6VYl -1Wf+{Se|3a~1#R~EC(@%>x-grZlm6eH4KmAl(zI<6|Rkx4>(+h6Eq=D$t1BHIVw0FjggZz3>o~bVMtD -Z4q_G|<=BM(#`lm{-5_g;GUrI#}lm)>X0z`Jtrz<~q&jwVpxgs3yJI-UJz%&zkNo+8UV+WU-yz84WcJ --u3eFF^PM2h-CJ?$zF_Reu~`UX6E3znc8D?L7P$|M}I64pbjFk2LUpdF1$53Q(Oz>C?N{+xuJIy86IDA|W08@jd#&ycTEFF)8i}^}STwtNE} -42Pwp2u*;`;|Tbl7-+!ve_t`^a#4k)rb)0O9|^`cVfiRv#=ZWh-Sng2*aQN0nK&*?a-dUN`Wo9Zom7iy2{Evnm>6&&{{j35zcZvYPhw?d&dG!1~87S0$iq5&u%kc -GmHz=Obtz(mkqLeBj)S^pzB@7=q%h>wpKiHV8gzWeU0CF9GMEfdc@_ndg`wbxk2Kl$Vnaq845mz?Kfx -@8l!p_{h)pd{|kyKo%HPxH>_0Vn^ynSw -!+JkYX3D*B)9#dVbBdf>(q+fE(J7)puHUhFG}3!x3DPf+@`no-HVKyE`-Pa>zPh!G=1Qc{u_J9ey?Fkyn2n>JWXo;+D(W@d -`1Q>Ti&ygY6{=FFKR*4ZP(^Y;x9ug(n$1%@x4*v!JJ63D$gR`n%7^vP#7y7oi|ZzU6CvHEWJYSZ_~7ON!rt;s;Uu0Th22#UD%YCsX{Hvbk79@mE -v)7b$)j#s84vpEShpOz|xg|1OGeqxefG{<9Q+C&fQZ@sCpcuPFYv6u*MvpP~5Y4e@()rtvJP>bW#rTx -Jtu(-Im(K1+StE~_$vLVmYg$f}oxJa35KlHzxu_(2qZAjOZT_>@*v2F0I6@fT72H -5C6PieFCgKcx7l4Dm_%NT8~mOUlNwMiig);DgluRFM)R&DRMzkm8T0`1eu#M=1UlivN)zzGx;A#En#E -ZxPExAXVGG;vh+Wm53Fx`CUTx%oTFP142$)E#&f7gnW6QkcYoE#5Ys?z7&5L#h*y=XHxtn6n_K7f0g2 -Ir}%p*KIOUm3yOb=;#axi-%csqK`CTV3JWNOCn<&9l)?#0p{ltQKlha4+z2VoPm|)paw&d&S&H8eOIQ -516u%qAkD&M?DgGpiKb_()r1&c-{!?Pm1~&ouZ-!MGlJO^(8UvwyvE5I&|nf#N`_~C@wZOIyx>YA|f(6rCZl7T{;B}8 -PZWtAv%`4=|7e%(a|Y^w<5rhA!f}#Ec$&y@bm -L)Hw-zzKb{{-zfG+GDPRNqqhjJ?K>l5BDoxy|)j0Qle9wc9YErXR&w=v -+$)M*xj~oPN)+cw%vUOk6w@!8z#sC;U^Qds=`(9Hr4_@Zc6LT2KI&e@b8P6ibg7%3wJ8-#mCQqtMRfA -7%0~g)|F_iHnXWE)Q!pyo!x>%`C*G -g!b@nVMhWl-duVaF@J9O4>`m_df}_G>YkR -BmL+MA8AjZb@>0ig&kUp^{HnM*mZxL7fiLc`=Wm&;RQ(?q$QY6a$9(}H$9;jEBD-;oR^tr3+DY`y4a^ -y(XN9J0GxO9)FXATq#Gh)PwyeRS7{1~z6(Yd1EKGG?uog4b86iMHxI>SsdAAa~@v1-*Sv1ZL0v2NWuv -0=jo))iiT^;Omf-+c2;))hW3ds?g_ePa{p3cGjj79V`@0qX`Qzxi5x^UXKn=8M>gbkINB5LyE?*Np<#sVb?i17G5wTo;Azqf -JsQ<1q#HS81fkyPX)Y&e(o#OYU_!f#ENAbr|{Am<_0mWZK@n50%`zd}+KldL!umTRL{>LSdWxx9<_yBT -%&Ka9h_NH#P3joB}j!+Vtjjw{+^-qewm}dSC&P9FFv8TzJ2@lU@8FHAUii|)QDOZKH-))#YX{AOTnLE=p6sM9uF;DziSK+zM^}C_?s -Vp{PAS!ciuX4=FGo-{PD-r)c1Vz!w)~4BJ=BW=gys>ap}n~zWCzhy?ggM-Q3&)!^6XSXf(Cb(!nPw;K -QHcAIcgX^L3p&ckW0gc-8D0xmCP{P3~z^74W~g9Zf)wVdz?&W%F-LXP+^iR6dM5?8lEEm!E(BIlqG@9Z2V4lVtAPPe1*1k>u`oN> -lmmx8LN?KmUC8n98vO$wU!o*tv5j;}7{bcI+6J!HE+m_#HT+PEZ|Y^T?4S45%9@511%7)E&fm -@4fdJf0QBBe~EIqeEG68KJ@-OxmUxV>Ts_=ga7&S=OuMREdL=PA)O(|0O0@r`|r!Q-g-+?nM=}{xekN ->D3AU7_cH+Zef#!tT{(L6DBD2;+5pHK>IupSxNZ4SO6xHxBR-I__kJmRyo1XzrR-iN<;ZuXeD0K#znn -jRgz8GWKZF1F?c0|J1O)UU9exS5mnaXW0r-OF;4A1sSpav~QTAVb^%Vo`;5qmWd?5#*1$cx1DB~x-mo -oYjDJ>sL8Fo<0eg~us-bXa-mhv{Dq4PE=JG?37k}ssJuC8AFXYi-C-UYy42fZwG_$&1U^#Jmc{FRhLj -!GHzG4UTXP#N@nPs(0IL-waqp0AcN{jiil+g(c&*M$Ft3l|{Ei-UuMJCH0Cfd(pvD`){OkR#w&A06O3 -c#blH+(7=(?$Vfz5xSG`zvFY2hCv@mIgoJePc-x)xye1M;-7Iu%7C}kI&rP|Q=cLA_8;<3{gDLJEfZb -N!B><8Q -*u4ogta(6-b^2V}h=`2zppkA4C1(9Rg2q?1%fLBp3)#u5!NM8l>ZueZPG17L;Kg&ww0cF_2J*4LkE}qQy-6h{)+NI{Y8JEmj&tr-gO#)Kec< -Z?2$D2{H$>K)Qkc038G;Q(SRqF+RyPs!x+M2)Hf;(dVK~BdVNNl)N?QKeV3HCf(GKh`3cg -MfdzOEyYfGRHq0Z@a=(Ko7D9!PNwdu^Q}?O(7%wLc)Fw9Y2eU15X;!3k5)BU!4fBbH -IYa}Vx;n<)N9u2XfIrCs%Rk9=5omeffd}NqjT@PsYoSA@1#y+bugaZ8{bU)@@D|a)^_gfuo21w0wbUl -9&Wn(bkq3+oIVfZxJ}3tcqSrQ>R}T)I>(Pn1 -6@-&q_bcQ``iTUvd7#ZaF&Xf&+SX!r-o(UN;3Wl4rg10L2fsDBVKDy7%?I)J3 -ISb;EMW)Ixu(cT&BS&Uw>34Fj1%KjUYmOj^_gg>r=4l&X$?LtTehs6+D_S}OBb1vk|Ljc@=1w)IX^#N+H5wtc=2Mc -*I$15CAT}!h3caPc%yGXTLoPR`Y6T&cu=1YK4q8HztMPO{d8A-)^&`PT76zdb>WZjfBWsXO{D9L((&) -sty|yp^z`hsw6scUN4XrHc;X3p=bd-T#Kc5dT3RX}e)wVe;DZk`J{aEvU(kTQ0R4nc2gWd<0qrT|5Pc -!?*zJs#A3hEI&&zi=Os~-~rmH#z>NDD;M`y+Z{XZOkg@=ci#K*_agZz-JFQ;*TpUs;$Z=^ab$xqIlIa -5-dka)l2jyqUKc;}sWSRQ~oa77vEa;29C=!5KGoCCcMWr6yDI?eT&%E4HlpRH4$4{UuLdf=*S5uVV{& -<9tpT*>nw$Xlz+vaqla_jt;V9XrZfZ@pELZq2v=57Z4KfCe4v(gJ`?>1m^FeSghF`F62|`e#+gK>z&w -oG7{VpOfY1?`@IifBEGImG?EzKde}>LKYPjon5|sxtu?L{x5gkb(hS_%3{9LxRPZFG@xF9PT&SQ>e7N -a2=v2{d-Tz$E0~YKI1+Bqkx+J^rJje#|Me^I=+DmoATrh6wYj;uU*CWK{g;7fa&j^Q>aImvX-TIC`+c@^X_*wkP`8U)p$Un)_x_R^FeS~@%7Z)dI&z{ZY02 -+Y*ph1J=m@#A69c2I-bN~(dxEEtF=+}^Y;0Y5l1lo+U1)U1vsV(_OKtMp>*Is*VDaH_g1%JpU0J4PnX -1D<_%sJcbcCNosQBl%jvB>V-yK_0fA93o^QJ-w-bwf8HM`#ZqS7?t>cKZAV;e_S3YhAx!{E6>S0CfO$0_ma+CfqM -mIz_EowX#sTKe2oFZbhhTK0eoqKk_x|KRRCl7%u`JDqBgq1->fg`atC^N#0ln&_{vCdK`T|5$z~=iuV -7NS6-1XzW5^dfiJ!E62}9tiBAf(Z4;=EoK3jzB)0`ZUH7=w^&8gTp#Px#r~0@A@2GDufeXqTb(Z9mb& -07{r*hj$b)EY+v^9`#T@FzXP%lv?kQ0;v+GL|X1Uu428KFGTpA)^<5r=lu>l$%);ct{xqYN8m8@Pck$ -Pntnf&~jCwOQOx8z11N1LB~aL0g0e@`EuE0Jwv$#~*(jbBt@<-QE8z?k@aw{h#<<1RC^qu|7J$Z?r+w -ck?&_GKIdjo;iKSA7!KK5LnlN+(VYYbJU>~_?Z9C^Su`SM)^k@QMZq+OAAcQv5y}=UOw{3BP`!WzUzQ -+kR`}E-q8mDcYS??^x8H51>6~bL*AJ-8eA>gqJ$qeJpjgGJ$>%{Ri(@mjghyV -D8zohudB70QD8}4*XFMNFF!*jkq)Z#`#ZbCtUMCpaFd+^cS>$kOTBb=sUqm)~)Nk>qqtkAEv`I~XagX2hU_%{Rcz;_RS%TYb -DkUEzgGiFo^A9*=q#*E`aL{zCJ+5TqJLo?kpQ~XV)Tb=X``4(zs+24VGfo4k8bkj@^&GgYslV-Np%m~ -da&`hbCZZK=_+cAs3@ZkF%9}T?3Rdhwn_Yv$TIA3SHhWtKbqQne1 -?tB&Y*K&FAQ>uw7+_+S!nfbN9 -x1Nuj_>*$NnRzsJCo&rDE@r+Yy)(f%5gY{0#X=8l{bMjbY`=k6Z2B0x$HjNiD_3=H%xyToKG)Oya;kq6G-`0Yd8ZyvxC^j|-mIwqfXB=J1v5^WA};tx`$e5bCPCVr!?`| -95h808V`Ls(}FC-dXlGT9z|3XNY=NzY9(eushbhYk!}(Eq%(aw^Y(VLljh@_PP&yT4P{J+a=#@_1BTY -r`53$>OMVx^y4(%&z>`9PNs3aTi0h{LbrZ#ZWPZYVm=r1h{pV}_KNl6B&iko04v@#BI`SByt(gXE@>pk`j`OE+QtHZ -;E0c_PAaBSyOgva`!<;JC7ceKh<&oS!Tr>RcH!1T7mjQ>>wKmiVtalpohh8yj)~xJ;f`Uxr+ADCtya{ --L2lMM#YkEOj@54G1=0hQmSYyK4hC^FlnmtK{hv#Q74lIv{YSzB%=3jR$xb7M-%D;R;1h02ttq60Pm> -bvS5%c0$U%}c0uW@Pl&q*81c?~hheZ{0P!}=Yhkg#afeUy5^TYKSyl#l~D$J`su -gz^guB|ViPGEfmYlVf@XcrIg7~5#9-5TW|b?d(S?#rh3KT}^Hz_Vs`zzeiT@WX@k+ZXSvsT1|dqe0f{ZU3QT&z?Oyd%=PQQ^AL&OP6 -xHhjtfzAKHJ&0{8)0MBc_VrysxhNdEB6CsZfu$YWNdYpv{R`J+$4JO}11Bhmcx -QQntUCFTJoD`*HS%byk;heY$0GZB`+rK`UC%!$Dd|K+L!+!-7DX4MDn))x;#EHYp{Ifp&(0>LF92jh|SZ0BaxpU{{Oq@7zZ%|N>y!YOFc^-Jlk|jKM -p^tsQbC}@2{%s@pQMXUVgET4Jha_89)&IJ6>o%I|J&yy?*JE7iyMTDW1-wAJjW!c_?%1(oskpW^3a@s(Fz!I7BPUqjxUKk`KT<8U}Kksjvc(dVPh!P>%v2@`mY8TA5T@qh>V -953``tf@e6CmL4&iM(C;*PX*M&Pkv@%FD~+wgKO&>hp>yFYp?4qfI_?8jGf8?Y0zmO%+tLxyXd-}R0aDa?qjRW&fTeoi2zZ3kcdAsPpmN`(gGid -uz&YL!Eg8cnJyeJXs{K;Q^u-C-@F9PbLWR2Lul$L9yXFS*r2 -epdLw+QWQI9|V_-ju*@x*y*qkeqy$tVBu)KgE@YIinn+<1o4$a(I$=h&UXc&=Z+K6dTewcB5K;RP;Fy -$?elp=XZ@YhI8O3+4)qXYW60}^GzLlhT>TCzg7oWE-*8e}_4o)2nT8Ih>oa0=_JE|~g*ItJeQ^A+9jF9ciAHlZE?SABoSp|>~4wN{MDfji_M`93>0mUW!7GzMFBPE7-2ag4n%_QSXjYNVd5in*0|4>fogY<7u-+Ca1^)2Xo(Dk5eLf6H3W8>-)P6K0(p@av@Z|vByyv~g_41 -74gd!5`;IDp5K(A}Z8W88qT(;Vv%&Li<_DW4&}tXj2-^+9|if^{{NCFG^9E`F$d6YKqRCb@KO5W5-tq -ke(gEv(7m`vJW##q;vqf%3cKN0^@&LltLm{;r#NAbV)*A*U$Ikt0VkPti`JEWg_MgnVWG5dQvi&CEOG -yKg>?_?z|k+qZ9LT}}TU2kka=E|dq_QzI_0qc1ngKf>$fuYZ$-w9ro?fAmKfCm?OK%hdN57~`Xz2Hr> -qyuex!XhNTj`6R@}I2(QU?%lg<#lkSPH=Ll$Z#ZwwUEo -|?oE(7D);t7G(Zw6iTJsb^L2B!Slh(YnkY+w-&07m;=2O;uM0jd`zIE!vtXbxnQ?s)32XwIKW%tjYG{ -rhKHNV@`j7fPp`8nyfZj*AR_D{{9+I@P@4(6$;*%|59d|Q$=FFzwEdq9Vt-Ge#|Y}(jt4!7mm^ZC}5Y -fLRzjYl!_t&{9|8May4JGtapr`ahNYg$5H#`KIV>tt)b%hTu`!iI^`!#!8@y4b -5x@VMaB!5f3$2>v$sOt1{@(>tzrQtzDJvwQ!e_e;I^^!~E9=;PI=OP}yQWBW|+Q{3mDeO~JGMxRgmob -GeEk7wUzecSZCwQu*nmcGOLCiZ>0@0)!q`X=_fyI(=SHT}x^wGQbLGCZUpWMRnCkPRU_LXLzq4(%Q~F -w_ycCA2!!)W1Xj;Qpif&+PwL|J%dj!ls1H4ErVQ>F^iAw}fvC|2llsfbj!z2HZEGV8HSLs|Ktc@b-W` -1HKq=dVt$Nzkw|W1`NDyVAw$0z_|lA419jzI|C04yfE;whz${2BA|+d3*40(5)>aaCTLdA4!uV7O74~ -3t2p?^ey93f>SqtF2rcP9BK*GaxdSc_@E&;Mz>bvGLjwh`RQM7v!-HN6`YcEZstURk)Trn6J=^x|*0W -Df3vq67&xd=S{{PxL->0gMGmc-CT1sq;4#`kQs%0>-hU(d$XV31QT{JYogeJzKMXLdlAeTxEv}&%FMo -g$7mQ1O^N|YvGjNt{gSTQh$60lCfq-d!Uf1pIPj3FuNjX?$par#`+>5Kjco#B}~b6?$i_WOCB=euXmy -|ar};7pu@*WyC_9DW}E8kgZ$@orp$YjHhp!mYRie}ubnFa8XFfp6m&GL_6CiDVve$b;lZB%Q1z>&c6x -e2n}da+I7P=Yjn+ItwN#L!YL*=zgGoicVzH*a;qRXSv3$agVuI+|S+nge$Ve&w=YU@q1A#4v4SBRPVZ -X%bP4y6}I -0-etcRL+2ax_YO(W}lkPm}pNZdya&cI^E80ZRd&b-1{lPovHG1EdT0Saw$^*dkBv9>@*JOfPpiU_c32_zHHTGW -khp>=pOeg(gQkKm&~?h+8Y2kK@HnNJ8=MDoZE@+N5{lju^qn_i;_fyhsIA%B6t%iF|fVw=}1Kan?KJb -U#iodFq*vmN%ljcoV;ufS3#)1kJnO_t_AO=A5tKnYr+4cfJ)jf)`C|@H;_rn1&$5MfligxV>~=e3M{Q& -g(+k&)1Gg)4DxEs#@6Ls>o*j0g{?o5nC=YEwb?7i^LnCMso{H1(GMo*Z_uzf_Z}=?kf!b^Y?R(i_)&& -)Ii`~H=;cI!a_oTPSJL+Bb2E0jfF7(x~EK--$Rh6T+=u&+|$N1V$_kZd?3R()o2~sh>Fm*F(;}*`@zePp!ZNaI*EEv9LAuqlbj(vWCA$(Kj|U%1HJ@Yxl3LF1u<%-TBOp| -8t~ffs!IJ)wW^bDzZ6`n!T!252H<#ZE$l{V1RbQTM;={z2Kbb- -4}1jQ!tx;Q5WM3k54J?FjQHF;g$1UX-NG8eA*ylj!3a*9IWY-?1ds)i0csn6(3`VRkoKigmHf9WTf`78O5%z^%@rg^N`eI4B4D|e=#p -7ILhM)^y*Rql|dWngTh@6aWAK2mt$eR#RFt -TGU4=0RRAs0stQX003}la4%nWWo~3|axY|Qb98KJVlQ_#G%aCrZ7yYaW$e8Pc$3$aKYZU;k}b&#USu2 -0BAJ9#79zb!IS-wlbtm%+g@8ScWblZaN4%WeV*WLOWx-?F?y5Cz__kK>zcH&?P -{Ub~@7mL(ifaA!>pTyyedsf5SA5p9YQ^V1_t1LJ%1?RLZup$1{&SwY@2l~A_Mrzpb -z4S;^JcS*RiC--_k7H@b6s;RQ|vPOdBm-_{XM$4lQ#ify!2sQAJ)Z@w1fnwb+I%B*L+>n-hfth_`X0(c(G -d(&+Fr-zis`eHm=9@3js|5#I{>UrtzTI+txm?Vm&amdYdX!Y{&IW+K6-p{@tdMiESjjfr`^{ov0_L>$ -&Y=l~A_vVKd#$qtY3-tzEZv749WYfE!Vb>+NHu`_zLEA)&+(a3$<)%cyjf<749g-+$=C=d*MX%F>O9T -VxGX2vOT(S^nzKqd)E(vf#b>Jsa2A(^b0J_?`ct*7?O$(I&)QFSDHJ7IED2t-y&@bN0S@)*?E4)3i>! -3$ZNzV)b3D`RC5dpVT_9tkOFpYb>2_ud{XzJ!kss%5W`9EHC-5G9WHkY@(LeRaQ}3@?FzBfI+;(Ir{D!+Hjl!f0b5_)Jjk5zncWxDpg2VDC1K -0U?qy;btMMng?FB3H|}@y-ZRhhFzz4b{Y2iU<39br+IIc{ww9vv?SsUr1^4Khwa=j3 -qAYQI6@DI19m@A)JcII!?0AQtN8p|CsCup~U$F9;d%<}15U*!V{1VSa=XVVfzZUCA{PqC9MM6V;#!_B -y=JouJ!J_l6gRAgd=MhVJeE?VD>lxr{0&orZj}ynlHE?;Hcqjg2d5L4xMn@8%sN^AGr) -OK0Byw2vm5Vr^POj2k%4#kc^<~Qhxu;eydn?YdE`6LLZts^aTYzS9UppB1K;c5e+&A775%}6ev#5yB# -cO}MMN%w*DqPLuFDp^>n)3=>kNL@XmrPEOKw-M#f9g*uCtc>t{U{U`_b1vioW)7^tDf-ul*(Znm}J;8 -P~6yc_tX(@xY0T=JVU@K7cy!LEU!)hJ}Eo68*QL6R?HeiGOsfv2kU(D0v;_8&)xYkJ(qAnPqHW-cx?z -r{JgE>y4#aN5ki)i(95m*F?#;4H5eG8;@;wdn~W)UVm!%+cVKqR|4rJHxVocjgh-Dq5K*6;^=_N)&u9s38e6cw& -Azc>gBw<6e^wW3Yb(?@0Hv7R#~3Ht26hQQwwh-xih@=3R(3-gIqYYhqgtMWQ8;Cl^)#rXst^-{6;Ci+ -1eWR_$0~+LeRR5~xSl9wE93d*#)rY)=RBCc-aaN!=pbJUyb)h)A@S^*o5Qzd_rfj87!e4D?yBIZe@rv -@uls-emjIA>(#G%y!LI?Mk4{?uSnezmL&ng`|teq|5D!F6pBB+mIWCll6Yvs&GC`BeeG!m$9HS8Xcxl -Kj55@nUS96bl6iV9 -YiHxMfFbx2Q{646@2L|Ib_>UwSL>G$VQg=gVUsyH!*o9OPNu&?IoliA&`l{QHx%KP;7^$_$C;J}z$Z1 -Pf7c3?h(^FOq;oF--(H-og$^CPg6XmRX@4)%F+jg?7g{D1`_4D=HXQvp)%TZ6co;9!su=4#m;N6q1-K -$O6eXow61;6Ex)!$7KwVL?!-v(i#IOoIDK9(Y=AvnnC%UrIi*HH#OOg*pLwmYWb -Rfeh5*e&-_*wIec&^Ztu2Rp5-xBgAAY5fE_Th?A{xX}yjwb!+_Luss+SAV!oG5A+P7_&v|H=ic?Dk`I!X3AdyazkHHy`Y9YnB0Ril|+j(evwK{0z-8a5} -ZUP~05&1k1L3!5H-7$8@Iw|Elz!U)zzc4)_?6?r!6GFMh-6>`3PrBOUnPi{GgCf9`r9>%90i@jcU7k< -Rx2K0M>W5$pSZYQ5vuXO=T={&!o?e-%IP(C$CAoc}C6W#7@IJ$XEl_aTRCmxJevpwklHudt~1D^#8ft -6}@$8e+ft?CZFPPELfq7qS8Gq%8MblHWtC^pSmUygoKwn-lwU@aNmQAN$qLAFqG#FBX64%(srbVZQfj -VxK6=8Me1B*8I5ha+-(?J$gX;ftRHXXz99Wv36aC?FY#k`7LZW8~E)t*nnr70w?r9;6yKM!Ek|Svx`8 -ETR<8<8z>xl^kB#3G$V4c=1@nx{2wZxEmuTdE3kC_)=i&H)%BKXZ(fAGc^P$Hv1p-R4cA#9>pY9P&!F -C3*qyJT&TouS=apO|qI|ml)A4TFb>JP^HTzw)Yh?L$O>7gI9y%(-K5v;3VY`G@{%R@k-7%+9+SYbm?0 -*ivCDUpZX4(qa%}CdW?;UgWNS_e<<@;qugf=t3p|4jvpqCxUH~pX`WP7B!+KP8KX<{Pna>@dxONNtrA -xo$+nq+wUINYV+-IfIJwvKrt;bmHFkC`?R-g(3DZW{~Ue&n03{pa8vdh~_PGZVDV*9vs-j77rye_@M6 -EuQ1B`%7Qj;t8Dv%%}1GKG3t5-*G(xSnP|8hzIqt4x|qqls3#yI@EZ_=yIQx$Bb4!Z_TkTE7wHB+&%4uy_Q_xmyqr(-f#8)5=zdaLeO!g`2P -)>|U$bNL8My;-M@^s3Iht~tYX=5>7?b;`CKw1&vnO}e;1x%oKW`)^Tjtj;mQOiNgT5|%s@mfV&rkOzH -#e|ItKV%=WNp0=*idyMT}JVtwSQTJukw`Zj-1iW52y`#NwCF;6_cg-hkp-MB|Uhq$y5T#deKMn1z2QF -DY+gXJ6vA#KDl>hcb(Z;fe+k)~Jvn_glORh~EPeoa*gLRQlwxS-`k|pa>#PKFQe}H31wDYgv8|%9jG( -8P@JM`$`1X(&hKOM9Ms-qJ`gtTz6uiHdhKKN=k=qme3h7qA${mJIpAxj`otqUbrWBtCzDoRR_k2IFN* -=q~+ZV@5!KEf6N?rSWfl=l~rcE$9R&>7Hc0q|nzqWhPquP@VxoB=F}btcva{a?yF%*%Jh -$XjS@W!iAEf2ZsB%Y-=86r+{8j@>9MmHX2? -oAKFtY)0Zll>P0&DMA49|wHAVIKKAd6H3v<(rRs!uEcH^?G~68~O=bT3jfiH-CCVq}ON2LSnQ=Vk&&l --nD3~?k$Hb9*?=T{-fex0OK=qludZSbTrK2&Ty%Og=-bD&PWR(FeirQZ-Naz -puS461ut{{K!A!q24$v@l;tq-ijuUC%6`vh42>qesBNY+mtDCOiV7^xL)m9<8*G -;ERb#*N;<%XuEg*I!HiLR~=$PM|fIgK*6fjFWbvjYC7SBlVX-DvCq53{Zbs0(tpguJZAJ4`;_lP+<41 -@iJaCv@*^#>OLNp3@e)PeVQ<8ZGsstc#E@eF@*Lrphu){mA1-nmVN6w@d1M31mUzI+RHr^PTG^zcpt_ -8{q(CxP%uv=~t*bh`jEtqM;eMVO{*jXT_tLQa}G3>0)KJ;kVzZ^t`rWLgw-;OepVB2BBcEp4Y_V#%fY#v!xk<_KtiG7mzc06XBWPQk2dj;PtD0>ImUF -;CY?M~>sO~o6Xu;Z$-y=ABOW`+K4C?+$qN7xj8ykDJT4K?Y`fjpGohkjTIygTxG7L=EY2H>~0yjV1#9 -kp)FIk3>w#d*-%x(yD#IpBqdXg6jpcd&czJq}=Zh-O%}IG~Ps-B -^okY8o@@gQ~FAlnf`vu#Pi!uhGqg9{D3%M=oeTGQ`Ve&k6{@>+^HerRn2MwEHGo9W7nTa|j9+y=__)L -6tpilH84{5#!^r--k?cOXxuj-~8K|hPpmGd5~@4_|J=Nx+Agp&Ogk!MmIExjo7^kJ7Q_cE>>dQW3NZA -rA#duz|eKJuWYr}0+Ul;;)g~V6hy9h9{s`XB)C!P3e?gm6yl-UvljArT>4&R%ced$^W}CceqJ;MJfrX;%0l -dq_yFAF8j>>z(ZX0Bilkyw(AJTMGW=eags3={^HL)Ts^!Jlg-#YCMR9B+U@x4}&CjE8~bmbO&uQKymO -xw2`bu@BRqh=l~zt|FS~LYuYvyw25%7)5~A|OhSIt-Tl{Qe)2DL -OQf+-v^{_{xp#`1I{^!9V&56l4h3(6jw18`_=_}OYtqFII>XQ`U5UK3afz=V3i>O#K8`Uk?PtuieSou -Lp-~1}mR1+4G((Rb8ZpK|8dhkuH3HS;no6_6O#7ii(n`_q=cwc9DE<49bUlJRbvkTeOHNn1CiXp9R2E -8uU0XG@{D@x@`|$}THA1NeTS593$zD(l=hMZF7}?-CEk33ahBCnvFEK+8f~e*QN=_v0|k078{Lr&C%j -q0ezlq3>fNoNL9JGWue&857b>uW`5fAN`2}T5MIbXcT66yn8y!a;V8uVS-X2qKQMVYtR66#M=c|8f= -Ps48+PP1ZUntu2#1JE~puuGlIju-opr%H3YXxGwSbo|KJUJlrTn)sMq+qyFdJGS5A?INxnHp=P4!~Dt -d$vMxQy7lWbg_vtzN#j`GRX7OBtLiWB3$ab!UQ0;9;vSBXlHZ%(H#`m>!|TMW(H&c@ffKNWBjDLC^yv -u4;rZ;)n!XR27fHmoEbnoko~_+S%i6S%iLFj()vDi -}!1d_g_SN%S1yb`)Y)JaHKCc0zcG^6;l))H0v-O66GI#)K>6mu|-UT?g|qY>3bF8K3g;t;l6&l5w6n3 -M6YIF;&zAz^r2eln~CL`ea&ujEF@8u(N2KP5@}5_BKytzThXUht^4e0kF9l*Futx0+XL{662|3Uvags -Gf*t)*Y4fa5ZV~!Der_#qfPTae(2n(AG6uV$UyuA%1$43+PYOW)HtBf-H^HBf)a{hPxF%rY7(s=zJ^m{_ky~^hhRbvNs;vo~8A?KfMAvE?wkgKu=`tth+bcS$FTuyC&Y-rB9q+e3 -$)Rd-~Lxju}UG!udW&-)vEK&#!5__;Gb;T6Vv_VBR*{!8@~&_xAVg`(N>}zBy6O){Mn1l;K{~&Gf~1=Y8BbxizDz#Zw`UFJhfar=Iu#?w`xh?@dP= -wxJCUz)wAwL-@bC?sL_soAuv|vfhhdKia}}ECM`>(H6p$ra7PVYEIcEr`e_>CM;`!^Lq1LpV`jk;K`Z -SqP4f8j{vULKt@oGk$cTHEk>KFvjzj2S6K7d~H)qI3Q7=>_;iVpUnF+kChE86tWvr>w+IOn);vbHT(Yz@>ZhXaJ>FWEm(K)~vvuQ?T5d -Cfda@wg)I@X~pUFv8wwwG#?TKeZylvQPm~zk|UXhw5vVf_p?4DGJT=9rPq?yMV<8F%h4Tm -w!!(-fuDt4VTa#RoGB(!HXNjE_BJ6IA(Ynz839c);k@~7omSVvaZ~ -&S*VBa&mfJ(!a0cCuGgm4JBOTi{fV^yTTZ+hLyEg+5|3}3FNt?ef$B29v-*bV}c0G -N~=2TI8&XVr?2jHXM;`AM{DLmXnJUG<-A?Rh|HbtAfwockjf$A>Uxc`Vee~;hys%N%zAYW4@aEZ2eIM -n@g-1lc2;f`!2Gxvbs`qS~8p`JO$vBoB{n_#n@9g5cW1CAyweN8WTnC&>iyc}afSwFEGn<)R&(1sw|M -tbxu7U82Pi!`BppT4lTC3-2kgY9Ek!~x4`*BxulSscEzgV9=L`yB3g`z9kYGTv9s4O9Lm>ZMX>KOL(uuW#BgS>3}jfmS`W7$J{;A;h}qPyQJCPE%c9BJgOiAvAx0q*S{*q;2ZIi#MkTjcl -15$H+>XwXl8c8U?v4i>ivml4TFos3V-l(x4K4r-ox4!keX_!HfxcIM -D74~S5*Ka|e*yW{U+({@XnM7ty87WM(o6TBx|xRUV(SxaBFJC?6)lCt}g^;AB}I#MY6>FgCs&z|7;uW -ZX6ZP*r3cBPfN+}DHjvG;NuDMdWXKK`^-?3d|!t)W;N_>|k3j$^kRuba%Vsw~(ixW59u9ZOGJIhKcW; -?hp$To%V{L?Fk%NrW%s9p`rKG5AK-BW<2MIfs|qbtcO!yII*Jpoh;R>7vF^N9pfGKY%t3d@E_w?1|fB -(x#jw!fvaW&GxNIUmmNAz9k*kSqb)*H#M<=4ZeaWub;`Ot%LVbiauh| -wVfbR!?WLf9+Q%CBm3wE*2BS?#SmK|L^Av_npc?|yX=l_&%N}hNQ`fD!ye#n8gBf9j#!yK;yoH_LUVt -tD=zoLA!F7(&7zDJGkvw!>yc#KSr=XD_;?O5_L)1H@kT)y9%_H?4%%K9(DAMH=b|1DMb_N6F;`9F>Pe -&`pLR|Wmrlp~}L(j*_ce4oJg-=Un(T#x>fKi>$`|8(05*D^Mg`A+wEkCjm-KVh`4*6D+YeY3RGP}xrbffc+QpQba`3&%U+A^UlkG7auzTKhMz-B4!NWei4a25LwXJg(On(5S(Oa;o@i4(|M;u(#ZV^4TvrPr~wFV)<4zRzUtL -!uJEZ{lbH^g<`arlv2Yvh2iHSyVQ*n49nonE-BXp#}x!ZPtLf;yhWyG3|cZgC7$q?oevpe -A_6p0G|2B_-4CI>I_uy+^}$Yz!e&Z`?a+_3vnsqT=WtD<&eBzUStjK2Q -#Ns&<6D*Bi7E{|S1P3c40~PLZdEqCD;5n0ImTrZA=>$N(BSCNz6Fb6U%++?HE9I{{B{ubTOaDELi+{m -bJN$}9!)hu^}uCh&E3HBLg2d+c&~trQgo=AB61x=mTVu&eFJz{4*GgU`Y}6Tg3Jy1(Er*sZQu`Ep4>_ -J;Rl}h1MGs}3}fT>@SX6j$!pnpK+7CpY<*p%=_G6p7xVOQ3GSplz;U^L=E>_>u-MEaT!Ec39@C6J|Ne -C)Kew6JrEV4Ho;)5Oi{-d%(S5a$H`@#2a)sl=tS@;?`Qr6xH|M@Ou18Bamk8RHejy>>MaZMw3q|OYZq -c?E`L=+cAH#RMB~U$xZwDp_^w&Hebu;C_mpy76{W-i}hqSwq=KfO*8gMZn-3+FYgahQnP|#n>Mz;%OEYRf;}XJput>(Z -Nqsi0NWtHOe9w7k0}jq(9?Il8_&l@;*WrCrzM2!f#jW(;X!$(09=;_3w%$pj_3*H4nw?(5@;P>tfjZv -eS`ibshZ(D7!kaZ&)&Iv4_*V5g?i4kB(e`SN0nNod@L5}~SsSF^tufA~BI0PQ-{{eDog9BS3m)iyk7% -U+jou_0dzYyBpTk9Jo~rLAqfsDj;@EMV4kP(A*5=Eg_a*RZ4ch*@N%6KnlhF1=UTt@Zh9lRbwQSo-;O -Y$8#(7gdFA;G%ohVf8K0l7#LH)Pf4&OO7F8>{WqkQu~O8lMM=3c9%C|iPKLg>@Av>|2s$Bb6xE2KgWe -4w=$GSuF@Jw*fxGGWbs{_bb<%PC8(xwCIjw#Y0VyC(8Hq -%X|A84RpB#A96KhxkGavE7x2DQg0z|`yt3t%0lQ9Kl(#`j*4;eYOW=I*h(Y%M9o<9cyXMt%`tk!`T*> -Y(&hxZkS6qBR#%JAqM_x%;>IaCi-zpM4s%W@ksf(Toat!O}F=4>C-{9D~Fd8{u=)?65(2Qdv@HT8}f30724AW9dvZWeB}!x>>rl($eXZv4(A+a?61kE&~=usL5oew^*B)3_Qvscq~lXfKR5iL$;`qO70sag2Yi5uMOSp}vpm5yp7*N3 -EUFH6k26oH7_)m^v7>XpPZUJlE)D(fg35MvuG&JNx2UTW9n+Yxw`_YX1Le*fbx}#5};Uf1Op7a?Zz%K -Fqj{=d{83?T6+Lti=5k!1_^(=yt(BUkAOycp2)ISbD=EY7eaTE;wCN8Ts`z_+V#^GS*#`Z@H=V&xaXMw*Lv9lEQ{u2=yh-W?XVt8TDKw2Z+F9%q}@looez8mwP9 -IZJxPv7Rz~IyHl5`BeUqMbVJrKyUKwf9?E`ox#~de^{`P6u5*%~dpkR{o!U5#J3vGNlMUS-VdStE{Gf -_4z7NhVk)|ZIE5pcdyFb{_=&Br+S9QBs#`yF*! -2s-Qp$C2!_DkFCI9gGP`hmYikkCJ|~h-7G@y5ExCWnZ9d70<7XO&q(h+r%>a0uio++@>GuvmYv4^hJG -gFLZ_XpvX>}Dsr1%7Y$EQZ{}sB!H>q&&S@}$>hZ%KFD7~a$G2Hy9J8p7L|05ANr+$DHVSpB&7YNC1g`%IsIru`Q?>%TZ*7v})HfXfN^m*U!P;yaj^DdC@G;yXPF-+h3)^ -S^*^!kFP04SOQo!txGq1FsTpT-!~&$KWR3mzsDFj)8Z|t&SVueXQ|M+Mis@K>acwaC2=P`}k+kml(^a -)*j_|t+R_Z&Lwedoi@t46p`JeyVeX`S97~JjN|=N>bpzSu)atBGxa6;w@oWWSu^yY_hzHh|97HuvWhv -{X8+3y#=mpE=DfXkLda2Ubk=u?&RouS!aiOxU4-cG+N;3dDI(&y+lbUVMPzXiFmvI@7)1K@p^Jzw8sn%0Qo4X&j9__($EnV7w_9;CJ`c3;U%?iB?yU -}l^{rzOf@u}c}JZ)LIma&?1!v3i}3z)VZeC$KIy~krhnKf`KspVQU8MSr!^UW{2joVK1HUlssC61#?ENL{NXgcnj9BRw*xl22=O^BE4Z@*ZTzf_ -u|pi2>?vD~e%6u4Hl_Q%3tQ(6v~87@tSnr2zu>oaCpLpE(h_T_#dN=T*&$^ -P})&JYz9qEtz=T$~HU+>S9ank!=%ZuBP?RmZ+-P+)i{mpVJ$~}Z91-%@ujW=`E<5LKGVE#o+qgExp -60{%CH3o{UxTuniXb!Jj@C9!6%Bq|>(fM*chPqicpY}R?}H}CS#!Ilm@%D4BGHl+sC(rz=)ccohdz)h -+TN$?$Z2{arP%*OYH^dUl{HmsWhJI<^NJ8{@8uX_r)Xeo&WaN%zWI~zkBaGg=Kx0*=P`%P*zVJuElY| --XibiYJlMSa`3L8T@K0f{`sWzobC&catMTm6rp>9JBf^YZEM#2g`$Tw>Sw8#fiZkfXkkjLp>qof`t|x -}R@FQ>Wv%$i8T;DL)aazz;OU!;W+Z}u}T0&Yn(AVlyv@+h;UyPQLE~E)@%kr#~KDcSm`Dkr2UH4pw*4 -BZ3iQ^WX&xKz&@$&0^?}mSB6ZmI>$v>OM;Ge)4{Ii_=3EUY=?CGx5xCMVx15m~p^;!#ra&)_+Mf^8P4!#xa^_v}uGmSrO;(Uju(maq*pWCdjmhj$bguT$y<(*o?8!4ieW3}ZoU ->9)gdbTEgu<~VE!)}yWzDH|doRuS;Yo2?`+<4{~q?a~yx`;3y{rxC&5w3SI=7Ij`og&QnV!|8NPnJmO+a#oH|ZS2Z^*za3ZUR^yu&eCtGctc$U3RV%b*QvZN2>{d}*ZxtW&&N#BO!y@J -tYpS1h02a=Tls{{>;kYJ!(emGDvR{|?YYmJuI)ZesNS`K9eUjsgW=!{RTb~`-)~zPJ>Pdq;<1r4gxEU ->Lvqwvab8vuv6Y1BHK)*6g(Qic({elVfyObNJ-#_Lm`rV&IKYOaE-Te;q>m8;a`csU4jyV19GU<0`0{ -z}+(r^AS{fx#q{ZP)h^uxDA`en!IH-oTWZm+J2$$yiERiL4L#?etU{Ev4@!xgOJ5Hn{qh>Iv1JuT~^UH&AdN__w-w+FC5wwsV6El -xlUe|ai2xT3%ly^-OIUp#_DL3=J=70>*zRFCFeoFAD3{io9jj#c=qFa{q)r0K~qosmDRMP(%^6C6=hX -vqVz$OE91T4YjV!j?GR#i*NRR`k@a=h6m1_AVxHfo)-?oe8rPMS*sXFscG*ni{VZ^E4*p9A{g) -N=U!1G=NdH#BeVd8%Okt)09+@7x&Ur87VwWaL`vHqR1+do_FF3G5lWQXn*udWt0@d_I>}i4O{twc=EM -BmNx@;cjS<-y|Y9riko3<7GElSk@9%Kwdjrq;M=M9c|O|ed|zvWuSfi5SmYcD= -St{54(Cr%`Mt;=qwVj2OF#6ZR~wu!X@@*V(Z=LDCnL|uc=VBTaG+JRk-TPJ`5gQ4`Q84mj*)(XdA)Lj -vJ&eTW7IFk$TLx>Ep=UE)Ne_6mt*E<%wufaoBilxZ^(6BU4GbsvA*Pghraa5gucW$%vfJaP3TK)@6ea -h|BQX9#bT~KH2ableQ6E*l128Vuv7J=M!(sY{7HRjO+sJtC-x^Om=xJJ -#dM*o1gr%J*?^oNnul`ca;0^ncs+V~__4deq^oxP$j`-OB;o-it(d6ZgNxaPGcl*0KM -*0cn^~w#(N~}M*FKsmYLUNwOzLZ?QCE;C;7<-<1*k^Ez^cglZFCVbTw<5d-sIg%i9_IIl< -kyWxz-jc`q}lu6)0u7zFwzPJootihj4sgSyjI*WSkD0LY3s>lu&u6`f;yMWLarL+&+-&B1LGN9T$Rg? -D7dZeL0TD8HJtvznE)~(b+brChE?`6I@(&yeE8!z|YrpC)18*Izy>(wK@*H#&UY`NfNtRUJu9!?)_UV -d!_$~&JchEVr@YeJb;l(~mvQFylkWv*K32-(bU{T6%IP1dTG=}7aL=|(5#6)&ZU@TbtGPrpyfw#Xl+p -}(@9LKd-~`thD~*egK4x?7CM>)@?nTuoKDk}(8ko~;RKyODM^u1jEJH^GmKrDNRI>RY%!RiVT`zx!vH -_WskbySTPMSN1+|HovJ%8_v5XGf=$-zLukLN=__qzs1?LhO*w|4X@@r<~MnQYtIv7f~5T}xJUKKxT1I -D5Af_c%Hen752Smy{PC;7|Bvv8GtM8Mc?bUZOY%nySMR_dv;MpI<4dCP&%z%`;{=H^_D6%<`@@Jxxh2 -<#bMLm1bXe8Ad?V@5oPh5b9dzLM0pg)j3;kiDX!Gc%?9;U}uJ1^s_bkwRtn&TMrxpY?QKn>OoJZuoO$ -mH5YJKn+eora#Y=A5s=BcE*66N-KwCk_Ye%2@b4enEgdN09Gpv}Ds?TUU-lo19GVSvA@I|AX_iCl|6I -Bx^mNS;gNXBYgFMEEzLJtNz2XpDLD(fja8T?AOKSTx_q0Ne6gMT2*Os9luBvBR$6vBUL;L@1~m;d!vd -9I)a1XgB%Uf%ev;y~V(7Df+4Oi_LeX&C3_e#WV8~Pt5-jGrz(a@{gv`#ypfWL;BgHbmK;MQhE7WLV08 -8(%hu>s+{M)2DrB(znAta_rnl_KPX0j+p2SIOWrZcKgQ%s-h%v*>m|{rD$t -f7(olYNT#4>@8oVvxXe`WOUh3mh(|MM{MBf=X_n6zoxh%$ytp(4J|8eij8Ic=_HY*)l -dUyHpDnwgRV-x0EddeZ+k(_IGmszJsn%5Z~tFN;vmC0lI^>GA8MF(H(5xa9Y^&GA*u5C*!gEqB~%7_= -4}!4|&({LyslH|8FMz9q$tUF9H5H09*bzZQ#7t0bMlwOzy)5TA|!D*e^lo#;<_y;`0k@Ha2P5YyBy$J -9A<8v}<{5TCPWHzcNmnsrQ`Fr;GL&oo>xBz`YBqRv2aE`zoP$dJ$-EG3UUl7`Kn~0%>@TOkx_AQ{_Nk -LA{F{A|jswlLi@^Ce;47eocL^*Wz(B{{!_mj;P-;vi{-mCdQ?J?k|8A!}fc1MH%;tV*lp+*M}^kb_4o -9W2pDu!gDUy4NdkegH3jD@6oZg#^#@HMc_c -e;azW>m?mOlml1;cBcN5&+K6&IxT_tt%kZEdrPlB4KfKFK4_0Xdfd`g4CL#ydDPR~pCuxxUB)`tCr#a -%%+xd9!!yWDL>YydK^0qON$sq2;wK0`29q!+ds_&nPoE6fHfV7YxwflI?-4lw)qFr&G75)ytTQcOT#M -=W53@SDQ|LI*W-+&k^VH)}@Ha--~C+-7x2X1Mtn8pa+?U^KV5NYFz;3&$NuVF8 -tT@*cut=^AT$ZfAT+q$G6jOjK_uqJf2SG{(?{a*)$~z#Y>eUY7 -wK_4{xm&~$1lGNJjU=Al<|4-HRF9kyd=;Eks2Yoxp(uW>(OqGLl>)cQC%e#(cO<{uh!GZF?kpdA&bVxn`8UzJ23{jqb~8J(7wib# --kjRC^EBf7KZL1J_^cO1^mux|FTf60ly+dX$t#@O~GVISL-T2hH>iFTVYU+ --4m5LVJqk`*9{w*!em%mD*JmLCH;UbP+hmZ9dW7Eub5@&4dHwxuG%_V2#nQ#ivEpMvzlC1Y%(&Fu}#q -}a%%Jk*WcLE0PcfxVcx=L~UYPMQm=my*$e|_wA9pn$i=fah^k4kKfF4wuz=D7m-#{J!6>t}gxL*HIyf -1XPlxt1hxzY~^E8!;Mf5B5b*1!y)C6!ijyz;s_>nUO1t$mDD_M^`OkNY>J#d)|ijr_7NC$_fhR8;x3+(+ChlJ^lEy)S? -4ya{=p3fKQ8ls(NXn>_Xrl)X;G^-?eN7xDOQ;BjDr5%GUh?%%mz(Kuh8q3{A=@iGPga=PH)9iiQ@o87 -|Nz`erKrx+*uoRHm(4{qGbInJ%HP3s?AKpWBp|A~Cj3;N6k@34H=oua1a9r8QO{EmeDMP`1kyA9|LiR -bm^xdyFoNB7@|=A+Lw7=NFth$&|VfycMaGh+t9$Nz?37;+;6xO^3OrJg+EjI9YDf6t-kxF?PXY_~gpw -X?}$AJ}T@-`%GCRP&;fy8h|4=#IHV(Gh7ou0=BzT5qSGtnK -XQhyanxSs^fN1e)M_@Yzoq0m-2{}}fQ`NYEc#~fP0lB$aNE%oNwRLH@TTpQM2Sv;^8cFnm*jqkVX&H* -QQ^P|meg&ZqeWUgE3a~>^p{Khf)3TREZ7Ftyt9)0cDTG!h^3!XE{F`*5x?L0P7Vps>~w;l2vvp;6Ee& -Vp_Sbwe&K8G|JX3Ri&pkEKG}o)W2L3w>K0E_{h{~wPy -Pw9$jlag<_Mtld_xBfM>;Vb8OTu0VxIgYp*nh?u-{T;;&k%PX0KTag&Z6zWd6(KBB)@BxS~D- -#EiX)yvb~FI=BMP01@Ewp;-kEJ%B6JGx=hhczFDGKLd&(Z0nX9%$e6Q==iQoXO@J|Ac;6h2(jPj}moB ->VY(`{1t_SkP1^Qxc(`ICBowq_9zoO`M!Q8hb=aYE<2zco8DI(&s8R7QnqM;f5zUZI`&8*PnTueF2D3 -&@nO`c!KeYF-j#l+9&s6D%mfN!~e@u)-X2N!@Xztz0w+OGJ{-aI+{w-xhgr -^CloQjDsY>s?l(?^7eE+nO`vR~`C(2B-2dWpLyn4;C#6IijPP^uOQPmS$w>9=W!=UsdEnTpSxKHv4FR -qso^!n!^gL&4%tJCG#P1V8to(0k;L!=nqQ=o2D1+x`1m8Th05Y=-`ZDx1?%Q;E9 -)<7JG_`->sPuYISu8!|W2h8qc3gw3+N@&O58NUeH0UR$ZF>p0ua{?hG^@1k84AW=3j4ji -%dYj5abM~_yC_L)lNT`TNRx9rp<=+na%Rf7PQ|OiX>yO3Y;HUcBae;$ytIhmrfkUE9vR#a8Na+-Q5HxhB!>IJC1I@1Fs_iXYQ=^- -b_}alE#eXO3rx7M?Hlr3@vDsRObnsQpk9{kIYOkw`y#*!J{oNE0O;DQewrKk$Fp9@i^R*p-hBKi75tR -QS{A3(cEFh-Y!gJyqfF2Jzg_G4l(ljG5mz7yW3R^dEZUp6;#q&hv?)QLXdrD*7_Y2PTZvKW}}xqI0_G -f4^YrBNyzoJ@yf6rBD1@rf8!demh{;gM?Tn7CCX09>-t-_vM)BF4Z*J(gTZUj{e&$=+Gcsfy -tXzoW``V1YG1;>&A?CaYld96#JadI+>fV-Hf@?|Pk`^aw@|ui_q|u`Ik`W!-%y|2M^QuqUlVdHo%Pjb -DxR;-ywScx3H9%c*H8M+8mI10j8*qHQTNVx-KA#TbH}KA9Q&z@^5qJ#(97A-*f^2=&XzB+%dpzCpy?zAb@I95T|7p!QXMU=h*dBw -I+mTPFoCEeB1XO0J5QhSiXm#}wT%~14*?Yp0Cek()K`%;GFZ8@%WAw%I~EIVSXaj;{390wZ@Pu!!$)x -~)C4iisnY%M#vetzV5Sh{(>iU@th=4=USa$hfryE?qP%f#JWo5CH-?E~)Y?-#kw_lugb#@ATheB@nj; -<3WS66oeGJDTi(zl92fvn^HVxO=UxF=j+T!pvK-L -**{3FEQn_M%~tYYE((+s)ZJO~~fUbkLz<9$=fi(Zj;%rTYw@+~}hK-UJ|+VbR1^3mtf_8rhA-R7R~eM -x)5vwof}sYS5Vta@+=`Kn4uc7P>DEpP+{pOWlL7#%-JWR*3hxeBnb3Jry?Y7!ahrX{z7~%3fr -H506T35!X{}R~z(uV2eJ~U3* -wnfsPIJT3%Lr{(X4WFN5>PdNij?zaq8GFfnFd5$>-)SkKhjs9oS>I9MgXL%k(?eTv^;D#Xz5>3M@3ai -{t~WjOWxh*^=aJ_It+bT|orYZd<m -Te#C<|HP4e`+EDgIRcfC)b04JS{mJcF#xnPYthKb|-M1SJr_DZPFDeQNvmXmHPHIO&J=JDCvGO>USe| -9jui191mKsVQ`ao-Dp0T~7@xj4VN8rTKqExw;?M%7<@uVEN9@Aqqcz9le0T>!5tmQdF<)WgYS?KuwS#TgC2bFMb{^S -U9%dX6f3WtK{V6T+zm}$A}--o&xj(R~MMzJoh0DPN#fVI{N5lC(nb7(0AlG=s`SF$L#UQv#}yPr&I2E -3R#=tqO3hUN?uDEG}uB?4w-aNbW?reEc!NipX2_Vn+PHu*KhXyy@36?&3&vr*2%J=1aD=IYljE -pm|5;HHjyUEMWW*g553mQX?;8Al-m}e#?&3|tbAzNrHy~pt(o^ju8>b>S%W% -o@M$}bIo9?#^)&qW)5Kh=@tJY<&Bm{88PamyJUe|-gVsSo^;0l9E%y6FBD@Rx2eBAqhcE&at#n-OV&j -QGf{lnZ!v7?FF;`w!uIO3N77!?UBb{A0O83tPd57aT_5gtxhMr$fvAj?)@AaX;*)1A6YjO`xl8o&`xC -wPd=QyQfb2jZ?I7Pu~qW^!4FuYRZqDnULmXq;aK+?yqL%wh+d*&KG;W -)dEMMA`He4%L7w@>F=OWKgYMmmb~zyf7eoFDsdLfhTVLF{1Z_K}i3rp2JJT&fnsfBSZ#%j(4d0Ixi4b -(8{65o6%XBQufijqOCel@zY0_*eO%Q3SG`lRT^VTCfV`J-}-Y!&lGXIJVQKNi|48oQ5%8Jbp`kz1a5bMpWCL3+wQ*|*W(24YZ)cql(J)ji~@OcUD>$HzuJ$)@&d&QE`bs4`wOPZV? -J7dn}1mwP|d0qE|_AK*=va=$yVzF7=FA2DnabMiy$sv6+sT;B -Y*eSGUXunjMGH!~LadEoTnfR9L+wF4yQYn9_PmjoR81T*`I_-7^TRiUOg?PQGdgqzLBkPrWM7}3(Ke2 -9}!y21|mt{9Qd4}Y9?`1s_+b<*G%xBrgUq$6vwdZN8z7kdIHpeFSz(Rh?1#>QKC#vCf$w4tD%Dx`X>HjCbzgNx-{$+TA?6G01VfD#+c~*^FF&Rflgc1EyeQmORUZe -DYHV>^VI9r&)(%lSatDg!Zufx<6D#4j@m{AM}WPezP+2Lq5MLpI@rG`oYLQ@%d`1dcOX_$a8$Yo*KDP -`)I?7ib)|&SVMD*)P54>!#>G -4jp<3}#wPCbTW*Q#zN`AEeSXVE?(^Hfz?&x)_($*aTXw4@KF`{xkG;=ttp2YQsdHvn*RCRY4)9C%sYb -X;*yr&KygKB$54_YPvgUgMx^r?Dd~Z2aT<0XyOQJ@=&Gek|Na8EO6j_#+-y3F -4Z$)3E)f39|ModEY*acg;NbX-sA&NYXHx;nKbo9el*n>qv8_3HGz$=pwcd@)}u@IjY^TqeB#&izZcHz -wl^xgUURt4r<~xXx_rd%54THgP`VTN-r{87s!69=1lkP~%gOT~Y_kma#qgKAzuEW$q>3XAvcTsmJ@zB -SOpvujPP{3YYX=_KcaZ6@b8#+>gOr~h$Gi8jPr=zkl`{#S(d -zM{+df$+a;qU|9~xV}c2I7a__U?95V>&VZ!)wvd<(VHe@OaRNwondUG&C4@Fxp&0S@1i>lo;`!U)`Wi -cJb2Fk4`Nwov|YyOGz(?VFdy@r+BA7B;b?Zj_c$aP4rk(ePBg6KcpdogVe0lX?bS`XW6ha#eA_&!VQ& -VmU6WLr&6AdWhJM|l$qi@u{oG_T@8o5ScWBx-yO!7V6-`a4Ke1EI?{?=s+<@?zUtL%I~+k8KJ+56@Dxea?$`F^hXe(tjQ@ -_lK;*%ZDnHQ$#mBd<19mNoEPx~A1->V0L|vYGPz9Sy5&e1C`e{*GmgpJ>{AXTx4A@pGq%pF5YirG2zn -;_$GAXZiBpzGgG~E#CWe`x4HLJFclTd+Cc0sWdFZj`xfiv0wiN)2f(I$_mC`>RYOL9>SutibgU$=rzF -frc7YWg+E2b2-bf|TSgs1*->xG4Y{s*vsfnO%x1`_--u=G3tXo`Ir%DhXo|^i+~WhbcB9iFLR|ahH}? -fmXL|*znTP4C9O5|n>zF+6is!MWt)YLr3cOC9cjHP~hIFACd}ltU=N>1o=4_R6@*~4?@>R&m{K@~zpa -09BG5%z}%wuJ$zEisy@R4<7PquGDj3q4*RwNZYSWJ -@@r-^xRfcM+8UdIj#wv==w|G3dD~mV$BhTyj3@=kfHM -^r?Q97{4@nd?hh1{))xoV+_&r@MU42bm#tiMI;wjf6}?6ucEp)9;DyIeI_lRv?-q_Vv;`UCvwXj3;5ozgrQp -GPlwG!4Q+vGG&+E%LN8rTsQOGePv{mi_zZK>9vqi{L?|*)*@xirDdDhN;w#gn+@$mL-a<5Nct|r32j> ->(ieE!6A&1SlQJOeR2)_#qDk&s@P<#MDh*&nG+OlOvxc&=u$UGuhk#GVYk=W<2Zq_wZfDQ0tzi&N?>_ -VDi*qix2y%tE=(SqJA!f9K#mVaku`jGukNXe}7}Kr8(DmlkNLp-Fkh_Q{UG3Hzgu3>M!sS&m_!)5SdE -<8T3ERs!2cd_zC_SwcNMX5A?=COAKKc8$+vu9@Nel>#;Y#&OPWGyMw*WjqokuLQQs^W5A^>ocFN*(uj1xzxHz;#!L&}_=L_eYBSwaW;(w)7DZh~K -F&QTI6m~!e4d4ebnqumtY>U7a8oAFvsCAM)*TX|Tc)ToI(Or(RVtc**k_l81`ax;D|n^ZVTK_Z6ex-{^eFt30c-$r$dV?62 -&n?1$tx@_q45X}j(KzRs92hCK6*{)QJkXxFCAA^my3+RbyFoX)T5dJE@`+V%W_%iw*^?W}}t)rq|2^` -fB%_E86LU0y2WK1KDqD7$R2cX3}(x0Wx@bdkQPSPqzD@YPUSwuttP9c -gT1-#?iB%G5(D6vUbMLm~CUb?2bS+WzBNUzM6Ap=N9&qEiYDcHXYKB7G+8%rS02x_BCFd2lpSBeGiZ~C9bgYMI8Cy>H`5Y^kaSR85qlH(Z@+|7%dH~Op;`aGfnp@7l$urR{@jP| -9Ysrnyqx{d(JAIhm=?V0P?etFcPEquZ(VBGMoJM*ZjimQ!*vTEv5%g{(-R>Dd?|40=chIDFRh-_9WjC -OAWfHyXY>M7qo1%BRq<8uVdh-lCo}8(VbNWT$J$UJ^s#eR7j-8DDjjdz-N;o8DMdETt0> -nd!!3xK)KJg -uGvDxQ$@`z&QTrhUr?-x(t5mW&j$Xsf9&dB`Y+d)FW^2E-lKhtCoK6aW1Eqt6Zz7R2j!Ih3+(UM`akL -j?hEZ_ytbAu*Z!@moYg}AwhFj&IL!4-TvHBuu0cO)cfjvNI>KKMdBb@%*#^+JJwxp+Jn9>0y{=-`F&P -tJNngUf)TYaL(*obkz|}f^;*ztlLtY2Hxjy;~>{Z4Zcz)DgeM_!f&p65kSc`VN#IsjWms{kn5gXk#$5 -0pbeEnsk%m+QsITL$!d*LF@?yJA7Vrj1@6xWb2##&%9 -6%nVt6gT6|M4MZe@s@fs=j;jq}`X3Z1>%l07vCeqTR={OY;-$KJL%sYgBPpKH7cr%=C99lo7e6Y@Hk1 -ePi{D7h>~~zFy$*6!3NbW5!A1Y>HXW%?b77Xu|dRqBzb@q5Wq(qV|Z{Z{A0I0e2IDvqg_K4rTyv&i7> -OT7q_e$2}5fRZFSs~vCA__MA&w=oBa}2YWNC#MfOwn(cR#&s(z(ozffpw@{s4uy@vXbmSrtLJ?2`GW -h`?h`t;lI!}*QRzTYc7F7tEjgZn_~GL}o`kL4xKWPbE{?nyA5|3X53j$@3IU*^5Q^Lp}qf0!)Vc3Vvt -WLq8bY;mV`aGvb*%_*VNlhql_j2C0y=eiHsu7#qEbUVoXm^lBrC!SX7xK$#w8T8@0V;P^Njg}*I!2iV -pBk~u(fB)Xf;u4!ZEgScYVUuM-XO1Xyo!Uz}R%V~u2!mG|oZ0tpI+7}C`=LKwnjcBF10Q?-}jm8YMpk4w3m!B?&;V8+`Iu>q2E%_HGp4KPaoI}+Q>cM>y-{(4jX}cMs -Oc>$nk+QVx(=bILS6}XpWZE+)oSX2cTohk!K5hC>g&*9s|saz*k=A%_`_kzg8giWC`?Sp&HAP@ID6~< -^Jw|_S*!Ux?KvMnb+cde42@8?j6r(ml;>%?pAwky3F-M*WQ+Ar{*v%=aE0AD;ec9=MR}D&Fr7K3FZI% -?U=0k$vf!rONG`LYbd`u(j}2@F`@{KFWKRUvMx*`Ztrn7lAJsgY;b)^W?URXE~+<+pnw>@@K1JlYeNuwxPg;brO86dg>~`OxnB;_8 -8BFzauM9{Wbd9fT?ejfAA<^np@=D1=u1!@Y8aexFFB1J43s7(j5O8QRV~99>taOXFMmDbDecsAcyvtp -#@|Nmj_pGRJECL&h&NMD2rz#+RgW|Ipp0ON1q~cHsp&4d{N2cs=mTGuYn)wSIco!`EKeQo;Sh!O-Ro) -gh`H>y`skXyp5k9ESx0NUQwCo%V=lspo)_o*Iq~%w>k}&GM#G858-=nt{g)u{S^F}Z{f#t8kuIMGrni -YSm$>WCKvEe9;sWsevtFxI?8HVT{5^%#`k4iSQw4gjv3dG6d%nv(mo54V@^<{^!Gu1C8$Mm_88xQ --iZZ;`}vy5+fXTu*`YFF2qN(ElK3+T@ujx?E?~4ILYHgXaM=_kiOXtsDnv#yqb$V(Ta3=W)yN^v6V}e -WD24KMY>VLjE3Y5V9%aPKakr9*>ay_!YS)$>o|luT6Gmy)~>7dX=BIdj~u^a+_pn-4nX#?u*A!!D#Fy|a$k -d4#zwAX;5m2PhqnBSn)Lt6kD6zI{6=dinjmWD)~Pra_Khg_KY;zje*gcl_wI2~Uf16EexBhn7eHZ#i- -U0hYXW0T8U=(TEe{$~l(ablxwSbFCMN`tJQa%!PrZ=e)oFetbTBW@bP8*_XBVUVH7e*ZQtm>GdVB0X>T}Dl=BkoN?Ud1u;HMA+0_0imhA8V&U#2~_+XxpjAB#M`6prf$mi`aU$`Ly2 -{S=rk%%ULE*o{2N)GI`QWe%~Zpmu`*1y}otkO!|EtWIFnMD2nA=i~Hs%{p>0|n}zmnieR&#OZh27IjJ -*mRQu8@u^Plguq~8ofL~x+N1tflvyy0+TjDnBHZ^97tzW9?$K8~iytJI#6eP_=IRnwHRvwosDq*R_?P -3#V!Vcmys&7H9h3g-Gg6~q|OvLa&o9d|UNcA0lmXqT}S;?(d+%GO;<+%nXbdSqq3x5V07tnic6I+{Or -P#)SbH1E5;^RJ&X*!5EK$n5>cHE$>w5j9$TiDlAdXb)G&%ET9kxzPkP5r5g8+zGL{VL=xsXxQ{AeV0~ -uD=mqOS|gJS!ZQV+%<7M)eL_f@c%-L;A{iOYaP`FUY<17xlJu^!)LxG@Z1VHZsvTcz9&J|D@%n_ox+a -P;(BHz&xDxIuXRVUoH-SM{n*)BIf^!I;CCB-B&BE;+uAfZ2me%t@VCwHIbPw57EXJLZ+r -REj_7I#3H}E&=Xuqpl&Dtxh%q{riYoqa)1NO0F)Yc5*H~j7+a-O$B)u&@$kW?RsG$BI=$$Klj;!fsw^ -l4_n_b=nwEpLivn_)}BAXh!5Y=YjN^BJlW`g`d8pu^8f*YtZV_h!|`8S{LT*DwDezBKHf{~RCQ?pYg; -K3XThOT$^T)+^-5hJbRQGiN$}7Ul9Bd~DXx`a}D)wuRm=Xzg1-vYQ8XoH|_=x8F1~FU}yx=-kpz=Y~J -R#)r!Po?7pcfO@BRHTm_mkoy!r7RU4iagT!i8|xc9^%^SAbxiR>uHrhh^trgxoZV^^GI)_R2Qf@@Av2 -^Vh;ykz-&5#2?YTpJw8kJ`TN1bqp5y@kxH6I@L&y3o*Bw~wZxEdwBnP(3(M}W5!$|bVtgYrc!};kkEd -?Z(?o$1NqSd|y+5nC>4>_i0z`m+SELQFJq$rM83!mhLL{F>%Q)YdFx(i}~f>)yXH^tr4 -q;!M6~Q`aoO!*O)DB>jW=66E$ef^cL8NslVz(INdvhy{{GI#(&R>8NgBWw_yO9#`*0a^Q4%`8bZ-e~^ -^%Y6#nwC`j$T&sFH}sq4wD&!)@TV9h?{T^152~Mt+83KxTUF8GOk!4LnM{&p9wYf>7oBY`RL3)Is0c4Ge8X)w$7WFa* -#aVxcK0R9H2euJ^AS<2IahHJj%^q4$f1}?XlbG+CFHpF1imjgQtblyP*cseYnWg$>UI(2ysKXm@#uz! -5u%L6v4xCG3&icmOSmwY)tyBK_j -aR|7P)PLK@vg-FSTFwDR-&SrHuF?O$M(k0KPi3{VR?lDzeq~fHVN9UIP;o}N-{Nd8KXaToj`QnzQu#g -bBH=SubI~sYUr*m*%RZftS%eGmSYGMl{;l3D8Z-0R$8slcvr(|NV#Q96r!_ag -RA#SNV#eDU=WY&Y)1Y*%zO2W0MVe&$zJZ!rRYlHVJD;mgtb0~+>e{3B3T=#gsnUGk0Dj}PDRH -BCZ4R6DW57ko|dCwS8r=(iu_G=mXrN+BmPwhyjPlf(}q597||H<+ -e1<)yt=u}Vr(SIR)ZuDRHeQv04IlMoG!rw-r{+#}T_%GGDJfzMmPrfSlYxpwJ{Px%w*Zq2M_MtTT^ul -Mfo%@7GWO2G}O?D2vM$bq_0jAWzbs{FHDU-R6!x!pC2K$+J426kK@QP!{X@B1&e5FWM@&YGNYhN+VYw -(=Rc_O_Me$KU0E#5QyNXI|v4DS6Q?TyE`0AFOVUP1 -S?&8YnitylEXDhb_Wc`*_usYeg^IU9``%yZT}|J)Pj7(#8SJdM9CcikqbTiWpWpW^)L#o{-FjLBxF5r -s$cpDOC|?gq|FSjFG?8uIL~CLh$L=t@?wS}5pCnj*2<0a@pO64_ddtQ+Zphe>zjh@ETQ}fh0ACS!-4^ -0qxaUSb*q^*kdCGmhY^hf0;$FNY@Tu_pFnok4?T-oKE^ijm3U<8MXZKJV_BFMfo{j`zOLvjSq_nucDe -T31uBCT)TN@&Ra0U+ZzN$L803N*Ob77wuNMCCI>3X$apyMj_Jje0fs9y@nsOVF@+9xMH-$G{=txu0%5 --~pUyjSg$L+#Vm%f6-rzGsKz_3e{)j4luueXXhfbX2kB#z3oRN`pZ4jT5T-v$=b~t#-$htc--)(4{>!!3Sw5@)kzyuf-Z!B -DS*WC7{0U{kLj!io0H?Z6c?Ra+yXPz%rTXJC -osXX(VkyYSzBX7eAHGu+ni=*9bHqHnf9F$du3{AaiJ7%Vl3Yl!}2$xUN-zP;>zL<9&NxKU6;&Ctk*E} -d|PU1?lmlaxURmZmeLnx-SK&96rCBUA5T(#oS_fUGuxZW=Xvq1&CvB`Mv1Z>2bPeLIhVtZl-kd+%7}; -SSBd=_vC%Viq71{E%EtLotV2fGQeO|ws*jS)$e&5l`swt%V%q*@_$W-@$KtE#bJ54b*PuvBG665EXd+ -vPJAC;Y%#1#ye{owc?(nx=@#Q|K8=iff=m}a)l+`%abUvt#Vuwzq)|Wg&ZE?Vda1wk7WBL4e^1Lk%^S -(|>=5wf(>gyV0@sK4Ph_OTTsg#7RR>hg4c_f3xSDdlsq3v&S+0dfKtTnqz-|{sro0Hw>q4Uy9kvx80O -;L4fX?4Q$%?26|lmps7Vqi|p0fUs%t=H+hf#qc1>S)F%;{CSmbXY4QS^lD}6V9~IJ43n{mkC&{tLl4- -Vh=RKC(_t%Xwg&K3`D1Ol&4OL8A5*+Ii)kmyDmoL4a|r7VFQJ{w;2{(GI6Y-vGWxL{H~++fKQAivjYo -#JWT6w_`~OPk#>iX#ShUMGg5u!l5%B{6ftz3%Yzb~*L1GAH~o7v&aU-QEFSCaBCWUD$#j-CEAEfKhxi -rDew?1Kq35>UtY#>=wHaeG_+D#sn~Yd!rtTui)Cj+QCYc+z%IlgZrTW(q?(sjJu86fq<$R<|wjb9e*@ -aC7w@v3bP~q^6!`oMD=V;kZ<=0f~Z?zhx(b}T^*j~#T!rlNsU*aG3^SVgRLx9yEan8>#_MR2|g8O)cv -03idMf0=wyubUJa%q0k`f;Q01JrlBWNEBZWB3%QaVslA;#SI$uxB^A6#9It+t-BmhiE_f1Lo{*p;tZyX#0Q3s~=n6{)UmJC$E`4a@72l^(;sS&g=6wsRKIdcABWnx{14zCPooXs4ueG=q-Moc1- -PMYEi8;unv@pII+>NLy4uyuJw76)WyolJy^D!?`DQvBghB5&v~cHPx^&jbbTnt1?>*SIT=UjGg(@S4E -m1SygPJ`Cg)zVocBOHl$VMe2(y5<2tsQmm;Wf#nyNL>y5)>& -&<96ZiSJCz2v)o^_8MD*)?b6Y-L8J}f&9`vFEk4&S!&{n?G8e6!0;Wv^MSY|{Akr*DiumOKtXh;P$qdmZ -(D8uoR>zbwc02I -WMH=*N*VQNp=xG(Vy?nSB3s?EPEbJM$1A>;&|p9q<+=WxvG!ovW;=QYJR=smoW#_c=yEj^6MhlPTOQU -T~fX!5$uqI%78pGqt;gX9qQY^5kDD_BAhQ6;u^27tsaJ-|DcM;>Bk_A=|+8?XNd38dY(>sJ@gLrl`_5 -a&?&zy^wBZa>R)=rdEvq$Dd;Te;C|wg>z-R7XC0{rbUd$%0 -%q~+&(RB?4=Ht+wWcK7J9^Z~BV;+;D}*M|6h9@oQ7Dgxcxsl{GNOY -m}7V7f8k-4F^(NKW)OF)ZL}VvFSSFpF3;M;H>AFinD-hE -Kh`1QKD`uBSA|;FRM=swbpfXib!m4Rr#=*W5PP2A5$q0(T@*#_fzPh+572S^YJMNU)|6CF5oda5Kz*o ---(l(5!tvtGjE+kkYzGGVS@tEv7Ow<)iloT;o{H52B^lmTJ-HSG;?Iqs40e*DJR2 -H>qVf)piUF9XoEzn_fQC{p>2dI6sh<9r`gm7~9nC&VYqg?_1&f+|;XV`#wj|Sw7N!lGgrW9 -xs;0@gQs=%+=-y7H{6N$r{agtWTT|dOoJTkj@7#jJ+C<&5pBXIThMG1HM-1n`^bW -R(kf%_p*-WWo309?q7ht#Q7AZ8-Ag9=^WT -GjcE8$F>{XGfuup!F}VKV6Z;`5h^h;%*st%;D~%2Z2q%(^*h@XnZ41OIo9UyoPPo;{Cu*a~7?`2Qw5m?!d37GnJmkv^;^i5p -SSP;<88$FfDLbKRw=I9m{?R_v!zQyCSt-TcmAiLxiZ)N_9XV2A?GSPC6rEFv6w|zZ1>~8+v@7K;OBI3 -V5ZY+NoHSjajOlO3}2K7VtWH{!XT3aO(S&{P`2nK7-L_kolR!-^SEnnq)wx*Vo|gHPAK%v>o}Kt}m_N -dYUyQ6&b_}6BKt9kGsbfBHj|6E1ZhN?_j+@R`0Xw)lVCfh8`5ULvHJb-+fJj$A{xrTFf<^W3u5VyiLI -!a(1~<@ji3G*HpKS|wm$!G2EiyB;xXi+W#<`*}J?dYRN}#W-Rw#HWY$oeI -iZ8OOXhYr+nO&N{W9P(MOr@!<{^egnXZO(edOL+vaI@yGar=W~A&$@w9$Ie`C^;Kvjk70-e8#fuYyv8m6BI8UR-; -%gyeQNqVUsU2%9Qq=s=b?+RbpW()0i+?QgF&5yf;HjhHBdUIQKNx3BnrKY+oDySl-8f@%^1Ns(c=Xf% -#F!`nW1_V)w0*aS8JqXRjLk*jt-*O7bp1pbuDI -gcefFL??d`I;JM+riO%~!6nb*Ky$Lum(ncG_@B@UfjyRLZ>l^H|-FVS99^&@3?EFF|yn$-AXDq|9lR~ -|dGmE;q|V(I7cXf3XLNzMX2URGmZq*41&KR!Q5lg?x1IgJqF&t;5=|B5u;;lf1q$@(TsPrd$;iMp9ID#`kdtQs%JFZr3d#viO1#e=(1f -tl6C1!U8bGE!=2+jvZXPF>vK|Eh~e=p?VXUDNS5vpXRAph`=szYi8$EcM6n!6XXzF(gp*ub=zTqudN5G_f&pPSi#S-gpQiO&b{u^lS-%z+@IYR~olHR4ZP5Er*;GevmTE_5;QnfsZeLw=J8bNIb^3P -1t|$KBMOnTja=tg((fqsj==>8c?1S{NB8DB0ZL~y{7j?n%Vg}`dJX&J}-oBC#zs9in)O*Kp&v1t6e@) -^##FJOJZKO9+=tpX_G3YZpn)zO^SlIg=TPr&?8$86EwnlG^b<}m%&QU7P@oD=9=ettUa6Ie~3`De{s5 -A9ZU(h=~XXW96@m}7u-0)IUleHQD|;yQm-^mi8R;hyb<`4!x@GSQWVI|gp+An4am^urqGu@^4-at>xF -?FQn_(}^}d*~I4>?vZtcBp1deH!hZvyRqg>slvC6-}MEQAFq{P7_Ht_i?TgS=7>Brrh0Wf(i(9-{NKJ -N?^N)gz*xS57fO0=F?Zr9n=RoSGkUXuH43+YFy4X2Ci+TPky2akf;~?jv63n=0l1`pEx`Pdse+U-AiV` -y)A@^zk#Zm-^^D{5o{Dm_MO$&HOfI7ieF6S+y~H9sYm;ws?h$xsK(X#I0%0EtH-{^2=7*?=px^k$>?u -^$J!k>%W7@v9ifIjPt%z&EW0KN*p!B*`4Q;@ -I-$!ZkN#V2drxPQwSU86GSeVmVo-~Qun(YX*g`06!vq^Z74u-<&%EnYZ1Q6IU8&k3RtB6WJ7v -Ya{j$*Y2c9o%8{&i?+m<-)UuQcU$Mw@(Us0Ihw7nl!13s<#GVNeehBHe0zAv3)qw?y2el~2B%-yX?bjGe+mPfQ+Sdql?FpldUif~rY -Tro+8ZX@NbsH{Z1%7wtPhL2{by%}HMCLwD~cpd9|@*(JD%A*u_Cao{{2Xz{Om4G#`7kqxM@E>PJmaXU -ZjU>BM`Ff7C5>d0A=ADW7pXYl@{?K-oKkz-~gip%}%4d&M^1ThbghKDf+asJl9dk#}Z`eA@X(Hd-siFDw!TA)C?=O_^--vF&p8 -1UGGg4YC<@uD#{e<2*FilxGKypvzoa-BlD<96wTnxYK$ulz-vwY0U0j5`c4&L}T&n73e0J1?L*j33XNcNMK?@9<5l#7FNy4Z2SSqUh@|Nr(p8TqQxRMOGTv2^JNC)6M0f+R!X{f -pu}~GR_Onf2$aJy4@{1)Y)bJ*)5m>Q8gN|)*Aefov(o3ytd -{m4cOCKYx+LMZ+DdH!wlObDR-l+ -mw6{@!jc?l27tLbrHu@GZj}+UrhAhwxBAn8aD9Kl$mV{>^!bZ%3jUGQ7xanNUNv;>-==o$jf|+71Nm^lqrC0wz?&t#-Lqgu9`e70F{Atk=7vK?r -d(SK3z=3eUoB+3h-^ye4W1Py&$#0jy-U#&`FdWQT6j}Pyd|n{jeo-{OISzfAz(Pp-pFpvVk(baqVwUjvJ!5&t{n<#pqZh1xXm4D_sGE{C> -jklIy5?Q+PXT@wP@(-+X5`2p=g>}jn%kUO>ZfH$VEOyPH}=o|DK9`iG>sjT_L{B$$o)P9Tjp%1vXh{M -V8b@U_c4tJl)^AzewWJ&CacSW%d;@!1Fw6-B54d_fR#B!|&is5P+i3wXBX>6>Jl-e%bdr67i1_N`>rf -0xE4n0?DenyxZ|4F5QpegS&`?S1$>2|Q*0`@afEf5!$$%&a0?*s(Sv0ZQ(ypD+{Mp4n4!2%5!IgufQ_3Y{;bBxE@g0GW^Fr -VK0CB6xTrn^hw3-4YLo1x8MBJh+g#~t$E6{`xgU&_xbe+yP%7vG1(ejUviem*iLelbqkqkFZ{+5?!Hu -RVP$_Ho?6zTc=tppwJ-$8Y*oCp$M6_TXxB?B=5bjI^KOn{yZJu+oZ@|j$1q_X_Z;&zr6sTql%c&_s1x -u0GT>e474}Rdo~!CsaW|6&Y>DTDZuJGC3Chi!D9XwX$d^R*5j{X)p8wLkf83{5Q_7KHqMCtTv15K$JfY+fuZh%7YBc@$_h1o_iu^7_@mm -nq{BYC3bHGekn^FIvx$Z))yNQ+kQ>7i2sK2b_bOA0F~E4Yu!P0Yvm=aV(p;!J+!}f$(^;(xd+eXq;3n -*tDfpX%;vo(#hh3n?l4M#(+~Z4gHgx^2i_+7MX;RAXxgWu^o@MpHt3x;9jhtmx2^2gt!oZkm%zHX_pehQ^I}^-)}r6&IuM(M|aXQS|-2tlqQq!4{Ku_PVenIp!(5-#O3W3_BNwpuUE(-cKx7;{n -Swu5Vu~7E&kX^!Pk~Yuny?|2WXBxhdU;zvvyEt5Vn0PeT(RfoCtmpCFuJwz1vwei{lf0{u#b68&0G5A -Ea`&Ql1*3Zy(LiN}7x7uK0KxJ)iM@yWSl`xBoizan;AZrb|~f+-=RiGr^Q-9vF|CW{aWxjZM0pAWS~0r^*uQ!gLxayntEkT&vIGch&aZ;>shg%Iq#8hMyucW@_u%xPw2S -0-Os;9vdw$e*oGln?0q_7xUJ;=yuI?|m9Jz>&UOvFtaJMQ1;d4RY9f$cueF$uZMy(6h1(%E%Y%w_n$@6M2%dnwOi^?q#4XW}d{8V -4$nZhz9^PTThym6|0~cCN>dS#su6;+K-3duW@n7JzxybpiS{TAQ?<_mC|63jGi4pXc67=iup7UXm`_+ -4aP-VQO;+Y@RpLKHB-jtYO%JZI(nCf4?Bs4(_o%^sYgY;s^Fe!sf~eAJ&tU4t=mVFO9}$NlQ1i6cbG< -18l0o%?}N;{%pkXY+ja%-}<16%a3zeE-=)o{A4P>i}+^>l|PV?(qd0kyo(~)PKoAD{y(^lN)2$EU>k~ -frZ}G>o+)J2aM*W6+&@k`+lW0B~j%VUH#_t+(<2YZoxHwH!%y%9G`WW;;h@GH5E -A&6xP5fjtt&tNv7D`e#ut@c>2bvQnnN4UX;t3n6ohG6ma6OA^9~rJpk8R=lO~e9Lf5#HPm_cKO7#gq% -+(mSHh3hd!%e$CwrIEpNZGXUA3&o30=6KP`9Gi2Uj^zT&miXUJV730o-~V@W4*VE&Kt06QHe%i#pt|h -pyvqxichf`XT^PA_1m8o&TAHH1b8SF5kR8%wfB$i=*d__ArLpELVmVY&|LJ^@Tb{`GWJ|PK_Jn}44}P -M>tPuJRJ`d*?xed(0<#qUDWJ(Te(R{r-9RABDJ&(hE+EO^pSstf&9dW{c8GFqv+CRz@C@sOCb_3FqC@ -smKmW4DcrCI%HUqjjyN}J+Ovmq^m(lY#MR-|1^Y1jJG5|NfmX}SKiiAcMd(r)&rnUMBPO8cfiEedIKC -~b~EO+wo3lyPbx -27lW7NSjG%GyQ43VcRoWPq;2lYRo*XY{dQpTZZ#%9nCm{0ef{nm9dQbTM3(y@c5Jg{flHB{Z!5;z;A; -d>ep!4;fCBImhVy3(71h^g2=2-G$zt;`6(O-e>XoK%uws#AsZC2UQzwocZTRutRjl?=Ui@)(D&@`h -*?`=Om_Q&UE@Gx$%&JkKLp!hR7bR5o-Xu<|GgLYBka&6`tH^AH#)0J_2IQ&)vf}@%WmwZadj>uIyjzxp3ChYsB$6nxNWbtaLkisb3_a -v%%QVXF3!N>zS!*AMdb)T0kcbMXF7jov9G)=aEXcn@RE+fpFo$Y`WH{KtLJySsvjsh%VR4p+S5*TYc| -W}bJ(F|8qXs2j(34B^xn*3Uj&m%#LsHpa=Avh?;A1dC1RDalKE5~`Ze4 -Jm3b?hR4dYpaHrKwy+wpXf`Nb# -Ht%(|8-TaR=kxe@6r?R!W@EqTg6h`8IXJ}DQ@WL%b(!B2_1T-xG8VW?}uo7>D{G>_3&8mvlrB7uizP# -^IP@zuj#X5jlfz%xej1*J*{||S9j=_c(xvRS@NM4eE*5~{&T!b?>vXkjf!_BW1G>|WTM?;+ajFHi4VP -tI{+E<6F$}Ta&UW|8G=2d;{6eQI@UZe+yx%EX3=oHnuq3S&T4vwI6j-#a4hMCKf5BZY*!8mUJ|;;Z;` -kzi`%wFoknY)8UaxMj-%%ER3YY+))MD0HiHB?oap(4MlPVBYp6?O6C_?d -XdpKImT)3&&X#3&&X#3&&X#3&&X#3;%~}V&PX<6AQ;%6Tc^!G*s?s|4irxaJS}Au^n)?2K~||k_Sp9w -j2InrDw&xgalldIkM9vFgaa#w)AXxOpt%?AB|^%d!}AmW8l{o27Q}(Nb@a-=RB8j--2Em`{h0#$4SAt -f#fbX?!X-?jtum4(3Nw4A)mnf(DWl({o~CS%Q4VTu)&Ph)xy|5!hyaixSfPR~)F8CH!C9;G -{$#5`j#=&M_ahAstkB()#kIE@SOx$;!HtfrQo_2?EvB5boU24Y}>&XV$dH8l^&smbMQJ#j`(iz3dfH9 -m?yL?z?4JAdYEUul~LDyY6ZjZD9_DK3G0t?-Czr^DnK!+vu=c)L$iLNNzwa9$mc}GgmA4zAFuF!tO%b -|9m9Inp@sY~1IpkwmHSsD!_LmRHNcA?F%sT`y!pE^vcsaAEy#vz=+d-4X!@e@2- -8-NiFIVR4m1KLF=q=(Z^r^8Fa9#wSl?hl)g3iVI>kl-~pD?M>C>amh)pkIJ(aOYm4WE^i-$?H*pm&g8 -kGOa=MkdL0aJu@Qm=j}TCWOvU&`>^eEB}iUFaKELl)k{v#cDlQ@S%SqD}9Enx$jqrO2aX?X4gM -rIQf$wVXVSZ>nU;otjrQa;*S$MM;Kk8RG)%%68*>su1jGq>3Gg90+Z_xMq?_0(@2l4tE)cUsG%+L3z4 -}7st+^4%NLG@YG`pouvsxL~buh0;LJrvBxLh9S6*4OW^5B3Ou{%7iw#;b4i{ga%2%ZRQ_(DoK5>g}X@ -e{)sn;B$iNO&nPlsACJ&9j(=!ZWz&d1=jy%{+X~t74*I(c>Zn`ZBgXWPR1kqu1BnSO4FyFSEA3w*J3ZUt#7$gXrV=kukVc%m?aUhB^<^A7AQiwYy0cSO|p(7AO8cOEC!{{37sjxU?1fw6xv4@FFlcbgHzZ$EHOjs@ebF|U`+jWPcv?p;FXZos(FTsQc2qrfbS -jD!vRnZD>&tEM+Yx{cB`T82AEX?=tr4>0I(XMPWr{}}Ka5QpRejCm3t^G+IT(4NZ8#@QPBmwhC=;S)Z -cpf79@mSwO*6?8^{T)TqGd3U57v3{{HV%<_1hbl5~#vC?GLDHqADwwjv@v3qvSW%;cctmb;?(Sf;Zq&@+=EP?S@9<5dp(+hDwT5n)H-bm{=Mchuu-8?>fO`EV -Iu)j=gyGY}oPI+G$8rds%GZp)PtT=n^+}Don&>QA@?eY3X$R?SQ@By$?gT7f@|7GO&d*Fu$NPaKw_uo -TD(RW~z%qDvC`y1+03grVv3FM)FobokYfZi6dpCpbwn4PI3S)As;L0NzBZW{ZeQS3}Pt?wgx)`5O{pk -K>qDA7+lY;{O1cTbe!-9vm~Zxr)_UO1mvsE?@=75A&j%pDK?Lp-%-B6IWS>RqKFb}qNiG{zem`F&LUT -yGJ5cTHek@ORi9EsA8`1=PpO)CaW7BXgWk-u8E??P!~3SNoro2j%15!n9h)h9n`7r -{h`k>R(OVfR$TFo^izK&n))gFxAhBO+201yKjuD$`yRDDJ2~b}eM$OX1Mly}yoH``uUW(`YM^|}==aZ -Pjy^~KxGU<~R><#x7;w3cz?N;C -{-yR`S%r*xL5@3s$S4R@Foj$`o;oS&(EbpbXDqw)Ct_dC!V@96X2y}$;9$-}ex+S6iasP{TNZJZbyie -yc|`u~rB^6u#q_JhH3wnO8MN4RzoeW0Uww$DHBi~58wZ)iLk+-0tinS0AsU&r_1pR_!AX(91+j1BHKK -Ovb+;El-k<0RL>_7-|u#4GrAzC0ccWig9|L2U*r|U!rrso50%Ep-k3K)_YoZC8 -y+XC^evjW1hPRJGjM;(JoPIne3-G8>7x1V8>3Bh4ve*SZJM#wg~~!(ZTwR3@5~1A9eZV_RLH->4!v2$_M0V{?WeFt%VAruH`* -d&I2O{+ReUnEx6U|mX<8gBgCDIY_Up6XjZ)l3)>Z=hSn#Gv6Ie=Hl$-#c2j~HfY)1*cBTlE0?cv|kW9 -WS&(cZ+eVcQGbaKzy>AeKNPt4TMpGVt#9XqNpT6S1cR9$2!y_ZYt~tSLWO_{3S=CM(tr?7~3{>|@aX+ -G1D@d}wXbq-xl2qJO}*nKPm@Nw%-(nnka=)vVTTZ+Pv;@xzthYpaPCThp2i&R7d@be{_8-s@bn55#Agv{cYgEbvK8=^ -YgAZ+@ISl@Ep6Il7e|F=8vZWYh!`_Bt(0YAk7G^P#{8Js{6SCGnW6#vxN%T;n*{N6evT*p6hYFj%U=tQgT~qewA}y%00Y)dYW4@|J+fs&X|$_d- -DQmBHzCXY7_aqE|4NPjS3`v4dw!!A$SbI`P2u<*q -7W}q+`G4{@?4f`1HN|-^@Zl@-Zdg#+1~vkUfEt*;92dJW$x$oUA4RVRp`4EHM5fHt&CILMbgBXpu1g~GP5cUZ7I(M9*RLqnF% -bE&8&jIXON=#JDXXi`a6=xPT$PZ)ZY>Oc{qH~aC7d4PJsi7dFPkM{c)Wu19&R<=}62e;;G=FhQ-%CQK0PjnRJOU*l%`eMiL_SFZTi-uU|MJ(_$ETr?hM#7pPUK8~jpY46uX*gdZ?=QSjg4$|lO0ve}j -O8XGW)C&v>p!N@ -bOL6bPKIZqy@irbM|~dDsKi(f+Q@70eUN7j -3Qt?(fs7{K~D86Z?Fos;|GD`rkqQzF@!I?}e*Z+w<{$sBDqN{WbmN^ -nUKkDWY=PNS@jPd_ujzDD^Dq%ukf8GqIOozw@VEGZXydJbkYxIR){4voDBt{FvI|P}{LbW;4;2;Uz3T -aLwR3@RjnYE$iu>Vos;(;raY|OiRJXEPs}y&qdjjef$grEck5v-v5!X`+6964NIUSQ-F1x&*$Y@iO)- -crD?YJ({F8l{{9O0y>+9k72H2wZ--r=ewDI`68>-F?(-Fc#GVd(-paIA -?Jm}p*BNX7+@^DN({GgLwLSth0Af;K28bY5uoO7K5~ZCe+~H3lxj(Ed;I#imGYK1xCJA#jfGdx9lkZf -o-tb5xt3;0KtSp>xOecQIb$wu$Bo+5{TkOXEv>dh%HBU|iD@H6Qq{F?EC<;JDMCeT;a{apEr@sTeoFN -i$!8d^>hrm7kucJu@U8pf>Nr#RD|TEcc-c@PUK8ZS=?V7Je8dF?=r^y{4Z0yYTs5`}fiD13S(Oyo4`{ -9|+lPS-|`7H$>b(hZ@5^Fm7Pr{_;PG8@P+s2=@1T0_scndvLtK@$7Q?Uj{~sd$#D6D<~atSvYS=_tTp1O;hRoa$It*dR^d%_@V`d4`k0o*tO -_VT}IgW=u(CTuVZf6MzXuw4&bussU4o*Mzv4>V{6HiBx}G2d68tQ{t04dWB(&retK=ia4hVXsGSx2W; -70DOlYxtI}sBy8-8qiN#-vmnbea?`??%2s`aYzKwO{XLPDkY+_x~$OWO*<6d^xc92Db7HTJykRI)V2RJM-pCu_o^SC?!=p4rJw3!@ry8 -g1X^PY>-TigV#=gkDPOnm*sz(%0ivK?YefzY)9$b0=Y0R-eHyNl-}dVfuD>u&Yb6D5%aM;k(D97H}FL -(WhEc|Njfl)*M*qX`!a{BCyMir@9l_oJCy~w_1^e(qCD(rb47W5YI$__D_gcdw-IL=+;b!5_0YA(mPd -%+Iix8GC~GZz2Pv(J#$*n#qSnez1JS&~R$tAk+Gid*x)`Za^!yX+J;S0e5dZ27+Nxv&pQi -h)fVctVtO_(pNZ%x%dK%Ley8+x<8!eFm28nMHD>8Q6UKVh`=lWm2N)+yq+ZG@jcHtgLQ2?Tx^$rM2go -#cH*(1IvYc$90ivxoLFnL|G1e!nT;k31ueIJULJ60(60$%Ql*$p5NZyK6sttR*3(?C%si-XP)I}T``9 -aV18Ae&3g;+5%y&@4$)kyqIq=%0u2nBUVSVK}XNrIl{lPAjDQJTJhq}hj@|m&% -4LBRSV~z{5yK*#?1Pj1G|O&9(XITLLF*cRv*gBWuoD*|Z?B6BArx)~r{e|Zl -C2N3aq?ZgPPc+Vl>C;zj?d?~TiMhxen!KBdtOG<#5`*;D@un&lF`fW@f -M>d+`!DYb}65xc$^dVT -r6X{P*l(z`PPXs6LF9L*k6>E+RJn^PWV6|vLviGCT;Tt<5*h5CDm`uD1YyI->%XRSe9s?(#hx{!ZP2t -A?GP9=H<((*O<2ur4J9no-vC%PI+KAM{9L_9%`(M5Zy_p~Zo%R;a7E@(-4q{8e*z3*t(HrZ0F(Y;T-W -9IZGT3d*pw91LqYpJeEIk|X%XpZ(BgZ(u>zlE%QmDZs9ohrS%JHFf=HT*q{|9rlSc%^!OY6mV2^c1GQ -3VVO^U;XmX^)v@|MR56RhvrL>q54uJ(^##8ECHE^#w-jntAJavW_XaakTFO4|%y(SX#dB=CNLEusG{5Ug^z5R3*d -S--`gNd5_Uyl+uSxbVyuPL_dX|l{xIa{XXTDuOoV%H10>p#`UUEBd!?fpAhdo;42fh^2_?&8pa;7DUJ -)2|bM6v9$NFH~grvBiZCtf63pmt79Bm6^!k0;5QshFFShAzUdO~=a4U#qOd88XYnHt)?yt4^oAdl)gU -O|%D-{Jz)DYM`UoLgSG}d4Lsd%MrR!%LI}Qu~+|9&hk;2nC~aT&3BrYf@TVjLp+nu2VlKJc1f~Nrt>K -LThKqybUxAJfIOUgj@B1qb@SLqMrubik9Rrp-iE8fuQV|2JxYrVNz3E*u*{2lA?R?FM2?>Z-1#2xoIu -`syjR1aSMBT=*EIz9XBKlK_9Fa#2WgIF8KARc-uYA(*0d*DaaZVA4y|!_J@D`>tOh!vnE~^9+J~?U72 -p34eDdh`gTQefnG=ZN{&lW{v{ecnZU*IXsCuD9b?)B9@1LY%?f#qQx^vI2@p(x&T&w$DgAP_;E_Fe-F -@g`VY-O4hKN638JUoct_-Z43iQ`giHIghnk(~i15qRc+s@HF)&#VTA6|qk%Wme12lH=OtX?Hx5>nx9X -!FW7i*q-Lweiq3(YH4hn>Hjj74;og`I6_|LI^Zbr8}c=eon<5%(>W+Z*W8TQW_;dIo0l6^`JdK -FxsGEEk(`@a-42~|6gxAY){3!ES*T|%CA*{8A$`^Y`=FWY@M=9#^(Dr;l4h;HfvuD|)&Q$@&_1=U(aW@@YTLK};3gLl=}!T8jNyqTj>#{V$12A -^+HcTa_a4J_>G~xD@LI^vnbn#}!-60MU2>@=O*OcLkZIrMQ3dT-lpolcZ|IMy|s1DfJ~MbVgUEg(Vmk -Pnx-|GA%Ftsi@K)N3uhY_g4x&z`jN7rcOFbI8e^#i}HxaB_yi0I5@W=UJ7Wz%i5>fP`pfhfS%*7-oW -!ja6YW%!Tp1lM^W$hzlzz0Uy0h8Nokx$e_=cZjU$T3BZ{Z^#uT|}QZQ2qEmfj*B)6RL5K13bY+lpney7r(>7y)i4p89dfP@-@*8@%L_6S+&Qb7vFQ}JK6-iy -Myta-_<8BDCY7PwY4n1(WYoyjH9On4I`-SXd`0Q?5DQPj}Ub2qPCGN;6<6SROUx?1~|-dWfZS#+L7j -$s0~MBh?ePxo9~qLE*;{~t8Fl;ZHSd(xzEihdS|XWMxPm8YaXmqe&6*+UCU8A&lsp(xWD(TE&qL&q;E -7#)3tm?&pmaBZKEFop4AJhmEPY`zN%GmEhq7;!Qg^z5c*xo42=a+?9f}p7Z=dED2vue9PO8wGZUy!*U -|aMqs|vQ^{Z$Gm7&wyO*9`oQlu${&!b27oZ>pX*ypmDm!XJCl$61wC}{T1luZccJ-|lu{@D4*1$Gjq~B+w=`29|(_f>Q7xP --Z<18UQd?s_J!`io<$4Yg-N@Z4FTHXts-@3Py&vB>knXb6oSZ6M5wd6Bja~>srhj?rSh-Vc^Yx6A;&x -)@lO79JcXN8#CkfkfG`r}zuNVb{pMF~3z-2KD8fMc%4sy2-AbT$yaHXG!{)12bWm+HFlF!9bcEC+Tb# -Ot~JxLi2R30Z)DPoL&IuYH&N@1@gwN)%v14(NW|Jd4%|`gjL?|0IWXK^99Wlk}!Hh-MQz|IZXX+mZ0d)$(31?Ni=ehOF-roelEJDVY+ -t;Ac-M~ix(hn`O53>7nXYtO`*C5c4qSyQ0LaKjErFK;OsZj5$&HbuLyH?eH!!cWlild~ -je`+Ze=9P|&_w@||ZBpMb%7C1O@;X3+{T@_h+9nA}jca#)eOm+5TroY}CNpp9A%BTPa>wOh_pu0Xn=Y -fxO)9eP~gGGt+i`PkpVk7awMZg%-t^01fWHJ@JDb>U@uy}nG%`xI<*gp_|E|;DieM_pL^;q^hD)Wsfw -y>4>GB8~nw5|<2mZPb94e-#Y-tSPI8Ww1Qn(m=={e6Nb!iGXGMHmlAkw)XR*R7f9|7~4Be0PZQRNZ&| -9_%$JA9Hi4SaHL~(tD@k#(O7eZK7`l_hs%87_PC^uuI4D3q)t^?fm>o-+vBYn^f67Y?Iv|K(>C1==Rq -CPq%P%dztcTblU}6&kt$c(!BF5n-)A@zh`iJmKfY`7{q)%NzXr{au3j4ZByrKzdm@rt|QuEUFnH_pl2 -uz4Me*F$~$;ntMzV`X6vXAT6+*f;Q-OD0I~}1Wf3DXfNl#^`k*};JwcNSm7eH-QLV!)(z5ozj}v8SbU -LBZ33TXsn&eTE36`&pEM2pDweU;dHNhE6GF?RiOW^pF>0MUXgAP*qMcVV`-AWH{n$ -veKUTmv^BvpF?YGj=ShCmh!Xo;n{7g+ghx&H{VyMD|Js2{*hg@)(TokE^T<;ikRT=BhHdCoU;u2Mvcb -HjY|HCI3<70h1I47Xuge}O9n%uVz4y6{gSU~U}&xzv~mE8^c45A%}M4uEZO -}VoPX$9y@i!MPI97w?_pTide=!Bqqh61_vlx}v=|Gs8eR8I-vi9zIw`q&KD9p`b2owH9O|Eq);RQ5gY -?dPT0=-nro0&UA5$IZzg8#eps}*}qF7BEwdtbHU`IZ4ozj~@b3~Tuj6`z-a|Gui%#UBZ1lt~F(P&&l{ -oe7eAKSk=fX=hLK8{7x7t6}ti}m-(-yids{dogtYWnSaHPY>&xrOgTR6qK)2Po#&RW_{wgSMLm5nqMoe*^|bMPk?ukIU3A~0dr;~~EsL ->Y^_b=>!OgMRVXoFtPjG}78OMzL(15wZ5)lLP0q@hi>iEXhBwx9NiCHTDM&_47zZK)J*xzAe@FIC#0S -yxjF8*Vd9k?{S{j0xydp{E6@L!m6-m|oQDQwFfgD1-;sTXFGlW!}xsS4?m0x;}<@#f`)(C -XC`07gSzxr^+jUw~czj@roh(T}iwG=UvdMik{)@c|qkD6I6bYruXxU2I3bO!~LrRc!f58R9_?;)- -ksUC8r+R7pXwG1sKz1S-lJg4K3SE@-#mS%ga`Yp7;$Bm6w6VDHm`~Vf1n`OVI>9Hl^!v)5>-PooiLWr -9=S%+ioFRpu&lj%xlF`<4^eN`Eh;{3u^>~ct2GSa+pXmQ~#y0ma(L4L5O3qs^`s5Cy -TG3wxL}fo8leBoo*y^cd+5vI2%HTuwi4X)hHWHo~b-0<)MN?`w;XESvNVFv!zsizn?>&TWGF)Ml^fb# -8SSa59YIP8ri+!`D~oOkMh`jM&q;NjYDC;IDB}?*MzpjO7$f}I%fZ=Y%V@ZYxvXTR_mwK--=Z)xA^G1 -5JPpZU!^N8qB&=Xy~9);X(+DR$Ks2wVdh3khl~mR@~5oT3K_Fv!HhiEYV9StvY*ykn{4hzdA4cnOcsq -}<~~QWSu%5)^k4Lqd2~uX^v2*HXQNxKp0$dIxwM2i-4P=GgKzEiPUB1C-&=*VS#Rj@HL-)uw?HrR<{N -Lk5gy-+-}~~|b6Wgm*g+sR+qZlDx@vu|=KmC|GY4*ReM0G_?eIZS<1JtJ0@0TEs1_6HGOhccQQ7%wd6 -C?I$2_!#=#$+m^y8@GRFBYye;ROaa$6+ZTo@S`KRSGzVBosrZug>y#fWwCcG8C%mcf2ZXBkN1FiCvcvx`+)m`co9n${>&nttTiZ}EMmms{zE) -V3XV0qlsV~a=0*E>99b$0F>(<%HWcS=JU!l`7hwlF0r+g(AKgoH=Mv4AeblDT9>2Z7lRZ42^LsCjKX) -Iy>g%|V{tNW%40M~B5$q6Xf0@=mN@Y^Z`Kfy6`BSVNdf3^J33b+PgW*Usu*Vmrvol4+M{PRm(7+^C)+ -<|SZKVvI(^-LCUM76KCk5h0a9jiUODx~Nxw%(1^ZdZeAEGfH)TOvufx$!X{#Z`w#@z?;p+oB`2lE`2_ -PqIt40f5jQ*=q)sMGNU-c!6g;5T!nu-CjQsYR=EIkm-KuXUte*yNp{x`3~~NN3@7RY~SbHM!UvPurIWQjCAxqOfIdAUCyv=CU=M6g*wl=9c98hcWs|~jVSy -)lk?QtQ-Sc_es$fhFQGYV8zw@du8@L&~&HNoJkKH-Cy&``f5{qa$m -6sgVE6mvRObA{(`*>)f%Yvg=!dqFyj6CIXqHcHMjGB(8Su`ofOk+PVnPp7{nx{no8AX4c4x3dL-(+D% -75rG?Ge+D`Cj>htaw+_{GRI5aJ2=`B7VPn3;h=gpYsy*7x&>Lvvw?@blh#@j-KQv{{0V!{Cr`>A^%>2 -^-pbb4bVO`NPOfD#=7ew*iPKtIx2_qDrj8Vh#vz-avhBi_wm=#nMkjsIv3#1hMq5=y6doZF8g+)oUPn -{PinN$T4KiflH>4Oq4qp?$S-#;r0)uS|69PmbL|TPgA(Ty*s&N^DVqu!*qJ%RYanYB(44U~2wU+irsP -*5?iS^n0sZ%txxJYNmQY#wNS-Ub%UUEE>{*kQyo$=}^RkE+8`S*MxlGEO$eTs)R8ihcdIxcjaQ|744 -uj%`Ed=d5hcK?y^ojY=Dw~FI-&Y~Qem924O_@*Ar$RP$8zg08m2Bu%DBrktFe7n=#=-w|iTFX6=5l{* -`#e6+Sq=JiS@@pW`Mr@Y%6?H7Z9h4(k0-kRrb8T7bG4byMdmG=tdCPa95jcMV-*_WIVuP-G{`rzG!w22z684_wU@wFC2(qyEJM_Nr_dB -fOgTk+Evx*N28vSAT?Uq&FaJw!E`9Emld -9h!Is7(S)4?)Ml0EeO57hTpM6(>k6>3`@OoK1$e~sGzvvY&-QJwFPkB{1`V;#Y5OANq9^|x;)wQq|mt -8srO$_E)#i}ig$$8t2<6n&!Fup&Ns7;Mycj&NM^62wY&ggmdO=UqpHKiMLhmyqkOb&W66fWDw9@Eii` -!}%Nabx?U9hx+NK`msYW|GnG?in$Qyc9`b;MLy>vXzoY4&e7aIOLPDCZeJ6f8Qh=JJo$v=Q=*mYBZ1} -Q7IB?Z>^wI2cIKRlIC5je!WFXWYhmL`2lD;CkA!Ur)=gisAKxdOT+T7GA_P87^1e8Z7qWaZFhS -_FHlx4{%v~j!->3DfKlN0vRQdQY;#H+dz`z*JJxpg&%!^(+8^d3G25gp9UuXm6_hbug@{P9z?ze&4OETELyu%{rc@6MeD%5FM=Or4h* -Lb*1;KiG%e8@CCB+moiWfT8xam_r!Z51tbYuL^r9go|4TA!|SK1TI*M$O~)tHc9b+F6RnJ(JmP-7SwV-5Z^M@lw*D5^oA3IX;Qs>u-7 -gLZjH0f0dHjhI+(GsFd@qZ*&!Rn%@2LLNGO&3P?WG>B=Q^#IbR%}LX;*zYwE^`R)VRQb{)7*|E&K`pO ->K*)BbnE6;%&{JF#LIPB*wy2Ry$2*n@P^iK2|E?`(batjnd%H*f&+F#d_%TwFbpG(Z<-*CidP6Zugtz -tYhp<)kH@#?~h@Ivw(izK)>;sNn`mf_4|a0t<4Y9Sf&lT3CWGYsiSUV@DfthN)r!i@GbN!VysVm=n$R+B-W`iazT9mc+)B -$I7HJl8CTfgKuv3|L#S2eAW>Q@Me3S|kCx_St4`lS$t_Zw1?Af^V;F^UH;yc*uP^$zHfJCV^yNI5Tt>8T4`7*&O)qRLCM`HT2^H$s&FaFfxz^+fFTpPoO>{v>ud+vJd+>|IG1_tcu -^FeCExJXET9q@;;uE95^V6KKn7@g3FI%5B<_`^;Az+)*Tyhb^}Jh(Km&yQJ*f(wOMAC&tivPo(5Y`;s -H}pme1Eygnfnhfb1?zJV-@Au~1Ya1S1VYAZvC8kwVYK#BXcHhGf*vjc2xcPdyujeSqgV2-fpXlqEXkhn -KyIk>9{!gRpOK!UPhf{8lANr-N-gR`n`Gek>dUn4_vd)a3_{fy5-H-OJiemRaBI!-5>2s6x;FJS(^*t -V}v8c1@&m4tSUMPM0PWS6-R05cZmRlqXh_5FdSqzVIgJ3ggl#3AK-Y!GAa8J1sY_nDQ@?RMy=KZF -kXU4gEiFT{dMoeWn(d*=AF}F3h>hmPqgP-hPkmLIk^ix$*HSO%L5?yJ%lA#k%Nj+r8-Pl7F?8ps&Rb* -fhLdjP>28<2mN;nqirp_Wf&DcKxjdpOoE2f4d8EepZt?yS?q3c)qgxuLZ3O&qPn1|6sx4*Z=VR^Utp< -;NT(b9i8~BFZjm;PxJJ`Z+&pf9LjgkU7{R%*f-~fPYQ0W%{i5S=?C9r5h)yNTz}gw?;86yl}wO~3p}g -hN~5Ku1W%-KDQuu^(!X$D2%A(cu=1oI))>m`OFlENvx0xbr3pLaI7Q53+*A3al%)n*=Yvvm!hmj_6}A -|9=)_U3&S3=)dYbatCB6NGlt^h$b9*n?yw0bzlahtf8m;h^MxFuauvZz0?;7j|DK(*rK8vMjd$|;6NB -c9W{h#Ud_M?(*se$S_C`}ou7dCa>(lsOX-l^96rZkz-e#+}upw`j#f7yE%xTuQ1e|*jYvLGhj@yg;AQ -53IHDlS(60r8Gz!2%1c!m{oz3Z}K%jkFZYip&%*^(8MwrG;gRw?flID-QWyzV|(!na|98=5pqonKNgR$|a5?5^1Y#;}ay>qq}>@wrY1jw2M31MU8gxnd{|OcH%< -2r&+)1p)O-<{ylul*f$s9p5a6*;@|cj(xYu_(!H%G>DIOd>Drdso~|(sbSEhTB=u4?+(yray1tHp%`| -0DXNy;ZO*YeXwj_&zFI!FL6k6JBCmm{W^R11oszp?uT>eH|lSKZXfb?Il=MtImdT -SZyVn_?LW%>s@Rx)KIo}~G}3Umf7E7vkKJ)Ie+PC;^>0XVunoG;rlmF3ti}c -ZS)PW87y}|A2k;jR5_@0t_q5EWSXiu?H_o4ozlX<*U+$Bfq0lzz%XVdQ(N&fJ=1G*FV-5%Xr!S6QAUG -478_HDaUe?zZXb3FXmUQ2T%KeqqN9H|q8RhwZooMNoQ{t0TcoeU(6e&7+rw{y?rq?AE9PG9;fHp+y+7i -1^G3UQqusoQQk$LiP@6A-t*E{R$;(>{=~6>~0QqD6!7``sar?We-R?BcNAK6(ZA;qHd$=!&-^lGmJCe -SOvZ%u_vDc_a4%)<{%t*(yBge6MivmwNCN%DN+#>kb_Z~}FB -ehSPCUchy0)dy@5J*0=lR2%4}a+T9IzLRar=>*G|dNJ3iP%i!x|nVJ1_JiE9+ql$iK-;-^r!# -wn!`c!x;9Vef+rx{Wf-cIIr&4=Fv1XZdB#b_m!`8B|Dp-FS+VZZidrwYX(`Fx{rO!cLaThw=Z?`*WRR -dqr4~O^gUH9R!-`#_HK%Y -VQ)AwV*_VgP{biZ)=9Emp(*8s|aX)y(}`cj4}j?u5Bdk= -=WsyDxL!-c7i7M|VHw-d=X^#@u0CfBi=~4{|g1^K$x~&UXES;93;_1B@32Li;O*c3SSm{r$xJ#eH5){ -k3-X*PZ#R_Hx!2&-Jx*_SfSM`o?p8p3eUK@1QS%>vQWL1bxvks-qU_NEzAz?tOWlz?NlbGw#+M-F%(g -dZ3%1lbb)fX`I{$bE9eShVdTVKR6ZoU;17_D)hf}&%spaf9X5QER4k7;~Glm*@E!CC7!Oo16d%*Vm-* -PK)A=G_tvuif*x(*o@?h7;Kh`8@MAU@5ZsmTRRuEUHvrQUK)|Nj>p8%u(e-$FUE3N0xSvLq_M_xBX+(f{&n0x=(;(iKe;)RuV -<-IujhN!(!B|K@@G`+yIS;G$#i4%)C2Bj;cmPv% -yjQbx(1K!_vl5~zKqb`*#5=LzsKx%ytxfdP#)+p`(1i3O2?>i?0sGOEE)X$mc#VE2;;5fiQ11UROBQ2 -4#33z+M92ypzplHw=SU{uUY{o+{;(3gmXEZC2-37@4EB7J=JQy&vXO)K6;qXkx|`;;dda;mpMQ06xUn -2KTHsg4O#z$*VcQW-du7E2nv?&S=xW0_H#8oC9kDhU@Ykc{Wtvvju(WZ_sHV@zS- -Fw#`F^6g?-EY2+e>UJy6-Z3jy6Kfc@~44PNn{o%D(2`ZfEnMw9M4KwL338ngex>v~_+4?YtkDnrMsOh -+D?z8rVDvN!ob!GZlDyz|fBIckRp`BeP)2F3VwcUQ@BB6{dagv$-i-C?KAC>cw4Cd@XNb4vp8l -wdj(=hyJiV{7wTfG2fN;6tWMzrR-qAGPkozl)YVZs95!)eLQXu_Jzc&V9ycfAmX8V_88U{cbW`kRCc+ -fHuPL59l*>E2z$rJL`NUFwoK_2NhNvYm;%ZD4OJ~NyHA^ZfYL-MZ871>@)(+FvzWxpAI&!O+}1Jd@?K=TZ;R4Dfh -7+>Z>Tyr1|@v6{~&i;zgUp4U>|C3MbvPPf1qiNdPLLTNq+9$y|0KyK2u!(>PfOOw&+K04~$o9a1!>j3 -MsSbcW@ -%y$?+w2`wlf=Dj`>`5N4QM?_4$fhtIdt@o6a?ri1JIx(Zf6$Jr9M1@(&1Id)^|v1*D>b*)RhLI<@2mD8!zmBEb+ -(#*Q{rtHU-t(Y#UOCI}))iCK+&WwA{JetZoBGMHG!) -$cL`6wEs*mM@^btxaiE{;C3my4Ki89Zx4b}FqwgZ8-|iZ7x4eLS#NF}&ZJ-#x3o+ns{=lZOFcvTGq}{ -oN)(`qU0f~Q4AP2vNP4^q~k-sP46nf&kZZ#x3%E(vgG5oaRKLwp?ZMa -0dBA0VDYyn?72z_C4I5aMXW1jJd0vk?~}mLRT0tU%n2cp7nL2*-HD(TE{}59BxvF&A+O;u^$v5Ni;>L -;M}lJs9(Ys6iZrI2thq@gc-{h^r8{BJM^!jCcmI0r3W6+d&-rB8DQyB2Gh0M>HbZ5T8LTL##mDhj;|B -7O@`jPsH|vF`tOBh$)CR#Ags!BbFn+gLnY(B;xmoO^7Y9yt*KY;}(|lJy;IC2fE6OM-Z*##B&C_a&eR -n;_k^PuYQ+f3Jk_5rgf0+7_NK`>RZI+4cqUmUwQb(tC+tE#KJMI;Y^#kJ~eV1a!-*jg}fBGmq-_KZ{( -EYyAw|>RMkyk+(;ltdOT6Z;QMT -c{k*x$h#x2RLE-;@+O7cZyV>mkZX~55%D9}ATLJlkGvdtU*t84@Quh@3wyokYHxSs9>RWay873=>6*T -%H(mRmU&UX45uc@y#=p}& -IjuE=96T>Ik`9Xr> -3UsEc#4gKHwRd`ixvalg^T%%XPS0teL=bWp2$eSR4+isaZOU)tS%JXIS&i!Ugo^3X+T(U)MtvsZLP|5DG3w`wbZav8xq7RfjDg=_VK6=%O-KOz9s>>m -a6a%L_m^+dn{|+0`a3v`*Pj7UDT9OIB)oq1WwIbLnDF{Lnx3G8E*1&{90(ZLpO8lYAA)ls4FiD|!jB- -34}dcm&cSf@q2VCVXi(4>elLXcAvn|KWm$FUx%z&X{?k?d`zj -JVdmD{|ZOZ;~f%W|Il=Cn|M2D7yQERkujm~wR)`aHeSs>#!tL_YO7n!ibk(%&7ER*WlEfDxyrS|mET!0t^cB4WBnJDq~+%s4H{a9ECyOD(i}$OK9%WH${%>p -#YbA6&Zx`QXa1d0mHwJ7XXo-!Sk6V??e@QmZ(4tgjJiB%8U~}mYJi?&f!?gi0&gOpLawvw3k_E1^f~! -*uE%$c?zZ}Rci(riQ{Ow+Tc#)W6(r|RbG4A_mz?xV+q@%l0a`V~!--jZQ9n4A -)*iJs+-|=DobbdB#yj$I-R5$P88vrOQSOX -N`HUn{=diHAA!V33P>*e_2=AJkel1zA*b_FEuH+seOr~g6Ms>`p9St*9?KPcg@TtT_(}zTPQjm7@a-y -BzC*#qeTryj;=U}8SV#uRCuX9DzdR%j_Yc&zo@jG5q8Fk!%33Npy3zZ?ws=za0CXRLJREr>@?;D%3Ed|nPDlMSFz#G*& -m;Vv*hKifr5V#|!?Z3&T!QY8p`OPvJk(RxV%8#GkLlQi@%#(j-^X-)hVoM=pW)8$k^DTlEi&K_svDU~ -rjQu8V+zG`5_||5NfJmj@FWrkxt>6xfc@{Gx$Q4d{<~dv`(G^7xV!y5euqC-AB&y-q<{G59Da$@pXwj -}iLjb~xB36=hi&na#~y#;$)}2+Ub^g=XP2)iS^3=at6q5VrP7yIzq01lwd=~(Z`io$wb%bu{>J7lTer -RWR>j-zyt{qJd+%57{NTe~AMM^#wRhkC0|!6;r25d|Pmdh^>{!k56Q6(a<;hdEU!DH?%s1bjtvmPK`T -7grUu?Ma!{sYK{`7O>)n9(S_S^4&G+qDmuNyaS*@>ibbN6WBsrK@2>C@_-*1m1pwrk&^V<*4PUAlJb- -osxL(6d+XK7IQI_755`FnG}5kRd~d4IeS`-q8E*9|iXeW3{^Uj7)u2c8+28oZLKPzG<%6Vl9|Azp!Y* -1G64{C^fBl`NInrJ@V+kJOBUg`2W-P508k9ijEl<8y7!5Au(w}^2AA#r%atTJ!QsBZvW=x|0C@GKcWY -e9Do1xaOnXh$DisS{yBO;=Y0Q@{3`#{cgS;FrawgH?cqhUsah5`VCSoq4714%Rf>?@Jj<_AM60r)g2C?Cw>wHHOa`G|Gdm#EE`XOo%0}(?IwTQ`xX^4f0C5UB+ -m55b{)rd8SwTN|y4Tz11O^Dn -@Mv|CsBvT6XWV$|+Ovx!AF=hjqtg}*Crml!kIYWAYKMN(`*T{dNdrpB_PzWyzKu>`gSv<7A7=1cP)R{ -@R$qZOT;vxR{f?N__kWD7*O=Lobl_cfQBT@Pcf?*1D<{}!-%znU;NS{0cf(`?{e*ztP=C>f1$w60v1s -{llPOCmIU2n#8g8qEUAP#3|8lTL=6Tv=-+&wXIaAxM0-PptI{cr>cn-LLt>8}2!2IioRSuU4jO*p$9h~d2$Sz!;(Z!9(`ppVg5ud}2$4h;>`ilo< -Wd8Leouxy^?n%Sp4%Ljji239BjAu`6sJwt=Grf9tFFOJ5BWb>pARqInoxwdr)?dK17Rm9Lom|lVjAzj -Ih}ps8H;`O8odqoS#EIuRh3Dxh06SPQzcgJ|_&*Qg(-XN~uyp3jZalq8Un2dk?%{-2mSjYp?nE*eY?= -xG7dfRT6aNOnl|B*wG_p$({y}|$zxyGUJow*2DnppPM1qB}+4TrH3AJWW7zlYYH>LRmW;-Hq^;{2=SlO=bU3uHU?oDNM8 -dhi4PiCt{q6^u0n!3m2TEcU4^D{(#oZMr~dc89rr?*biP&K~oZRX3w?0Xir=OCa5E&Om!_a5#;U-gZey$;-2vUQSAXD__JcCh3l_%=0Ir*6`6{No -96h0T}n-TobCv$NAQv1V?)|i?#1J|+n7u|0=x9r(8YyZ{$8GPMe=8l`gJr10BD(kuH9Y*Cw@i4w$mFQ -Ouj7UEvMfY6Ysc*jt>!>2t3!ZzQp4PkDzy3U!omXu+leg`%nO)HKewNv9L&jmhO+WW}`1n!N&u8cOhy -CzsDxc`7H{RNRbhgi~tfBt*4RTXO&+OB5V(zr~efv*?0ex2tDpyCovw2~cfaTiHmj2rEvjdO!Yk7I${ -#AXp#Ze<@yL@wc%DN+abTiIu*)r=BpD2&tvODfNKIyIZ|LXtjinLXHaa@4U(hoarTK?9wH`a}7G4h@9 -57wogv3YO4)u{byd3lYk%bZ?N>Y1SlLtp%4`eR>4#MkS3zVX!<-_akLf4#8M+F?djtJk|<$^9zG_w(; -*?w{!KfNp}(5cIHLx9^hO2Mqsa@K61cGhUv1==F}5;+F>wQg_NX#&t1vrqQMse_gBTIVm}?_4~j6(r$XY_7P_mJ$T>Amp -jjB>K6JOIobZ*CwI<&ZGrCA`)z0IT7vc3bbmH`|1^#Ns{5CX^m;{YFnzRJKTmV{K;KthZOHCm^y={Hf -M15rZ8tG?_h%a}L{EB@t~T`juyEBMsw!*OXUKz%6$KxE{YLwpUwwDh+*+S7K5%}=y5IBmt&E&_?9dDS -rf&J}WWVpr*S!Ar7*UiW=P48ocp&w{q0k0)iKeDIXcNY$R((f6cKI2Tv`^8~a+f#P^SWYkKwcnDCD8CmpVtTi -UMvRLT9?3Ez#~ay@^3V&CuI@@CCw$kZ)fCsKS~|Fo}e+Eb5po4xniQw8%Hq6QutR~Ek3LwzW$#Rp{#) -yFPQt@`uaJ5kcl6_03oTl$!fFTP$`|E5O=Pi^|B1rr|J^x3fR?c=0?!5GS6&(yJpa0J^+j#Vjt82a^YLv={iRLr{ -yq`Qt{-}JWlPIj3+*0H1V>gF3I+rZe0=)rzCZid5bc6bE~HmB%r36=y-<3z`ry>3eW$)RqvngGI}Ly4WYl7s%=RV5Uyw;dXFJoxQPO+E7-TK#nB!?m4@J~PkzxNzOh3q?t8tLpJN$NEcO%*xG -QRR7AwuSQ*8pPxQ|;_zi3H_RCDZOXT!kEPgSdUjYiy?jQ&#lDlX=Xc6@|NG0S-Z#%1?rRt}qRgZGshv -Z9k8JH9@O9+O+82N8x@AM>u{m@;H1pP?-Gki^jj#Cew{xeH)~~R9?Nz_#rwQ4u)^&K}1W?sntI{4`O6Mp-1xwx!ER$FT#R7vCHQ`GZIV~?g -Xbc)zd305yBqU29vy335V-x1`FqFLRG-MMYbt*;d(@$A?@p`EZTae}FOa>l4~$v5JO0|puO=lnd_DJI -K)35&i=*Rb*KR(wFyWkf&1x$L^oxgtby^Vw3X`j6L*)8||5HbA4m)F`@S+er&19&ikQ7`70BfesMdn;oPLM`+NDGdc*QW)CXU+J?7^3l!bC;XWH%)?i9eioq3`hq;xVg2_7=a2l-fAHgrSx?_-e&%1leLi>0ys}G!+b_yWx -HfFi)yiL&w`$$auqwe)-nws^ww*NEyLj%AfZ*5e{rS-8rQ3&m7!jUu{ba7kvg%67EOrH!XQ!!;!&e-w}^Sy}G- -y-M;dqp$Uh!4O_eGgIinY4Gl;bH1ue@u%%8rY9Dd4%aNF-Z$iI6@sy{b({WXoTUU34y}4 -!Q{`{ZR-FD+^Yn%6J7k@RsY2cVs=jN6UzWAkA#zgZsoy*^fnHhBQvooroTi?Wd_H6HV{kEOx^?v6M-4 -6^n(&GKYm){(;a(?N@_B -D;&XbfW5ZhUFC*RC!V$}x|(+O -)Zvc@oHew#_o>R>Kel*2>#{MfbN<)QK3}lJ`q0SL#)CuNFBy3-BCvXS(VM-$WeeXk!}qQ({_^F4kzaM -WXxlJo%mk5ikT{V11Yo`3gD(rZR_`TDidE3DO1mfx8EvGvFo`zs -@i8DC95^1zM}mu|Jo)lEFmiYGs8YR^41fkvHq`qo)_HU- -M^r6(5S3>iqEZJFH}weO<`qWVykm*G_ax%p@Qg@7$ygJU1ygHdAFTY~R+pkRW?z~g-?((_h-SsD_Wmhj%%Wi#CExXfxgbv -&q;L7FO5AOe;l!!K>HL<-|3fIx!Jm`0F=m>pCFZu+Z;zargA8;~aTv8Nqde7$~e?B1Ap_L;S>uf5Ji? -uY-Sj$s~T&&+|KrY_fYD6y9Dm5V&>wicC=dF;ck&E{-e36UyJp7Q0alZz+c -yA#Pxp+?@1bI8;p~%I1G+N~1y@6Qd9g!y^_d}k7T)gLyhFrYYk%L^kH(^39-jgUqF5a`TA@6~_7`b?F -rUbbLc`5P$3f?Q -tLjyxWD4)XEHO~}P_$wK54klT=puB#aNG~^}7rz0;#J`;Hva`7a%9QmWjpJ~J27nQIBs6e?2c@=Uua;T$nO#1k+(*khTIpq33(eK$N1VJw -;^vQIQnld@{7C!@-pNdh5eBG3Hu@MEbNE8i?A2+uEJi(y9s+;#{NOr3waM=ALRbRKFBq~KF9-veUSGQ -`A6PM*avxUk$>cUME;TY75PWrPvjqYf02LWK_dUi2Z;P5A1LyVJQ%t93ic1k{g4ku9*BH6@=)X>kjEk -)i97}Qy~uNrhaxXTejoB;lMLr67Ir1>%mB_~;uSTv#UW+^&c?0rDd5BL -B!2i~J*(Z~&r3?vC7q?b#D~GRnP>ry=(i{*n72w;}gMUV^+G@-pP@kyjw^h`b7U7vwd_yCZKvJ`i~m@ -=?frf5!TST!TCkc?j}3$YYU9IG{>F?v6YMxhL{MdSuMz1%UMJFn1GGkw -9x-5R#PlHdMec=MgWMZ=2y!3fTI5}jCnFz)JPo;o13VLQcjPt^Kk^b0Kk_mWKk^C@Kk_OOKk^z8Kk^0 - -lPcpP9hiSWqPzhHRee#l254@55Eo4TRMy^zO>@W@kye;i=u2>-|ng@5G5!aq)*lnVdI%Y}dBmBRmU%) -juDyjJ)}-XQ$r0JTZTk*j}2`Dn~Pa&P2;LaxR93pw&w!Er#EB6uw3U+@IXzu-84EJp6W5c7|`3-WU0I --J8#XR8(nn=7S{HCXO`qI1{92s~`xQ2c<_AF(7jNIq09S`XC^+Ao6tyvr#XducwC* -^%{|zkekrI8QsmOF9+kX;GDjN%tf5X(26T4tmv-*(LgdW|9bQ<^26iF#qhbvvoM|WFn>9?wm}CL0E&6wqpLdo@|WAirk9npM&L)C)-1Z+=$_F(O*7`koTv>bmfOHYD)pP9?#39|jNSb4_D>6ypMYnpr?o5Ni}BlWCzd&!S%_aH%(WEZvE6zN~E -Cc&40>LS`2SU%{4DiSe}k_=E2y(QDQv)D7pUd_!H&w6!AOyL!(Q5)8qBbaeXqoq&FSQEBv$)aZP@H -3q9%gfodoklvPi}yUP;U~-Wh=-3*lvA>-pZkxO^>P1Goa(=Y)&EH@{u7+i!|gWR#ecM19^C(UC%KWuA -M4^jQohb}|KW=Ao+{@@hvncJK3O53s3^w>r~Fx%eWtnaSf_emVYMVqq2F~KENxd)(Efb9D4r7wyNKuV -;<y`THV&ih0 -38l#6;on -qe759MNBra>`}8Hn-*1NjbN59E7=J&=n{mUEDA681p8P1pnZSz!<4&kK7XuM_q_ -{-Ll3^3Q}lke?IwK>oGJKk{!y{#$VVmB>Hxk41dg9>qLrAj-wOTBBkfG!*4xJ#39)UNsiw;{A;##XNZ -m%Efg<%ySMB_C&e-1_bIC^Xi2tUxmCF`D@5ak-v$&9J#oTS0aB8c{TD~$V(LSthFdF7U@^a+cuzF%sY -yCeKGIfgmQ7667#rXURdqP;r&ql7V<#kVq75B4TyCGp(q#EO)*a`HVBMG`E2B6ih1}Hl< -P$NiggM(C>Qg#VqJn*w@`@k7mybt7xyz_-GX>bUW#&Yofh-pVjjI5<>HMTG0!dDiKs+*A#ySAd_VGPl -rKRp)=9|wKPu+YYtjGH$QzK0`vx(uE>?&)p|EcpjLBvHu5UPIt4$JXNmkFUoP}3)*XbRJP&y+ -^2d;idGs;JQ&9dSaxo7-3V9C7A4e|M6^K>%g(!bj=tnL#Xe>oufV>>JxPK7qP{cZwN|e7S?1fy6TWgW -Ogj}p!5$jqSQ2s1(u}(p(t7t;`Bf>t4buel#E*IlSvF=5zi}6Fb821JuFBSPg{*2I%d>!%>jVbv2!k7ouE@i^Vz#e2h+tQT_z-Qsn3*3F1@Hnk`hi|!kc?? -&E)d_QuvH|G_?p2#bOJ&}JZ?1}snk$=THqga&B68XdD_~((QpnRXmAM%fcy^z0-ybSrv$i=!OdH+|Gu -NL+}F7{s(>u81v`=ES*(63mB(}41&$i=!DvCgLnGE9gSG`qi)INi$#7E>#Y1x{t)s&$JqWt{jx7As6e6a2kmeqI|7LAM%Hhi*;6FT~;~Dal2Vki98> -9HFB};uNL`t$QzKCAa6px9=Y0w^LK*e -bM=LY$Hlk?efemS2b_rILy%H<(=kqggLq%U8-esDflA-CMdUG-Vz`o(#XQ+^k){L}w4oSwHhyXQJR-{ -H^i(`Eg9{v}`6Q8%qlJ&WPg=Z_cK~5j%v*mKu<9lv%~TU>ayQ~D{-k?RfTdGa`sbE8~-oSU5 -T$GKj?%?e)R!Y%T3m-7OJe4dj&D@(tF7r5ky&qEpH`1rhGmYiNb-;*WVCky)-y$g5LAHj26c$SlXvrG -NW!hS>8o6mzd(#Pj%9PN_NTRPs;=krg_ax=DDXPzU+&*wRF+ssk3;x8s6kE-pXYGok8 -^`uANYIov*mu9&!1(>_TuwhbL93hN73(yc^~1z-?N`1r=QP{&5`@9Tzn(gD)V`ai~7OmeH`tP&kyFw{ -SlwXa>UP{7Uwx}vqH|>n=8+g>t7zeO)ApE=Y@@O{o(UMj{3v7Q7%s-_8$)a7MK3Og!R=Z^IXcga*N -d1~EV4ET65A8=X=l<`W!zF022!a(trwxL#MC-JP$87QAko?&zPpyZWD}u+Kb&J?69e?chZ&BLl0#-d5 -}n96X0zUvgybdOfk?xWX)Rt4n$EdH(|0KVK(clFNtJj)L399nS8~`Q`Ka^W^mL`FBTp_; -T)A|2cAcIM0*o7v~nae^`L_m@o4Jm-z2=lvk}_k10x*tYesB{TIU!@3$D93V)a3iN^OB<~{cTL(hJ@8 -9s8VilS}O3;P%!ka&QHw;yEq^p;PUeCDJ>j34^-Fynu$ID&dUW9YZ@7{#jI8*7++R`zkm!~36L{JRE* -+Jx6XXYR{%UoiJ=UA|=a(Q$^P`pJ__e(Jtcj5qzj&^By!E#s@keZ}Oxer0I8vgS0?`$yE*j3-`ZSoOz ->GtB+g&~I3{*B0`+hA$ZE6E|4;k_u)Bj -Kr!@5gV3^k`b&$IA>Nerv%o@bb*`if!QithDj2Zq|FO$25Z -CJ2i=dPs>gq?Y9#*4j%=ZDRgK99ZWX$<@Q+RNk44xJO$)a{pR_5E|hW^_FpTm7VOSV;K1?bEI$gq03^ -@5bP=jIeoLbBbU8UWa<-giV^dap{WbbHZ-yd~0s?eY&vB&m_;!zto3C1RQ^7*TuZB%#KN=%T0MaN|VUN#x_ezVmQp0jm-yQS))ugc3=63lj@}-!t7pEs_&vwfVdno<1_vTgFu -q)?pzL*ef3>&`UjZ0r%OAp&U>xl7Cc4pYjD`RT3CS%yhZu*_|CWw(xS7x1UZA3;oU9sr%D2!&dI_ --|aggBkcLh9X~&}CNC`Fz#E-|re%aBsAGTI_T7}Q1&69u!_8}(uy?|54^7n>v?w -PmX7&xVd>q%U;g!7ec05=zt0GplM>c;>$x8W#!dqG(q0i{(j53`Yr~CBMXNm{+6Cz8RmN4(ZCuH|MbpBs6mi^yVV^eUmJfu-oJoz18X@ -CKhWZWGl14lxxV~K7@oHAP3_$?*Zg`v$=WsRIsKFY(;nF(Xbt@7}@L4@u!;@D4XFw<`aOlPqSW_Nn?WO;OE_6-%P^fXOr}p;uHz7<;e1je(qpDkw4xJops%2?@T?b3eJ8s1s0mLsZ(NtL+@NZ{d)DIUkhQmptVR5 -!|nb9`avFZbMxm@#WeJtl68By=55kU{WE75&;qw;0`>EthcRSn^m!(0QUAY>FCD5HRwfQBC{K%H!#KJIfz8wT!LM7Ai;T_>(m9D)Ae*I-(Yd@$5X1yB6UxL9>Pr -9X4lRGKsXQt-NOyFXkN>Xp{1+YNNq5fIf7dZ-a@1sX`=~Pk=X=Mm`tglVkH_@Spz%lEKWi#nnk}=YK_ -e}im2A$ZU(2z~vKZ*+b5e8hx;ZO+QsT(rvnJ_t^*W1w*8GvfgLUS-L8eUcobL*Bjtw$o!WFzXd;AY&&DMzw3JJ-`f>*5y2!qzF=sAGe|jjls#1UEm28(W&B-ss|w#o_DXmdxVt+bFTQWprcHY=N% -+QmDreS2w0F)WvNI)2DUOH--622Db_@^dK{}9bxU;&rR^&3l=QMOsDsGeqB{upV^wxn`f73T-;_deSy -wyQ@Fp7t|}fcbL0NB;MUj@+zNTTA^lWrULVVlO-ZMKo1eptO;ee`&8e)}R1E!2bwmr{_!) -*D+tB9=&@N^t4m{g>i#&1FoHt{Q2YV)k}iv7jrm&{ -(MJL(fJ(UQ}tCg>8bkjjZZ=SwcG7Z?pkN}7a%F$sUoUY^{hYtBNW9sZI$&9DC-N#JfJ$ATU>v>1<|TK -h?dmXv-KtFbC1=-J-F645Zul!{z9Vh=8$CHS(HCkv!pjTuRZ_rKGWI;6 -NhZ7Uk&BC&|P_T*>N~Q$-Qf%quw=4Ya#Nw|%-w%HGWWRgX2f*(R_0h1^55Zr^#wA66fFlA2~L;t+BRuO}2%Xmn@jG{@_)v@L$E#IO^KvYQa`cD1wq*8qF=pf2>CNy3hrhNoz -lyX}?}ze-^8otsqkbN9_2YAp?gbC+0{t9^bgpu$=K)F^9VR5|Q?Szt?th3vUVIed`3!80@=-L-Wv=Bg -1?29>z?SI80&zyO`oQBHjQz&Fh(U-W5J!Rkkxunx5Y&qx>>E2M(v$Kf*z-$AnYB~MLry|Jbr~b3g7zbM!2PvoU)(+E*sH@&5 -m~tm`!NCv=_bx#PF@n-R1>sa8EE+IcOw#j`kSSlpXvUvT_R{ZO)R@EzLXEH{blg42X$2G>O20W6KYY# -(F&muq~}SbcLX`}!O3-z|r|jzGSuSbcNY=zh@c>D0eXgEaN#e&l|M_IF#`O2ns~B-ihO@Vm~550bA1u -Hnx3!uYa{!(Sl$);jTg5~4YG!dqO8mWe!-LY}(9wW5e5{`77s>Zs|0>_jR44%Q=w*?(XO -IPSnn!uRa7jB0n7=PDC)bnc~b*Y#1h1{h!u#t5la+ihY`;pHXz4-)|8{#vFWr!7s`w)*H)*{ -v;{)yOrFy<367BL0UhWHHPYQ%EHcMuOCo<#f}u?evSmRFY$N1dg|EiC7IupD|1bd_InpRBh=<{K^fx% -x<}IX6~k%*@rB$siAKk8?~&#OJ5;4=a;!_lb1w;*PRf&_<_Al6BUc>EtVE9M#PpWF{Dl`st*zinmgb>;SeTGa{ioZ1NuM?ac-07Hs}}!c^52aq#xAQI~7gzJg^${;50s}EHX -b+FMpqPeDtKG=mat2Hipm=0Mn;4vy&B!Ptxb*&trz0Y|>{;CljQ}Rx|vI10R!P6Q+kJ$Fbm1yo8hWW> -&<;3`j+R(F(Pq%kAYCmq~g!xlUnU5C=zciIr=S#j4MvQ+weS8ZrGv(%l@pwogtlSgf%Sgcxa!n`+EqR -?Unq%+Q-?<$_Q&<{Dm0bY^RTiB`Soq|i|pcu7DmPPk99=1w)vHyA+y8A3HWmE#)8u?ziL$$6*wb&~V` -@~=u_SNdU6G|jhSYk5N6cT3Rg<|((FCnVMFZu`-LkdW$4Y_CxJ;~*t}w`q7{X -cwk@WE23C;d84oejCoNvsXqueUy6Z3VC{q~V!vYyW((8AQnQ@7CFu*9({%ELTbu=~Z^&e|8rmV*PSX}1nG -CnoRQrVJMEZL_OK~!-0_2v5Xv9QEIwdomjHeQ(-sz#{O -E}z^IoV_|vbI9jdQ6e)S{eN<*R}PO$#rcVWz2{lIrlQmMr&QarT{Km+?>8+Fr9w&+3|S-S{n5IJl?Z# -tFDKdTwuweB%&ZIOK-N&7lj>VKB2&BDzH+!CbH6;47q~7g#DhR=gIfww#l53#p`1mUi%zX7azY -Fl6K75-!xLnih@!y`b|4)wQKTOT#&3`!g-<$6L?&JUUdZ9S}e_l-_mAlJuVG~>_(v}o02|)fjZY;4LH -$Ygtu8uxoC)!7B49mg)-i@a>YMRl%_!dVk;*NjlKOFHr@PBMW{e~{&!iI(o4>apv`^b9sYoZ67ryn}4 -A*rWlpT2#0z~awqzpcAY$dE^P$Oh4G&-|~!NAb9VdfjLK-{BxUZ{hXz7G94F=!@dxJ;Zh}|9kwuc0ez -?wX2Hw0agOW0R5LbsRQ%@q>BXG16Bb>1*piv4um8Crgb -D_3ZS|ZA+rGM02}(M*v28(07C%X`>9AWV0%CtU^hTb7eac|H9|e;JMHJC!XCJ;2zjzU-Am=XL^K>L08 -{AtBi0dS&9?-tsdkXDl3Ew&Hc10!9k>O<$B>PQn{G5AjoBqUg+SJ{HV4!}d9(QKmbg -XkUvHj=}zj@Xcw0#;G^VD6qXn63$`BgKF<;2t=HyK4cpV6TcHG~IQ?7rK%J$^qL`I}W4kQFKz}FuM1X -PNFZM#{tF;=lXJn(>;OGRb?Z1di_SyJr~m43rEtur9j`kkZ;gadM|x9vtFVv>{Fz3BkNTLci%DeX -l9z*x~s3V1dnczP(jHfRdu^6xc+{*zwNc9qZ1E&bm5vt|svT5lW@H$efrSItJRk7hPR)zAe2GmRGQV0 -wr_ez)C8lwma1$V!3FxCgG9Y^1(20gKKua$JE25>vL$Hr0p=cFpYTHxAvT0ZGg@^~l*>AX~&K=%@;BN -d5sPpLZ6NH@mmp(2G7pnd?VCt|rH)=tHAP2*TOjqU}alT0aekE2YLW(LO^z!9o+$#)hZ{Q)(pw4Ugsa -=;xD*jq=)PQbtn$d@WZWy*r`g7_*8bUn#=DQz}K^&GI1>N_bgk7H~;-TO=@X-srasSH(viN=$q3bfEC -vKLjR0vdn16gZFW(R5yF0Q?QGt`O`6`pb*xUTB%Bv;{Q0UKRK-$40Yk -;3Xha4XfbmXITWIe;HYP#$H_z9fU2Z9QujQqy{>H%*nif$rr4?Rx`n52YK}H@>BY4ZIzyH*)*rFw{xa -8+m(b+{nu#bQ2+0ARf~uUT+%#eKpWOz<6h|+eNpUazZY(xad~>7UT=un<{v_NO_yCJA-%;%K-yHUiA* -GCmE`ecX_+71`L4mZ`e-X6|9#^b`WwE+MDftNT2GGTiH%1ADXWZp?PzT$!?LekLS -B$KOu!Q{{1xF>5}#U-Sh9f)Cjl++zSs9;sNd7_A&Htz^f0@dV5|~c$lZR7O)1~(>|r)>LlMIg#1YR;i -L3jW}TD+*a-5F&mjNM-)oN1aA_*rFILWt{ -Sx{Uz!JbZK;M&aoubzlz$(C+lXUO5I#uZ@8Xn5$6iru}ss?QX_dM+Rxw@~_RQgA01;m!rH32=xiQx*FI?XT- -d*$=cIKd-V~rtPL)(pI!WiS*q9{>0U5tD)mo<4219@jnFSaIaFSS{sDN(uW(%jEcp%kdqB+}gyd -5GCzKmt8OX~{mkdDS~61zSY_K-*&xHkaOekjc! -(h1kQ;s6?dy3_2Ru3wB@%#q%=2!~&1jMK5L*uWPD*AB$<$Wb+XVBmGqx#NE)qv~ -ibv00e{s!79;9kf_PJapNcfC~GpQao121&%dH{=^I6EJXq#QG-_U?6b1KIU=2ia`>)euoSOdjnU8NaP -A&<4}p50n8a8k*}cN(~gwL)9#mAG~5rP`93OAHj1XBPSTDB{lI<0C1L}NjgUwwU}2<0wgNUq!O)1xV< -Zv_sEwoP)TzqisQu1K4S?0)UNT;SdJWgT1d8V+Z6dWZ^e;&g@qzJ~Z4#sl{An^FU4Yeqw12DvJP74lu -9w(#Ha1Hl1HpgVY>6ZQYUfBK1+WS5ZMYtl=Sn0OctySh*XxUJDJEXu3ITV5o{+f`YlpVERByc$YnI41 -=&zeB67CsoX&<#SH*n=e7Xd`>D|D6#$}a1kv>sJD+w;iYX#^gR~k7p#JhVc+x`#c`)4 -c?3uKqmv8fCVkp6!)oqv4D)VsG-r-oc&fPrHLVJqmAo;K?0|n -{h8~#=X25_sVA6tDAAJZN|Of4({~(YE2S<@1Vh{*qR=PI^g7A^Ca}qPES&*mqTCR;ou>wK;Fl}Yj*Fzy3vb?%YYLs;bDTQ>VzSTek>4g0L~qKpH9YAuEp9d`Rn0=FdOI=rQ}xH*7vP=FeZakb3ype&o -h6`;k9D{0VS1ke?we@cHv8Z#Z`3$Pvcb6oMN5``LaYi^=CNY*b%?!1j-k&p)PaqT#c$ZgBo6^?&48R@ -Sjkkl*0)TyVcZIq1Iua(aNDN4b8^bN^uej~qGnF?aZb=|4;3=N#g<+m{_XLX!q=G<`RYT|W!#cPeM;U -vgvVksC*Tr#dLVHEC8Hc({=Z`hk1wI@G@Znj^;`3t7}Y>e_9K+x^U+;WSZx^NFEt5{?`LAJ0&8sc*co -dPpeuzHlowlp{?yj@hc1|6_Lh&8d5uZZI+R5B9yZrpeC4%>Gn -G0~b^ON9a+@#Y{g-8*VJBF2;k4$qoOiN9){dW1$N8JDqNDN;bkfoMui5{W*sl+syeWoOTeVc@&B$YmeRj=$^}|P!r}n`qOWz@4=5p*IszQZvQK5(V+`dr%v6Il`wn@V -CtOsxfAU69jW$5p6<8jfvKtFQTxU{JEl^7JC{@akJ{}&{_#fzVYL|U86WZYxBGhf+ld>V0$xAQ2L|%URWwK_?8d6qPMqYdEHCDbA6%}O1jvcHo+qG*K`Fwu`S#s4*b~M__!Gj0Mp+kqr(W6Jn@ -#DwI7hilqPM~l$}eI?mpf17+^KQ8)H(jLP1gzzB{ehh?9fbcUQd^UtHw6`H -kA^aK$|0aa5g77CGe7#fnz7Spu;b%g4D}-MT;opGp`yl*r2ww}~zlHGUA^ar>|1*UD-6{OwzHnU$hr0 -d%AthEqDwY#+>M-w+rJ;U_`(3welii(dMJ2pHjH8`+ezd -pUkjq4>eM8$y^{>8zMsHoHd{i%m><1{FbPmGL?9}DWEBg4l=gr^Rq9{Tj@8G!P*L~VF{cw|&0gs15mG -;qLxuvAAP;}he>5!2PLQ|h2WTtn}k0h(UpXn4>cA0Ib1KDB$h&VC>V4}s{RZz_Zz8%_g9#;0~|*RC^{ -vo!YZsewes$@)_}v~SmL{21n80P_%_q2VK=s5G3)+qdgJ9+Q?3L}P&Psbj+vlj7r}<5Rm(1)cpmb?WH -j;U~;Ts)*wGf;VCbYfh5d}4H>zl#Tu_X}eBBNC(HqZ6YNV|sMyfF7v4e_(I|;rb(46vA -q$qp1h9AJac%R3ex-F*-hxWrD>Z><{TrjT)k*Nr(p>U1DO|wQC0+WO?ey7OC37(GWor$a}=Zuq1So<& -mvDT8G{f5*;6v2v&~qXPM|OHaZ=9e|Y#%x7HpZV^b4T6XT*Wm0}-t5*Il5R -qgwY9uw=7*uCdQ-a3v?NRC38ii$Fd7Fs6Ni74v`DGDWs;sZ+&A15&l@#Hal<;*ZM%zMyGF-_5|$X=+W -RF-5~q?weD$VdPC|E<{zHxn;Cp6`D5ey#6YY-aXj{ol_$dk-HvxOV@Te&IoFt_uig7t0U=#E~PSS&%O -8AKbThM6Wh218*MAJ7b2hEZ=>1Ow8r|W5x^_JZivAty}fse`Chr>cfX#UVlss4jDDz#ulyh*dG(k;wo -QzAnC&broW-xO{2!djEo+@vZg57>-$D*P`iF32QwXqYj)%ML&a|XzHhIX0mJL=_Q?-le<%y$@L|3C`| -Z|TpGh-(aDTtu%80sSq~C6$(m6G4bD}FbkSM)2@!XtxX1%dojuPz`&ov&W$awCdhaM8~NOtnQwXsK0) -*vM}b(m5xB}UmeZ)x;PxH6cL=)araz;n+bOhb*O0E^?Sk6gaU*|j-?4K@2p`)ns6%*Ic$jiqr|zNQw_hLLjt{u$# -vA{9TgR@U;Wu0#`e*eEoi4O{cwpckL;0LLZocWp_QAJw55FNW@cK5bTK(~kwr$)0;kx$SZ@(e%dVCQc -)ZtI916p?q$GE*sD_sA-f?NCtf4ZUb^=;aOv3_;z`rj>YgG-&W3It4A2~dfq|Nq|y4<1~~c{J_Vv4g(-_FH0qN(T-cK!J)5A3jW+e-rjI -4#a-;i>j)s?-U>f6tyhv}MZ{V)&?}q(s1d?AS5-=9_P*yu6(D?%gYn!#Nxy;QO -O|&bsyM*U#R(dGl-z*@n%ZKYvt4M#j**yu3J$s{tEcaTa#VEw}Vqv}n=ecinXtXcs(m=+IDH_e$YEef -l(UEVHb0=gz&EhIz-2AE(mN(poxy^wCH3>8GCxIGH~JcKlyiSxKLN{<-)JoaE$~M{HT<&iwe}k2NfJ7 -x!2jl(Z!Tdzc4C=W033Gh+9lwJe0=uVXMzV`ef5?23>cvktixivfB$|lp&Q@_w%{9 -d2j{%^-g^Rm@R0SNz=vOdm3ImH27kW5-(SRj*5SLZ1pag9&JnvIA^(w)k=-E2n1KI-4?du+TelMPoH% -9{It==QAMd{Vu9$$mtgKAvN<~G5_#ZgH20-4RC*Toa+w=ob@@GWR9}?aDE>ZYfL}5FLdX^GBw1>!XjO -gsSbNg9WZfg>94gA};Z(r7>OPAgp!{@QR1V020fEP3eU4ajH0od_Bc>m>>Uy6zUL37X>@InrN3*ZL*! -DG|+L_-e{>Gu&0_=u?Q`$WCU7>C_N-5G~&ZxD5Qoha`R(Z!1w*E9|KW%$|F*G|AMr{sm4SEDD;1IWvx -UlQG0K@{@|(;ql65Bj`EbQj~0evs(gMWXS0i6XYwwkDbd|Ia`F3|U^%t5>g1EDH;O1M}fGxBwQ&5n!w -j56~Sn2ag~(kbl@+&e;S&qZt1CKKJ1;i2;UhX~e%zw9Ck-Ermcvn^iMwQ -JYj%!fSa8Ei{^ctF-0k}uF7`{5UG9_);E#2;lH1rA>j4QCvNF%HEisX60Eo~s&y|Ce8WA@(zMCUB5b; -!z(@>civ0g$uNI%UZI`8bPZjN7IT)dI7}a-w-|WHPM5WL?eO2r>Z`)%tT3je%GhZuu0vv`}DcTHlI$j -#eKga_?gd3?!No(PHfK?0Dj1cKOT^K$hAKn5*NriwhU6ctVzNUvlL5;$Ny`5nQ92`@;`CgMZ;Tt9AkhIsNgdPe&v!7@OzxqYbnAljqq1WY1z8rVpgE)yL`Jc^?jA7>Ci{G7cJj -9--><;Qcu^TCZIa@N;~>q<8P$oj68W0Qg}Sv4zY42gE!Q7kmd_E8{ZkgTF=0fbn` -xKiZVtkJd8|ZpOjJIBZ)viO!t*u35(BJH|7Oc@io0S&cD5w$~3o?1hy7{{8!R125!+z5;H*2G}Jou>G -+2$ZKFX;j7@=Ft*Q&q%DlYYuWv&c-8=NF%JIv{7<$?%hCqZl4picZfXq8pAbWH#to&#(;lZU50(oYv@ -u3cHO6QNezx~Q{u!P;zzw*t1x$b&`T)2kPhjJ<@iF`_aD@E7nLCKyU>r&qhmErt2i67XGjK4eIIL80$ -OjHj521xAjKlb$ls#^kzySj`slO)1=>1mx@(-H`7_I&m;9W*nYp9Of|&vl#~r{}|(M`|GzqfS+YS$Un>V0^su -e^UssZu0&aK+}D(za(mr5Cjt&D@vXT||GN$RtKZPJ=4(e!eLo?c=c@)(B~7>B>J9L-H0Dgc -E|8mx&iUOiF2{RjL~{s#;g&hlJTKATk>R;xJtgX -JjinZdLu)rSLyh%wkd-_LXHx5EFvr~G!@3ZAq7UH0IE4~|GrPu~Mrp^wmk?CflTgI2zp;t96UDgPX+z -HxB3r#8lbKEJ5yv)^bm{sH{peQazj_&|2MUBq69Wfm=3MAN5Fuf?G;JR0K(25i#C>}KdQ<4}*CX=vOE -I_2l*?_}FaAt50&X3Q9}SS$p;oSB(PQ>RX)B}-SDy!F;wLLLA+Uks{;p^L?29qrpPjQ~X!A=G==1kB(Ydo{_cOnnU4JMjD4+!k7MxzTY#Gg)H}CA -@k3UWmCr%V}=e$zL5^#WC08hXMJp6G%4FY}`at|L3T|qqpc_h9=j0D~RmwE=H|I3%4(UsPJa5C$zAtN -K>@N>^S_bcFh^wCGfgicEwx_9qRGiJ;Xe1Ol8wH=8E;064$=8N0_Tj&FH1p45w@9^2$+ynWua)tFb=o -aLkWyv^q?%a=|rz1v;pjoqK2|fS^z&~Wj5K2f$5Z{9bz(G#nAoE`2Vu-IH_ka^y$PjST$`)cO9M87oA -6>e1>9cX;#-+$1t_FU{CMIMF^=5nrxKMMRHf@^F-_UTT(aV7IFl80J(xa -2Jd8jLlKY3ZFc`cz|VLu>C>lAC#K;7=<^$IydiJ^eNZ=K{SY`LB_+}Q_unsM%WO6a_(5yX9RCAu=mG2 -xzDH~d`9Z7=J%A2CCvaWZV21ryzRrT+;9x!T-L!l6?kdIC^J(2&__3}w{*iRWM7{`kn772S1@0;q`oR -1qmNy{-@KK<#JV(|OVMjqz*#8Y1Hju~T5kAoC^@{UAYo=2b+qNg!N6uo{cky=%6<^<@x&0dvzajpC{b -zm5!)NFlwtxlvhR(8_idZ5oElt=~)^*|EU~3@XQVyX9&`aT)2?fW(hy79e_Fty; -B8$g|X8OyDhR^N5@988RdL2W&HoTDENYufSdlzt%5mV;F60(-==M4F-caA9&$-;0AahTdunYLL^-s1Fwe=t10N;uD1@;ee0DlDE2^u5k1nt2GS>FX8 -@EJBAwib4ba|+SdV4SZt>;itsZew!Zki2Vc5%$Zv1?UVMA?qwhzjBOF#k_)FZGVkn$GBPX-GlJ&oLB? -*JzSF?6H*R4mmM=^TvS@quhBDRR4PhzwQtM+UE^&_bsL~=m20#O@VmZd-5hmWdM)6eqi)H!4N$i&)op -8a+g9CnP`AdU#^W -Y*~#*fWkf&$7M!j_uO-Y?nUc_}s*??b93s<2MX=e^e)qH%?7B>g$uC7K%RK0JWC;@oA!uek6+CE!O{` -k@at98(zq3)@b|b@?H~JKUZV^UwNKpo4cRw=IgjHdZy?%qR)l?3Too0#eQ;zsQjm9^?EzNPS2Y+FKPM -me8>{OpUDO$;{NzEbuf6xd|DFfX1pe#fvCpVze!Qq-=BYKnr%n=$IqvJ5K7 -7R2_m%e#Xyp<8A@muec>76Rnd|_c!ue|w$GP#^`!K+N#K3?B{%7mTG*JUXJs354xjyFKj{Ewa=(h=Zt -nl^P(BpwTqDO^ZC;Ehsy(dmBlk3BebH1NGd-m+7wf$}x&ti+%+LIk4YKf@lqK>FtAH7%fk014uN4?sk -L9Y)zCdgyh8$SCW*GK%L?UnlX-vJ9ovD%*~obIc~o;h`#=Kfe02FPQ)pFBpZJ=0#QJo@$N#QK~kC9Pb -!GF}S@)`py8i-CR{YO3fjpeDTO<%}kKhR4qnO<`CD?)CNBpcCkKYS%|xF>~h3^f`0pJgx1$0tVDgKno -1i*U@WotNlLonNSafJfg>h-o|{jzcg#CZ?2e^Dqs-uSbn+p?Z3W%FWA2a4F2z27%lo;=oO)+iQ2f7N7 -Tj9UqNp|^tjaZXOFu_taYzO9tXZ(m;Z>#7A;zov}Vnkc<3VXeZ=R$8?b-|*zPM#6@5eWt58>Us*%M4tfl>0-6MZr11o -`b>+}KG_@<1`HvOyzUb`5B*$M|7Cs#Jw(i}?Xzfmcc4>KJ)`}*yrRc~UflwQAzSS=YQ}HuAJ$*IcyZD -zue=hE-Vow(z#;Q$dEX%504-pTunzVRb*j@NO*nh -|Z=mA;8+S;De4@W+xlSdA)PWZ{=#KE<_vc}ejPeGjnHC@mc`4#J8Z7tou7bbZFUc9v|j;fD-OsBp+a9 -JLwUY5tz*^goQdi_6NzlB^sK0f}d=;&xscSUbHH8qu9c;N++-vK6R3#6?Ce?XhlRiDv^#%E~D{K4X00 -4a~AnfK6!e`e5$qlbtZh<^qR8q`a#*UtnV+1c5Xo_gx3cOoJp=$U7p5q03aygX66khu?NjxFdf?`;G< -{CzS8uF1#kW7%pn{_D}B$0MxwA`gVGM_&fLC62Rb^ytw7?}lp4{=AWXT;5x$;MGw|6F=~0`eSX5KaQi -9h`RCc;loA!6);SmJXz2k_CWSXqzuR!vXlq*6Bj2=oH&7HIg?`~{IDB2e^WVC2hfUXpP!PF@-})rsL? -<2$Rh+^pufg`xwc;bx=T5f_lU?X`>OLy&*%7B>o`9@srY`f(bVW5Yy7wdVmr_qwOq&`YU$`}KtHs71n ->ZM0p|f0&;oWFHWP5}*s)`&(!3sp(ElY%mLwUC#(0@?gU^6r!h{JF78XViJn(>kP1gNne+b7xZlt}ye -Tf@3Y%nUmT%kEb*gx2x`Sa&Ljq9N%51$X4gWkfEPd+Jn%+L!Qivb$Q8ZY8y^i&YHGY)I6VC~xV{cBj- -ngsmOlqpk$ZNR;%vaSezf!4qqHhKU4{njg7Tda?D*wzU9UmqMY2425@J*{24R@91N8&PwTJ;keBTda? -Dr2mC10bdz|L-%A~5->o<(BnY;Y4hgI@;W5J}8z8rY- -Prq@)XL)XdZGs*FR{8sm@@-~XHpv$oj#YCfGu5f -9=X5%kr-OUR3VEMC5|Sj7Fa$JWN&K=w7zkNtwMTjqR*KHF^H_?b}7HChy0A-A2p>e!!k;VZs0KvA0B;@;pYYtZ77E@cfF}pwscS72ynO(l@XWP8C=n4p>x3t+wNj7NiRZ0dryi -*jPg`rPL`7$%P1H?Ko|2h5DScqbojoH$bjj&sCyh%@pD-}wk-y&4BQivnIW;MLT++lz>B$2_W+rEbL= -U?0`lzJL%;dDECeGCH1=2GIhD@81-am6}N^)9KW{eYr|~MuGQ}k?@9Bi3+a+Q&e~@av)o -{q#J)|(0lwo8XN}J0G9s}Q<gSpsTYA!cdnybx!;)jkw-_wNmQqW(rP5MuQLI5$oi)O$x5 -il$tr^xFYrfTBEw+|g%dM5xYO7KdRHQ44DAE_j6(tsB6y+4<7a59*i%N^iizHZF!9}4ATO>mlTf`@o2KYK7#z13`G1#axh8iP`kw(2S));3 -@FeVyPj2Xr(W2LdmSZ%B^DyBeFkSW-tGliNWOpzwNDb^HcN-!mwQcM}9EK`mt*OYH6Fd0lXQ?aSURLX -oQH&tkO6Id9e@+YEDUl^-$DTVoz#hl7zUKKF6Y|O6`=2#i?tb(~##eAz_&IK~>f|+}v%)dzHU@Y@6fw -`E%e9U4_<}xn}%*D16re+z_vw|sF#Wby9ss=J$gPF3SOxsANZmga0E)P%+@g~KdWzV%2*lqR_dzrn$U -S+Sb2Recsp^ivLtRumZ;>dF3Itm;%M~S1%QQ@d^)Hnj2!Ol=;q%+o;;7oC5Idh!_PMfpDS>~*8Ryk{& -fv#Xzs4LPH>q>B?xUyWit^$|MRpKghRk*5LHLgH+ushTp>5g?LxKrF&?p$|)+vYBDm$@t4Rqh&hpeNW -9>WTEkdJ;S-o-9wUr@&+Llz7TK6`m?jjVI6>><#rsdSkr_-V|?^H`iO>wRuatW!?&JmAA&LaA1s=k^~ -xp4WWieL#!dekYdO(6*OQHuXq%k}?Pt?(DYyDDAPF2x<>*103xdUu>V(VgMWap$`Y?qYYTyWCyru68S)Adk -)y;n92IJc*tRPmU+wWAGGvN8;%@dkNy-UzSW8|O{*W_WYF`Cfy!*jwr?_f~qVy)f1-lvrW2gA -6)Dgh6kJGb9=^3^|5;PGgDrOuqD#571FZ>N@VMkW6U=ijK#)MV>#O)K0m01$hk_G^7*h~d -f#<(+3HlWG={Pyrmzh9EC{SdoIQbQn!;4gV!GxsWufWCtm~z$?d7cRmG=Jy-}U?Wm&gAAP)h>@6aWAK -2mt$eR#W>LvixVj0001n0RS5S003}la4%nWWo~3|axY|Qb98KJVlQ_#G%jU$W$e9sd=%x?I6j-*O*S` -Xxh#ZBV1-4|2u9aS;(}(#uFT3tKtNGJz-US- -!1srtY|^#uV6b)vQI|U+P>^`o#B27yi&$yzoaqdO~qN@|d$U_#@}{f8_K|o9X=F6OTSNAu}`06|19di -2UM<%dT#U|E1(@Y#Ao|GmCdm-=-9#?T2{Mh*vn1tASk|0>|N9cW0BurY^oCI$LfB3JmMXcNm&j9<#ADU?|3KeD*g{C@Z-Y=J*kZpn>Gwab&C -P8*@kwX=(TpQ-xwjO8OA1=(2!vR;WH$2&J8*<3&a%#6+1x3P{o#z3YIP#8Q`F5z{&qYv0WnJ)kx$=;5 -w;Xm{IFCuKh9d*>gshdZ4Cxg4)5C(Gj|qgWUtzZJELW~nYZpv{yQRtN8dsAp2qmGFE~n8}iwkZzZiD> -W8@B(~u~=v&8(B&a%El;?AQ76?6-r!9azE -V1pCb&&QVWD#Gi*H-ixhRh&O*y~Di@6n>8aB_)3v6_b40#7F+2u -ZM2xM;{ATM!^;}A?4htgY=_L+v@fXkb{y&`=*bamWe_Gf7}Q>D_;W7E=5vvs!c1-eG-P3|TGIgDE;I^ -=;#?(^@91s!fmpj<*5GIRVcxWNvM~QntQ5xr*2s8}l>!9gy*O`4Mr=9y9h2ru4@>i;x$_#fZ|5*H`#uEJT-ZG9RQUc02u%yLU!)=6jQ^EHJ3<*R -)#7f0F3W76y9M)%lS7LE`W+RA02kGX6%{a@n{X%Ss{ru++|BpQBHWyFv|}T2Z7|3n$lA5efl%sD@Lme -*E?#|rU0(fIsy;Z(jeEhh0OsW(s9L*w2u)6~QMXc~RFC#vCce#eP47A^53iHQY=ym&JOAO{u_pFqfy@ -rdtlMyWeh`YNV+7dKxk!CCnuJ(Knggs=X8WPUIQ3DN6L$EG&==MYt!5YwE4-`GWYPNxZq7M;Z%&8y3U -F_#C&pE_1P8Fp`(zVfYQ^T>V>s48Q#I6Cb<4O0?2jEsnl)PvzwIjP0?;f!QI<;B?wVmSY*)F2POq__rGNQK*L2JlOd$lNC&9#q9b=;~d(WLUjPjxmA-8XJ -@oEf5?E%6X!yqeb*5*TI_a0owC4giXX@%1Sp|;a744ASdiwN~~P=VPvY1)gp(}7NOQQ^^0xb_TWHXN- -yQ!{3If@g~IOc%N#(`m}|XP&7IGu_QIeakbocVj5?_Yk=r$J69{8;~(H{=xwc>~jJzqcYCO5+A~l*Hs -|H!sQA0n=8Q2I1a%334=R1*8aGi;LB$?+}Jc_1r)J_L!_70x%YTO0R+c@%UO$zm*Lpmjx81~fzB=KLT -G7nw`hx_hQYiD;j>(GU>ZZ#2k$aL=2zM|bS^L)-Lcm>>NXha(s&5>$#lb^w$rSyPKA!CWa$?5Vz@P(nebhLI4!JV)ay834mcviP}9{87r}U+V2O_xQ9a7o3Lzn -s?BgrKNv3)+>S&t-ERi)?uzUZsB1$Zqg0bY74Oo1o)O1XS9M`sFbzmYa-+CT%d^Dle%NGxZ0u%ejDQd --HuEyK@QKq*m|bm5ie=Bh>{i}4O=h2zc=NAThKkkc7=LmA#iz+?LlPv634R$%*h>kDt;8D|gNaBEoaf ->+AgPOR1+)ef1aWB?_F9R0=jhfbC}xu2igCW<7}RJnc(|l;vkCVp) -Egk|rDVvZuN9E8PtH0LoV)?hrd^F|^-8;ex8$2wmo!3$O+lNp0i;Ztz{m%~7o!eGYzTi!;;3>4=P -~s{o@l~wE@zZ@uoDnZCfk*?uV;3TW0`2g**qnWFCI;(Au-9)uEdjRDRls%@(R_5B;q$>DVFQ73IP;;? -I1Ru>?bY71(FR)y1U!5jvW(#cFfr?Kb~G2VS3JUS+^`P>=$Qk7|Cr?(8NAL9#K9SU@GI@1B%C~GXydo -kk0Jn6MiV&nG)~}ca&dzkehDjqy2=39iozDJ;rQ}H#HfG52zQ-dZ87}p0&vLT=}9HuyyItv+_gIpmLR -r;!sj6@8!Z8IeLSY;k{Ag3!*gvZ04LfPR>EGMGcjs#!R1Hcz=k -J0}aDxZxkn2fFY*by!*_}nKYwp9k%=%qSXlp)%0#sbw71|!>g%~+hMNV$M081n3)JjzE90K|A55AD?c -c@@X!0NK-VKWA$9Q5{*x4{U;*6N00)!bG(&Nf|L-%BgLK=(TIzb^0g}QGB?L#AUA{PA-b`noO@vh^Xc -uf?9yw7V?(!k>4tqiPXu@02a5&>;Q}-=+O4NfWOD4LRnfdyt4-2{c6gnQ4)zrP6(NiYFVJ|6zxx#3pp -m|Lk-*Y)Dq8TrN6A!!#*(KKKL3Uf)<(8H@NE>Vr@cW;mo4F2&eDtrZrcP%;oIGR+k;d48?vI;s_BSxctZR_zy8@#k09^5-Y9XX!PZO~V!=KCDpoY -+E{>2>bn|29P_8cxfd70ct*^!r_0#M5kn2UijONVKOlDp$2BYjJ5?^9M*C*3q1rJMr_ -$j_PAz`OQ?8#ook!4JlWmnINQ{9+H`iDGzYt6_4WsRto^C!}h}$75RdZIoqF9z67ZR~mpisdXAOhr(a -xW_Z&sOrXX@;Ki+2Mj#y6L!$cx@k?cpCPoWlR8J61d~`{f~K0RpJD -(Ky;WoP!)xUsv2rg#TIRTE$eI0S7X~_TcjThzR`ExknnbB9cX2>Rst{nE*o$!ireQcwR=>89)Gd2@N| -$K;Jp;ePm>lneKGBN56h+6Bb{7ez~>?|1rOkw3`!K<6ByRYJt!V~Tvg9v1o)^Hw~ -rCq>(??AAGC0K$xdIiceafuPu0~c#gq@)LtAH2h|nIdy~P&{zAfNWrjOr@ml#GpeI+Z@Tqn8e=N4

oQke5T~y^Zn)SUt?ok{bXbKyr%iAi6X}`<0WX&Tu@tgSg~7Aq?2!;ynyJe1U+{Vu5 -DPO)#{>^C3cUXtSJr_or)j!yBNwf{+bWD;L)R+p1B@f{c5DPiqHok;ApBLjmjCv?U%)2WG@tngygZztE-(msz2mRZ+IgxtJ|gd-5i#+lEPbz -@XWaz7?M~_3dMV|62qUWJwnAfKF)^Z3_2iet;LE9}?SAgiyT=I||r$80k!61mp|A-8Hb6pN_mm$$ONe -DB%IV-mRVOL9SsgkfQwJLBTuaoG!6?#2qE-IjJ-aeji+JDN!4o$cU&>ii%QHos*blV~0{u0Nf*pkGzUg+kZRo9@QcR#Oijf>2{R28k6Fs(y){jOKk_HN33qhsrTlA{04jvtWvJ}dz_OCT+9ZxI7V -`SyIDj~Iu|AeX|o#$&Yl1}kZ*l(Jk0QNm;>b}vGI@W9e;Lg{9G2t@dt7a7K+sw13^ZjQFYU(>Yc#h4k -%I2QruaJ+wAnKo0I%TGxoU!`@BurB35q}t6OrK0dTQO5J9!sNX+80ejlA6;%v41$nuq3by3+Oi@t6E_ -wwsav{Fv(o$FzTj>L}ofT++u(H=t1o@LI3Oek@}wuo%w2n=&OS>x(8OOY&YK##KoV$#?{)!ElYZz|uw7;vJ6f5;WYiA=aCRIv!F_a6ukuGDkZp*JFd@vD2~aZ$P*V_4OAu#}4CxLY0X_=&KEcnn`#q65orTPM-td1eEczPg(kahMozZU2->y3505WQ0k+QAL}!3u-}3>*PGM_9YCfY^5ttxYDNOx-r1}k+%)RFdw0gA1clBhYQE~LxmlFwO9ioU`xiOZT0q*k-3t9kWPg+lp9s5i-o*$e~%TB?zy>iU3?3MU|bwYqUkf5*z-^&^U;%AUQft)E -=G(lklF4SWsA@ez4AJv*GVL9q=?SyH-|v480%WsrTm_+yyE0DKDIO9X%T;Vob-_($n3@$ -U8F6(3#z+zxlLk?0obdHlrC{0;FRGaW<_i+nIGP8f#g__B0~ZT7Kmq^?t -Xs?4@}*%^OVhd1XNJq=$y$9>|IZ=iaF=qGShcl&@`$C~rAeL#2r=CEoYg@;BFhW}qg`6YL>@ -hdx`kBz8mWpV1Q5O7Xaq@(W(4=}B3w$yb24(6&?QWGW@5sTW@vxN!d0Z*cnf4vr)4?ioR4AtL1^td9T -~^t?D}y(Yl;j*PT~^%NFcPp$t5g5J4JZtY=9j_U|E2`@(@A=u1;{Yte!QLv;f=e-lIK)ZC{<0fS`X5f -t`B;ZmuA#=vso!00`C~f1&DryLL}(PeTsfPNI>X#6hkAv8s%;YI=Y@3Z#n!CDIdv@CkdV2(`_dWp$gO -6&OS`5d627K$?yCks3VW7po7Jc$$MK82M@t1wd#ia3$6`gH4dwB34^u{c2LQ1z?93(4m!=)fyq-EBLO>q9Vs%o959&GrXm%JXFA_rA4v^RMoJ-4@u2e2a<=|~-aF>qyy`dNe$n=NO{Gzvk$m^B)p% -{5`#z;<<_Tb>ekiK*PfHoVE2a)J}GZ_L;*^rK`vZ%#2U^ykW9r;=9Ti9%@qq1(r9yT+pYqJfq2h(zv+ -E`;`6iQE?K-l_1u1R2D=R9uj+-z1RA{l|+)JLuA(NTV8sgoaXnbYaD*~%0L@_2e$SzcN&$=E6zb+X4? -dI}KLaJDr9!lqEMXT>S)T}Ql^kp}@3IyZwn^NzrK7))`WjQjxtIZxS(*oh)`+WeHwU*BVu)XzsvWtNV -j$4}0YI=xnCZDo8EN!oZAg{Lt%hS<+tvbj0li6-hqed(#L!Yni%&`5_2Bs*SHQOkdS{5zPZ2#5k8;ev2Qlkb;HIMW-W_9|0PIBqLnZ?gi3=sHPkEm -o{zDtwaR?Rwnc|yJ+6pz<3NC;V-U6$k(P1!V?hSAMniJ>ycROvfZ4LOI|sx`jGnIDG$66|oBGq*Un+F;)pH>Z$386Jy`iH*?N>bsB!209lO!x4g51U3cYOIyP}BgXPCM)iV%=VhGl@6}EAZcd4{V~4vh+My|lj%z -R4dGE)A6qlx+GbvW}TvG63O-_aV>IJrSA94m?;ihfJx8_t-|M5+GY1@5dC3VsdCJ{%K{jDACBTk7GDu -LXgpj*z*-U*RPP@6J~3^;84O@d~giJ0rjRPCmFNo#OFl&!|H(6Onw_3lDFX7`Mk9+OmNZY_dgPt&rY5 -L2Q~d~#Ay08i|~ySq_b24S;#LR(Bt=8v98^*HCwYM{c7k(fs(8)%o6ktZ9;O4lHFp&DcQdvO5fLqlEvCVL#WqDLk~ig;jnE0>0v|BgQRr* -wgpjMezp!z#o|7s-zXPbhi!wNo14n>(0np@yf!@v&*J2);9>V6wFcSte#=txUycfR9@G$e#r03@=E@N -S3!~LZTQg093B<&!A*L417`69?#uX8JG{exc2gkGiCYJRaNSd{1n5aFU$odz|`U$X-Yqg2D(-f!G4#$ -M!I-d6Dt*32Uo)6ph(Ff>#64a(Gm=nuOO3ICpS1Xzwi$W^}?JbBRK3BUog7UeN0*m-d%=cGV*iNr&u} -Q23fl#VNt3|Kq+ZMo+e8f8wzre{xVgV=wi(P}oZigRz7>la!(M$+I8>9?{$nDw<@Vgcl{}mVM{K~iBO -2l#hIPG^2aEp$}y`;n>kbvc?;-_n4Gf~C+^IAHNwf8g8G26; -he#k>%UnT?`bTa@bgUD2iP}gMLlFk*zP~iD4-cJy`t>M`F62tKun3jg$X8mz=v%9(BoPD&{8Mb23~XqP{f|J6|ozcWbv~n^N -Mo1irCQrJ1(=xn9LSVldJ-3N!Ig%4I%>Ue`3|1Hocsj&qVA2Fucpl%?SBTlR2 -x7%A@D1dWp@{QgFe^L_GgO+-XKm??zRHt|)$7SoZp0VQ{-(O+tEoT4{V&?}|`4*F~VGt)+DI}HXR!@T%XpoDW*CVb7S -;s;aO}?K%x!c9ruv3tlH2ybQX0h_7aiH(JvC`C^8*zj=-*Z4wT*jlk_@nMo;_ln@|l`^VZ+!-*s}%Zn^8YTz}Rqvu$ -#HG2KuBEwc$OGzIy{v~+C#E2w))1@WuPLybRm)jr&ntN*Z)$Wxev -299Qcl}w5CNa-EAZBr@~Rd*&~eG+Tq<~w3V1H__8bQ-B^b+g(%^XVs*A4oa7#ERY6R3u%QVzelCsF5t -?8a0xp3{|@=!RLG^U!j!gV{Pg#d*n}8eEruc5yVIZolON|C1M?QS3zVA@;YpLh}xYjRt^JL>}nF~03Y89t@P9LeSiUSe5_eq-`P*k!B -94ZzUrrE$8u&sPEi-d^cow^-FDZt)nLWu4V+OOvEaU6gwhSiz|NFGRJD3}%Ujqrwowm>Kd4Q+* -o9sb5~Wh01Oi4)e`i*epX@=vb&Pw`(Lr_X&X6fiG$Wn -u|y9%-dke-8q@RSQ`)V|yvo@Up|tXysTPa3=90NT{ELEivZlzbiqitMIQh|MhDVjZZ$Z-b=AAxXE&2y -I4F~U%>qgxyNDYf+wwxznx3Azg^D -B`J6ENeKP_$SKx*?H8R5$BNoM)57XyqHFXN;=@n?PR&j8T6?mVhmvg`3p4kkx|eF7|$>=e+;;_p;xw@ -4Y&hMZ|&#EWUj+XOwQY12{DtoBNjXy2QeL3`)&hp^7p3;Xhyn(*l@+YN2+-dTfE7|N!b!P?6b?^B0D! -kMTX9Byz~x^7UUNM3Cao-mV2DcqU=%szD>EEt{((#xiyDq&|KM_jY&hEfL91i0G=drGIRm)pR;^+h))2`SstR(?^V5-m7E}gfpM- -AhR>J+Z!&hdD4^|qt0(`Q={WczX?kx)oB0a$u*hCk(Sqcp -Ef`2^45n!F2p8;cfdY|;4#?uE;dcsEH%jDiX#fdy1E$>W5ICD%r6*FH7gs0nK!936l1+j$5F -YzFPH?CDke{7bpSGETSIfnX96>b7;fyjE=qDDGQZfMIMZ;14HnGlE1LLVS+0}Edl`Y(EzE|IZMvbIOp -4)&Xh2}cIBwJt+^+Q+Ew{3EfER~%F{a}DHb7WvxhpSK7OsJFx)_m04#G`9ZeT;B{IHkcZ8#{x0p^*MH -rcEW(mJZ@TFxhZ?{gx2RvnzP)%1Si37#K?HpRusH1u$DPY=PIY*d=o`o7k=50~@(#z@(>0L>i*KogoO -}d$KieT3ncZ@Bp4Kos-S5A;%X%#eQ|=1z;8}tWkfmb{uBeH8o-HB5FSXf)qd7*XNKVtd$5{=fH~w$H& -mL_!c*?L4vsaBanwD^p@?^GyS2t7X&?7n+r*6(Jtp}EVSou0G#TT7A2u)zCSiNIXvkV#H;2*K&TE2!T -#Er9-_>)u-sq!_z(^(<8ZHj1?Fn;-*7r$&pbp3y{e6CoE3|)TNkuv?&Cr?-*>=U>?ZetX@4W$zRqHbr -D8uaYcUwgT+bgH_~Siza9f(pzhaCrcFkWAL0w{(nP4ps5cyr;?qs{w<4IiK4KK^Mb8ashUT>}q&C4>j_u!!Ns+TNsup@$8RB1%a -kx|TFTY+In%+S(up`43o>fhgCJIC2C8w#{e|tz8scYQ=0x=uNW_MCa>WI_}Sx&D7fS@F0^E94N@|ZzA -c^{OSc$Fa;Rk^K=?;GMFZXi_F@2fX3~WhGGMBYKO&KtDdtd{nc~n3Xl?ul7e=}j%734I0dcRElB@ALJ{rmX|nw)-b-y9@TKX3* -rvTTz3Kw77fR1e@-AW8Mun|=ozj3cuG7{akETz0z+*8r6Ke^HS!iG7sI#Lvwg&91ktxpymViE(p8b_wnNKRaA0NMLG$s7d(@WgZ!!tgIdZ-wvWPAxl;hP1afS$CsuV3nWeRA1^f8w#ue -AGdHrw?P?OKPNJ+Z@@btfEdA@uDbu}%=`4Of}%gfpKurIO3H?qvX9FMK>FW(@RU?l2rTeS=nwH&{BBe -BMO6QyXa=TRAUi7h8-hQfDeX*c?Wn60+q82biMijiYT2P%%C^P!+MT$rUNBL|SEw0txIRG7ijV~%i`oV -+1w_3+DiLlo^D{t+=mc6hXJxUB!};F%AS3teoXt=%aO`Nx!y3#;So_%r565?MmUiaR67lao5E)9>2)< -v972hoV2#?S-5laCKHRzZydLSXj;OG!eRy40yU`q@!M*q=oXxh-u|&m|@3IB&-Fp|6E9T|q{(mj^f+h -%?(r9|q4M))*_$aXyFlYM32&d0Knbp_g2}()6O)oTKgq>Gmr_U6Ki2zcUXwK*B77XkeBRun6~G9fo=)_P{)mNkhL!C1t}S;RSJs*`> -IRY1aANGLauR}d>|NH!f`ehvib_Xe4f)3NUTJF(j6IJNs2G|_^(lR+j-8@~oKlK)sq=$6V0q$M{TrN2 -Wf@s+MGp`GiXhDV?V?kv6b7|ocYSF)qAX^)-)vWOW5-VBZ5tVAa+n}QKZH&Z8uKowFgX`;db-+Qn!{+ -zAy?x>stduP8U({bSC^=4qbP<;E_*tNYVF}%bNnWX@EP+^S9V}_wW+}k)FJL+d3;gRXUNT5>h(rW+H0 -o7g_`Fh_-9u>x@n4|2f*ZBE0bQ2^9K%&#wKC*dVlP*=+O7h)Ge`7Q0nj4bJ&z^=UE&Uv&{(^$4cU^9z -ra9BNnOJVzl?=U}u^q!P9kmneIRXV#0_3)GB>(~oR{}haKIM5avy#MF7|p15@LqxuWe5+p^7+VCC&?c -JgailGB5-Jn0f%$B=8+3AGwGNeS{>m)a?&N2Y{Sw18eyx22FG*P8jg(+GckGTFu=7_4S}rvbER6mN(2 -p~Uw*Bxr*&UG!;i)XXOTu`_geINnwE;%#Ew4F(mYvwr<>PMX~?2CihIsDvOEjgJJbV^Kiw8!O@`xpuX -0&^ygKP?h!&KgNFA-6B-^y2%Ibfb%vh)XHw+~6z-8yim&&Vh_<<2Ji)=IzpWo_5b4abVKSB;*{#AAS1?XAk+m)Qj8~Ze2#tDAL+P0{Ue|q%Ue^*Q8bsf -xUn{P=-{ck7)pD5dmyJ#>NJ~_)Us6j8gJS$y&nXV6nj~!Q(EJ-9jhwG$Sc -~3UX!|$YUfm4kRU2cPF!jqkPhJ>MhPzwO>C3P5^aiTvcZIW4dNlbqiY~owDjO*{RVB5kbcnY@gv3U)> -?5uX?G#RhPy;C-R0l*B>WDPQg0-k}rj2x!En2Q0UI*9N29p^f6hCJEqbI3Ru!F+g!kscfjjH7(C5Bq7 -=vbSR!)<-_aLO-Rl%sPugu$~}e5K!_K-bVu#;K*#@sZQ;^)&y&ku!|txt^j2n)Qj%XyimD1)_0EN%bw -uaChM)|xTs?|{`nh0;AZJF`Tt1Y32 -Grakax}4&(&ruGq>*~C@uB(;td2>w4IB{K`xK1{Cx|ch0c0rxDWkZL`(ky=SUVqN^;2G#xQeIZ@137% -x$XdjKWj34NyoJ)(J(lvw8$bo{n9HIJS0kBrqdxWUZFZn)gmTD-=V}-Cj9=t#Y1P|KFJZx -gG%Q|%>Es6t+JF9>0q^-tM;OfNr+ps{KoWLe`KI%0BFg~@(9WjKn*YUKv*@>PT7;$3Shh9b?x98M49X -wpBybD<;HYa0$J^YtU~A>G}CL(2X+C7Pkn*5ihu=Kw<%yc*_V%14;-BL&S5w2_ck`|!)SK2W#1Ve-C4 -|Ly-~Plr5_I%ySh$iOta_7attc!!WeS=|Xj#QIOJIS# -Hh9l=s?@-cZW=H!y$dM$gE24z^rrUa>>YV5o20&Pjjx8NV8M^j!eaWw&E_7My@S0Nl^vnWbTc57Q!&# -Svb1w?_?cLS!-etskhgs5i_cG(%i^cK&ZHxEMYpSBV2S5_L%qOpW9Hktyt1C01X0!xXxnJuV+&S0 -jKMfNS{V`7X|QKnz&*kiC6(Bs950l@vuC^5`TIgkuSWzc%kBB1e$Wdg%$s^)l_r^UU7WMUrSdj2wsET<+EY|)8rN|=!^@Q -3h5DI1KtF;%7(nVN{?(ORiv8b;*L~mstsI1XD$`JZ`L)^>j4Z1b?D4uXQ^=m8Zo*dVhB5-Gjn~vg|#h -f9=9!)IZ{~VzO{0^_4Ts(@sk}t&XdVL{cw&6Jbg8|kesmG#Hr(g-N-O50T71)Z3r(3$(1LoawadTkIZ -hvvZ-E5GZ+?HgUY@TZCl+2=%0j!x>J<=h+956Qr+}r%Dh2NN3rKB|CA?Q8tS -^?pDVXW1!VWy@;V3T;TL@$OXgrli}v7Qo!b?f^rMdcT)7qdxWVp9#WcRwn#j1XhQLZ9s`P-vmSol`!b -Dwz>4HZ!bNd?{hw{p0Ec(gIs7lVk>eV0{+#N7078T5+{G~Sb&Wfi>B2@o)4A|E|JPV6*`K-zXf`0NR4 -KU4R!<=lw~+Jb&EX#(*Vzl0G^jPLvs9p2#Dc9ttTD8aB7xZ+|gP8OmK*H$6;J%w5?XBW;v0mz8NO1v% -Wyd)J7jpz{96*qqE+m3=KVH>#Vnn6(K^Gg?9C5j~p_Gre?9iEJ#z2e62Pog$lF&N{87uXl^_l00~C+v -;+Pf%R>tw6imlEWgaCB5iv>G@ZMFJg;p(MMHunX60TtFQ<$x9Z-*^D9M4GdO2j5&CNxu*{>eC(_bG+4(9Vf3Ixnb|B{lDwQG@Zs`WOsD?N>_t! -Y*LV_y564dSQ*0W-mWe|a}vOT07}^M_`@PMBebas}{ghrc6G9v&L}b--UV*si}Avj-6y`okZTfFE82U -fS`83c*vupAP(q2HQHyGF(Dr7PrB>;8ite(B^t(;xj?3xc- -T(%<`wn2`e}pTC*t*=vCJga-JlwaM4>I!hVXzG@H1NRC6IwAB1Ns>JhtIXHT7yrGC#A_dvV&ZU#g~c4 -9eZ_HmFW;ygz#u9sMQlkW%~wuR<>hIoCb_LD;(QyQT>ZN{PaW{+$IR?lq!*{XU2-aOXLctM!@axxuA1 -z0o?&V1olhT*R_tm+q|rn2X{oe!G?`SH#L8^OJG1zC?@A -JCboJ#mZaBz-f=N+q-7*jd@ZH3g*6~lTzCSN!+-@TlK>QMvF2C;5eN%bhW){Qr=XL^#s^A`hr;a<=wj -EX_v&w?SL>o8i#a{Y$Jwi%MZqtxY~2{N*6XA0d@p94fh`M={07fh?@D{FVPCIeJZPI$W4`Q2Zruv$W+2`o<;els$aZ}q;4}L^Fe?3K9euJpvzn3%}w=HoKzl(XMhUwp# -o(_ZGmy3CMl-<#@5Jo!hk+aHktz --ZRrqy*k?kY$Rnlj2;H6~v38sD5M?~SRt$M5+hn2yOoYoj?$&jEmuVvNH!L)$h}5HJyHRiRcz#2s&40k2ROj4 -9yxp(Z|)0oy?{NyV3!W!PqRWGf&QYPgo$C1m!bvUHW8kV}%Jre-}~{nwb|bAqwn8}rq(=HO+qw_{B??fO8NpRR+k87khR{T2CI-)8~#!|77aC%ZK-Yb|1p$OgL -i&`pa0exL7uRKy2(VT;3X)T!isXM#>+ceg=ho(2$+K+Ma2!7%QQb+$l!smW*lEVMFLj{KQy>(}GvR5D -AX{K5YfAQuooKGqA6bX%nw1(=+GLY3s%qS#}%!64*C`0X9O1;C#4AYcvyz)UL1w+B-p*%JA?KqgUzYmImt?@wh)c-+TWl!1V(08Dwts -(N~1)3_hiN3OQ)Q0wNd`dCn*{Kk#Q)zYEKIu16BRAAz*~fZ=E8l ->_ll1A~D+$FT@0~F%4wht0Y!25yC_;9`;tfiuzUZ>5Z^!MU*;2%QD5WEGrU3X}&eUKRKzo`z`FOXiif -ZTM4Q94w-yf>TWgM_q}@LG5FF?-tT0A{#nD_Iyy9Nw;e5)5eX55x4T|RnFsq6Ug#gWzV^#8QS|;o@9)TWM6%`Z*`w@(q229@)P8 -?h-W~CCF)KAuDe$w$z1%qpM^v?G+c6GNW1r9N5RoGPn#cNry3lMN -xw8!9!!+YLfmuyNg$YhL&4dCaMQO=u=#eh!r;zCP8OOB*1u(6DW3WK! -zj`USugUf1aNu2C_V@V)&;tf=M$^#Cngm0?#_R4sa23+<>ixtEib!iuuEkBbz#0zf_XL(|L!>9=W0Hq -E6?g_33cR<(+L|j{Q0vui88xtgOp{4-xbOxrB!B41Q0223j@MUnw&eSWlYALQ6Hs%%y2a)>x3a> -=z5om^vq>K6-$ajr7k*Ck^eAa%0gtULQi*sx>~$H9UXQK3tGx#AA|KwO)}KL2>TNb03s+%JG%|}Q!da -BbAZib_s(8e8b#mx>j~Y!vB<8Ud=xJ)y9JJ!Q4M53bk6A@`uj1X@j9PcECqvJq>~fO@^(yEsG)FSF$m -QoqT76DNH}1@`<5=|J)u%lp -&_NLM)?!-!K{|9 -;Z&nQdNo+499HQ$Y^ugL|JA3ipPHLDVz{w900vxWr%44-)Z^opZeK26cjBWnIaXoS8=^crRlTK-k!9#^+BteZsUj$5 -k8ObsvjU>MY1AAZIbC-OI-KnZ~{eu!xf3S65JXP5t#kbqsJrtMh>f7YWq#hsI7V8{JHS2Z6SO4%KsAa -8T`?M71zoGtlzB^XcvAs%u6vf0-MNv)8V<_e`7LFaEa6d7cbHj_}X2tgek4gtVFY1#1V;3U!4Xnq3Cl -eXXCh6|qxz$JKOn76;A`-QUUo9(9E1g0dL?yjtbkhN0DL2XuhePQDt{wO_COWqX1&-ArkRym|B9M2)r -Y_1O74qV>6HI67Y$5}a0_SUpZ+dwlGyk8KNtX8#ie<$@Al#PD*UPin^&jqG{W~h;0-@~72Y%v2>H -rTFp8!UgSfRs%4vYbU+w_Rw(KPyC$0il>tByh7>4V#R0^^5lzM-c93C@0mZ#wOFj22U5D! -xurm?xWS5DSpS(#R68yFt2cAwZUTXbgA7YRZfmsRb#8diw308gIX1Ke$)y^xaQf20;wHfxeq!&hy#Va -qoXxwVUKZ7#_lleHeC)UvR>_OrB-#I_TP<5PHV(=6o&bwR=A(;K*B1PUQPAv5KL!+5_Op?6fv4z=Cff -E@91|;M((&0LSbtgcWfoVHw -vpjqm)PqSOQSHfJPE;S-H8&>v|L*bFH^(mg})VaajggH5RQ!h2f)kq@ktX8*A;!bS%4Ax0Hl@ZE|>Cn -~z2znMOOCustNNJuTppCCLZ157O|{?W08^1h -%lsn|4RVFrCt|UGAedO9EL5XQ#frDEWH6S5!cFZ89;iG6m4~HwbQz|Uj~r)pNDDR&gU9o?3}Y8CJ$A* -0rXZC@utUyikZE-zZvo{*TX6(|6C(NCN_0mEB7==MG++4pPG7}mPp&Lc>t^81O|H38c*Zw4y$|b;OLP -MWrq7_<-t;d(#$k6PR$Tl2^w^Aw6=g6bc$I=L{Gb!p1B9OxJt>sZgx7%}eOhbgQ^{`yp$ethu -tD{~G+wLO$LcDLCsX(*b5cQo=<@W4(|XVcW(m6NgkRl-fZWwSyk*Qbz$pQ?%h*-f^0XC^9A4ze00Fhg -U8e%9E<8C`jpna5OXfyYF|_I*1PGOfE3pm)5?B$`JVd>fUjY1{C$rg9Z(fdAVMcszv;yRNt8yY|&KX9 -qe~H%<3EnAD-y*+)HS|QKO{k9%r$w!s*@qw3rXiH+}%fBo`$$z*H;A}8d`Uzbw(z -cWg@z#=44ebCL!LV4Se^I8l`eq`$RFLNh+Gt_WToO=DmWzN1@sgF}Ld1#@bX%<#c&-x}ZcxZ~)!Th*v%~;%7nrjGz8tzM9ybjRDtONQj@{ecVOO -*p%V$YY93iQ*ADhb1J>43mxuH27$sEs~5`boP-{ZzO@|j;3bjCo-gPoaVjNYuB{v)t`06o}KAmBi*F&i%sxh09;Y&xBXbu`A`sz&|FbTzt2DXd*gm`lkbFL -gD#T&(P$#1T+>BqfPHnq{LjkYxa2VM0a(?b`u0*dw3A3n2SQyD5gWN+dP!*5F`xtVp_5p{B-S9whSq>wufgE=6Tzo2_(URz*r^7wZ&*IYW9FlEz2xM9driF4Vg(-ZBcY^Y$rZF_ -F!&vS;JjoR;5tK#5ZQ^h;7RI*>w>@u5wfHK)6T>Fi&O-Suvrhp$aJU@&e(+W7+xv25mCV}MoR1~wkAh -fsjFDu^%6&ped+^#4%RO8wm>sz>1xNA2&zo*UZQYD8P)5BdNIC?YvHy^o(AO(H9AJQsWyi%*qGY!{AC -bvzGgNiV?pPXY9-ZcozP-q7qS?B!zonS(z8AN!P#j58H>1SNnWI?zQnkt#+uk+ -{OjD9&1Ci;@gnTS{<}H*Yp^m_@J0Y$DiZnT3(PMN7P~uW(DeTr_bg-$Hi;^tnq|J5!(FSf8oCGRMjq^ -?zI@`!OjCRTT$Z=7~wcJG&%00ab2g`8iM8VPlWi0%#D+X4>&0zh-SPUg_Bs6j@OlKKK=pHw>gdV8_v|EuY^fW1Bj7DhQr4L6mI2f`dDRylrPn1NjL9VCFp?`#R4EhwReHjf-8X -`-K_$^}7Hlu~n1(WFQ;qvfwnIrC%vD+T!lf+J$BWZolH2cbd)A0(;gtbT=InCmycKPk|hc+c3V-dXa+ -xKI}BFa%eR}?O9kKHu2TU+`&ehq)F1gi-(Op$ZWiPe@-m)^RMUzob&%6j^8m9Z1Ar?127>Fbq7x*oVL -r(U1pWjjF9!lodd>8aF9-^yF5?-> -?=QnVlj+Ox1?aj|@+5tOy1)q=Bjfwe~voSUwy|?iv<|B}^2k?buz~#eEyL{M*Tlr(d-Wnfv>j8hV2iho7HzIOjEJxnwABkhw+P9zzssD>AVZUE<0`#?m)2~`bKFtwnf25qW?X`b$2c4xr?lQKlhoD%b>#c$9ESQZHGN -T}!ZUBqJi|p&rS0e(Z>Rk&_!AscNel(htcezzT1jM4szJ>&WBhiV;kB#Om6fJwFw~HXTXx&wSgZP -Bwh639?~1d^@lOdCOTzgX`So2nr`R%gx0W}YYiXsWc#3%rStHL3-+x{hbv=mg(&sQQS#%)96z(17B>d -+NNac-wEY_NbS=hqdh={1B^|#ROLs!+{j?t?>Ia$w3B#t)1)RgRV5|d0i45Ao+Jv5vYrp#H2!Y@Lom5 -7{i%l6JGrwKC@7IP=UYyl+j!g7JX8be^e!(T?FO26Z%v7F77X#*~E3z!Xv8=#339nC49FwlXN54s<@s -Tn~DEPH0n5+$aA7HQ7uQPV1#lOhY&;7lGjrSFO#n?BjTl=3nYF(vHsGIG9P#`HPm!HLESAJQZ|1&|TV;k^xg`n7^;uA0SB?rm!{I!r6k46BA{EVWX$LI_mjb>E -(Ps;OO=o5!$1A0k$ez;HEGZ=SodH&D)#G&nmen)x!FZ#scoof1Z<@x_f>W#xQT77VN{<{zdG!r`^dN+ -qCjlF=sq@a+Dwh)a(Ps52~ehb9NA)1%S-X8ACaV3Ad-fwvs`ICP08GV@}y$uOe_TtaT27AzQ(q_q`x4 -q2cT%KCy5fnFmN~Fv)I+*IN16KaUIu;ezrE4PtL`1mnUxVH@5wgR}8goS*`ujCp%mZHO9wv2?<1>Le@ -$4?@#5>h!9)v_ioIs4P<^%#JAbTOaWB;dP>9+@ApqD(DV&vgD -Yl1v1?IjPT^hulG)Hr!C!)yq_YN#}$niGe*m+(Yp?D=&BZr2D+)(OYHi#%}ZdFiE46aK>~!%x=XT@$> -zA7XV8fq0<*@{NH2)V-jHo$|A&q&|i_=u^NNQj^IPksR8vS8h4SUOlBrhzPD_~` -zHlr`q66xnDJ*SRrer{=$#7IomI86dTwqEi_TEbre7%6gniGW@0ne~)=m8>bk8j*1|6Crp!A0TTgp?N -qE~p46Zff#Vd#qg;p9*fxJ?q6@nx8Obp(N*%Kq^<|#9N+)*t){1Sx?8;81pByeUmF0~N(v5KGICYhs2 -!b8EBYJs58XFdj4O36Y -J5nG4<7f_mZ-0Q?;;9`WmOV1aK!<_P6suo#xgclq|Mu|#qEXxxo`e3dqa5@ZdMSn{UEd%izGVv3yU{u -l^{0m8?MD#avUeOoFrD?OSicifKaN8U%ZSTXMWTKaO#Kvi}J0y1Dn}dWEw?^Z$#jdx -7C-Sx9UlZ&s}ME9V}74Rb1k65rhIOuU^yhAL0aX~$=-y!#w+!QSvxle>jJPDBis;do{tA_WabG6Oewj -w!(zNmK(B$71wZE8vS4C-~wi3u)V@qf6Z3=kc0^)5JRs!74$HPNz`n!uTftjxU4JTF0oRwH2Mo`FEZ- --SmgUzg>X)e=dv*7{(g -D)(|tMU9NG*nhSUi2LPc5LfLmfYyPee;BDe-adUExq1CZBqLEy~cdeI`O?ti_#(fe$^`>sePuOcOzL2 -=YW*qDf7hY|4J#>dnDuJxt@T(d+EP4)0OqawdCnV2_U~?augnE*`bb_Bze^>kX!T4`v7Qx2(hv9K`pR -Y@F;ktb5*Ffx-jm9wu;3umJ;JB|*Q0lb154_fhNmZg_>=A%vV&ivxxzMa{! -Dc+eglIKitLtoU@kN^yRGJEMAp+g6G);i_1vJ9H$vtut;Fhg@QXN;=V2)G9Ugf~d+=v;cjxa~kqdNvD*!3^Iai@%0Jq9@Cl@O)04yYuGmr?OjsGy=@XfiUR1 -2TzsOItx0ltP$>8O8E~0nqpa^S()9)v|bu5x+$9Z{=d&T=N(3bYWKJM -yL|rp!aL8oKIb{lxu5qu&+)t=bbZhwsn#^$fQby{u6gPKmkDo*lJi$OQdSYJDr5fIdW>oJU&Iw@TjFV -V!@6cK-@EkN>@$0sy*<*-?$f*UNRlDRGbTDTmgWt#c$z)79CNk1*7SG2PHT#C4Wpj{^wWcW3>D|o+?A -HpX3sl@O}o9(CeLASgvs-kH^k&Q>J9L`w}B`!r0k{{+7jC% -WF()4CZ=xA!li>s|+tR^)wk?_#&#})gX`pcgmIFg-;_&JDI{|I7~kfF3uYUbCfFt=3ekn4vwYkIq7&x -Kjq?+8jp`WNlPy(Wo18%9@ey(@5xxw%pS{LtH+kTUr5|r4$_ull2E>2Q%s(>&0<8!na4jkHF6=1Di2M -=dC_|I4X8`4bEorDscDtXuCc(OAw?$6_H7p-Wk`()_e1Kw&YDN}lyBe11vUKSMzG%8N9SfPsWAaJ^3$ -v7baLU+pOKN2ru&2B;=14_IbY>y96yHU#kqtAtMm(hCN9_-)tCml|KL6MAC)KmOst}ltKKc9)K}b{Oy -!5tyrFblDF{Zr)ibNn6RzwXO(QI&)XA5UJerI2EMYB_93^XBw?dEkb9lSNq?DW1?~k{xvosj0Xenl?R -1MXS(YKSz?_6$AxW)4d9fh7uXZ$Vo#HAj}AP~4Fg+jY*AY}5Fs@ghJxyqM-|&4zQ5DG -pkn3Gp6)1X-mH3re=kjlUhD;Snl34y9~Yg{Ga@P{p+D*NpccK8wxg@Qc1LWq3=d -@PajXSeb?1kQUmmdEr2(NG=P4v1u?$d4r1=!=5?02-tId|m$$4jVdiy9=6ZvuyAdtfQ28W%06M!Lb#i -5G^~!of)$hr{v!BqP9Jvod2Z6sXh}V_G|E(QF=**9{01AU=zFQ*W!qe{=Q -BFu5u+1FJSG9|x74ebu+r#E5-`MR@Ph8@rdiTKjX9Qqnq1O78jI_NK5gZT*E!z|m83HpHN_Q2btHGq% -K9%~+{N6H=9;uilx?m*!LZ#@KmHOXCGMUmgZs{){$Bq_U%%)1zUe6Cis*6oMQEGS&v_9URSGUcf|?W435A%VxI ->AqElAaDN(hJA$oIkN?OXw2Dg!S@;%#cI9YnVQFcH3sWKU=ensO%Wiu$`GiYkjq&Lg2FYo_Exk^4XFfqt%!#GkGub!4nvG^Wri*d-#?d2%x<9eysiy3bJ@4DiA3d)}ir<$7yKf2mUGyez)GZ-mO>> -U&gRpJy*Ay-yBEPZ4>$I;v$r!OW0`kyvpx)euvMpmE-5=kRLpf5jhD%87X?U<{7|i&ghz$q&gwHM{;| -fpxwOV^cWP&C?SdCq%RT%wdodB_3h%NEfBgBQcRvYelMnlE;l?5HV@%-gfxqqJt(sy8J^epnK&t&P4CP-TNmOQL*kd=>x05T30LNHwl2VBDDc3cLJ5#sM9$U)x -gv#A5O)b8alyK#VD~g(2*i30{#NeEMFyKmnvk-tM-Aihf=GiWKw18V91Fyw(dQ>mXk@H(A|cH3%nZ|8 -=}SztsWFSh2tDSoW^!%ea)qYmERrcaip2Uhk;YAbd>gH6E|655^ErNU!Vnu2W{5S1rCHb1(Od;&V7k~ -uA$Fv3{zQoMvJ87dvq&&8%aaurMNYA>W<-%&EV&uUZ3sy$l6xb$E95Q+Q|rmKIg3jz{wZUPLMdddY2g -BT6-1qW*@ -wb`W6mp|N$WX|wk=%}vTLb9gB}BzHEsESZWukznn4^EJd|u#9SjaT=P>LV%eO&zhVe+UKD(1w!^07~m -o;azOK5iP#Y9#`RHFwYKtzYr)nK@PCJTi9f3{USvX&ca7B(Wh3S -Hwfd~USFPucWQ;nTG2l%X+C+8>;!UeO>zZt>`#cKfu`nk}b3d573u9R2S@%wBjajGeq2EjV_Xqxak^g -?re=qRg^Zd7EPuh%Gey6sv;0r!p!!$KiR+9((r(?v*rYA_^#X7e4jHmcclc7qeQHuPgkhZyErfR!1hR -XZ6_K=uY3+3cC`UW|M->#D`@7%ycY~~>_)C6DJMo1K!^xrQ0bA0}va{qeG|2M+l=<|Pr`)}3!m)$@W> -KHs;*(N4M{So}##XLC1OD)_~!c!f*8G*cXG49>lgfh|KJH>?(zuYGFdKPA-e&o8DN7=;VMlRXL^Q*>G -+Jq9iU{$d#-M{2 -Xx`5g?H$>&}%WkY{i~bq}vXH1>i?`QwAFB#!nY5kq5^XLm-$dDY#^mT=reHwLFy2X)QB2u(yK(wNoc9 -g>xBINh*4sAqwxI*^p>kstM#u3#GSPI3t*_{Gt;uBu{wXdWoT3##W&yJV_K%l2+0Dh6m-YI3VmzeLr6 -GGoR0fK`zd=L6kWd4fsbq_>|Zcq3qS@5p!#OKZmYF=CAHU -U)>C_Z1#3~g=jNPIs?GD2#nVzf3v-=S*O)zTW_apM>Z@rfDO|S%UG~#V!pE2yChS)0Q}@)&)vXBd95Z -|DWu9}GkGn&x9-Y~f6LjtY>am4nW*prPOeYX3PUwZK;%+r2Hj@tIDDvpaS(h&oGEi|bmC -%|Tx{=`dvKSBATTgsA6YnwI+Fo2d8UThALZNphS~nyVIRxZ6bx{~7NDvQvUp>ldqIcw}u+6LIoSI -93pXgl~Y^^@5d9+G5Iwh^{Uer=-Jvn9TkiLLT%GvcL@Rz!HxXH$N`AZQ9trJGC~(JnqG#REwv{M)FM;s?NGQp}BY* -LFaCEfcH%jTk8#QWOCh>ogA_zS)&l0?P+l&^ZceuuA -m82qXS|Gb2N>ujFA{L7LpuvU5QMcU(gU28L*H~5zqS!1k|gdXE8+KWW1B&~gi)R>)gt9i?16=7|e*uA -weD)+R~f27=VeC3`duJ2KHl+kCYExbHYj+RU(={o|v%*Ygc$KZfOg)*hMVRSKqOwt#o5@@$%q@JZ5@yd}j-o0#!pxP{%@k% -*ai>^?nH1tFiYsZt+( -_nRVOGdIM3|e&+)J2S$Q&iiw3V*}3A3KeEiS=lGM^UaSTZ*XGgrcSRG5d5xn7t_Q?~MwFsG1tn=q3KZ -{<;8wvu_XFq0~8WrHx!C3Cqj=aAVY%!|pqSeQwzu`*wn%gCH9%;jV@3-dZMrwH>#G7lBz&15zT^Fw6r -Cd^yOtQY2OWWHD`%7VcHxSSC#i{Ua`xV$G^9B|1JE(e558C)g`m+ekp2L#8_!toKOuP1`z5aC$k^mR#aj1!J!&Q`w5bK ->BBB+b$Fd}eN5r&A2~wa(1%zt?|ee!zxR!g`jz4r>IgkL3#C3{ChuuZvdRzE@ixyn^Zft3R^;WlcDo< -$0C1RN~g_Ca&)mdfjgKY^z!alyj`->S!76{&ZvOayzr8TWGQ7ndRN?a;wMPmn6Cj!aqXpxP4^>>bP1$yb*x{T-PE$)>MR;XZ4s+-OXWocNW -)r1!I(ZJdCkqjCBvE4LDlhyiWT#+J4ggTKqU|%kK-?QSw&sYi_mI6A^7S)LRV)Fkum?cUV3|v|AKXYa -vix`-c{n;UsmfVnr}VYp6vHPH -B(;}Y4rrG(_7U5!}E1sz2W5p$}`*erww;dXrP}c_)p4$r>&Q=;A!hg7QE`;E(?CWkOfaRt@jfJ@62x; -euX5#yO1P!wD5Uqg7U2p13&yT#K7B?15cv;wl$|4#Qh;l?~r>;RWxAwnhf|uLI(T~clp-QNq1=yHJiCa%|<^N@b^MU{^$+$Y_oP;zPJV9tm_UrapGRd3}GA=WCK&O5?&;7LZUqY -n!F=B&NJvFWb#KOfNF7FKRbuZ`E1Dx)(bMr5KIl<}T}!VR(7(M8oqN^tD*)ULmwzYYnWQWO%+Zv=P3h -Iwgn-A;*Ni%1}9pzS()|Ewp;h6B6i^8>B>fvV`AA_YWo9B4K4G@i{Y|NC5>EE%}DKOX-tmWpRL%R!`B -S)oHJhOREpkq}6*1X?2&2ORB%E6O!uPxukleCaF$BW=#n}Byk>+K}SJpw?`8;A7t1?B73L8OS}AbKk5 -^$he*u4>1?17o158mI*=dU%ibWW#gNZcwdpOA3O@G=m-7xr{4?Php1!8kOP>8Zg?xD(rGyYsddf?v5H -HsfAZh4Ofv)KUy8=^dOxIwGVf$39x@PfvF|0Pv9wlXvCVbul_~vs_V1sG>Y!TO -@?yi5GDFoKAlfmQXv$XW`L-Yp5&@V4G+Ps1vmQS0HHwqTy_ttIz#FF4n^U6cyk;FjeVy3zBhnpisyKDdl={8NF!g(MsQE&sdsoLY-F@4Y=?>6Dx{vaaqNZw>kmxpZiEi4`w^VrzmECoGr -5{?Jnj|FtNmm>^_XQ?3(UlnPC{y{xMecM>RMb0Gl18Z3s3hCHYEY|0_b=&UIw8@G(j;kaub!e(I5a)0 -C(^c(K1+gmhLu1ON1rk(p9xX!hkT;k*?ywjeO -h>p)w^Qx8+3WpB<0C-Lgtw>W2S3SmAWd=>iNcO*xuP%^Q&6^3C6ZnD8W|`Su6GhDChgh*5FR`%-V{%V~%%70%~|Xp87*VKpT8~(mTZDIcvrYBhB02N39@KJ6PZzfb1%2# -zBuC{UxO85^x@IrUK>SbTp}u6e#^ZOfYdC(3NN?rEW$ul(O~t}sY_h -|!K7e;s;pCo=@r)q4+dc%*$zDYey(&$_#+4Z=I}yYHM` -vPaOv<5pY%)B5kPlGGEe5)A7u-l7t~rE=_xgj%r0*!t#K-mhwQ-+M3A*!LZ2zEf-}y?VX$_qQD!+J*+ -h`I!C^dH)G3aoH$#aP+fw{>E3zz?Xd9RfJHaGo(2jKGG=B8;HnmCi_I5nYf(l`RH1)V$QHTa~xfkl({ -jujD&E)#wSpy`!gOmOwb?=V+a{3aQy&T`%Uuj`x62t!cQNny}6y+;NxJ*4Tda0+Ou|3PrAjDF_(OMHK ->Jvq9nr;VK8jkqsH8yB#^Bnt-J5JLenbG8Y*e~O^qpo7dS9C!}9?xnk<<4SK0#TsOwonRV8|Y_2 -Hvx%g=`yHr3L?3JILOp9{S=nkrUw&t@*VdaQfax~%wIp{aBopZVv*iwMi{X3&CncsQ@h$rd_ZR2OTgyn*+0S+q1I!7XI2$Nl_NdOZzEb9n}ci5AaZHRi72f{FAA(DS`1;fFv}9J=Ldu3VbuNJ+o{g*niJseW7r;O8Fj -Ltq9u&PnhkC@dGB$UP;bi)B2$J40Z3gFn9R6tAvg%eh*>Y -x=v(Vb6)MrnDY&5zxgyftTJ+);rq-9wbVyuTWcZ{f6Gg{juZiz>1J_WAXGe7OpYi#+Rac=EQuIl14(> -CAhISjTk|jtw5IMeY>1$_f@kjwoSU3D!&(~Ao4x@*s0<8M$Fxk*ZaELwQMN{sUIG%E3!nma8w#qgGNH -g|Y9`JL)&&$)cjDGH=PywX_jH^?+vcCnA!$mX9NI2*oWpB$SLQI4au}_X`Eu~D;*s*3oMY6H)*K~Xo} -&oVHl23#kzRSe(7NWrR;26P=ij2KH9HdO`Spy{j%OhSPaRkN{M&@xqmGb5uzHPR(YOl}hzl)zHAn6Lt -YC&{ig|BXYc@qvXp3h*7&P=Zf}7ym$wt+>26fxPyk+pjv--5DElh3-x0$E0*OoFZ19I8ejN*G8k{;xkhKKecv$cpb3G}xhvPjja4GQS* -wWxjzwR(>Uuie5wA|wmtAG`7)Zyy&guycH{kv;duj=q>Tz47@# -igH%!Pdm_5~Wa>`V3UbiaAl={_*1X6Q=3{<^m_*E@w6bcm^d)ncX*9M4)n5uWN=B?p6=u(@VgUl(5bS -|TZI}j0u$}#-hzV8mQCR#%)PcGGNGPr_Ln$-vJo$9?}<|t?|)zBI6{qo>bBPYXS4N_xF1B8%ZC|%F+I -KQLmKvOve(aY|TRRU&eRzw7?9N6Y(j+_J~qr;`@>28$TW2 -=qS>tqp*r`m6s#W@vAlMyYJs}DxwK!1XCGIS2$wT%QUQk -nEjX`+Z?eW$sA+~8kB&~$oy+v}YUalzzhWYlBrH^#HFfpJ3>Bm&?QFVUXQ=u!apw*&W1Pq`o#olc9r{ -^orfAH$-Y66Ym}?g6_#sknpqeh3w9q9qX693PH73v~P@_e1`}ciOA*_@m3N&FKysXuuvTB0=w1KfH2_ -SUGA3VcfP#*CNx1RH2T7_Z^%0NliDHE~+8$J8$8iUP&ja;(SOL`OL5-w!?7Q}Dr%T>vLYN(=nB-H&)U -UWm^&VtN(Wg`jC^TUv&{luC__uD2TRz`JDLq+gFn2eZzHjtPFXG~pb&wXh$kxAN@78+KO6M<$?JC|ed -zFqfOFdbX8<{grPR8oQ}!xL1y4>J}WM&)@wP&paQm&~5eDDcj3Vvwnn_pNOP7e1T`)`aG0ROWM=<)bTx#d35WA=X0~?W4b1=vz1O^*DFyQ`8FA?zB -rF^s%qsGf`^oE?*?n%a~V;Ry=X|{M;-Th-oheY9%z2G4=P&sgkxNSv&MSKV6EdqC};=fx*ySKCcnqi{ -a^n3Xru_>jORj@=PS<#;!cjjlGgptVyr~yqU9`4KN|_Cl1i8t#`DiP2bD)NX_BCDnGQP9nw8opw1yXq -T^wNbj4msoh1w4$!>0Y#)X`4Z+$E$H#}wM&Q?T>1(W^%2zi -2g@j8q#90%*r0)eIq((UUpz8@rq_E1UGuG-vWV*~+J^Fkx4e@8S&-uac!j4`pgK(IYDNj?Q3620&O2| -`GpO-IJ#%$w?EFFrySmOaNZr>H!N&agl3$W2$CLdAK18S^#<<6Hl(vo5!dx08{ImKv)AjECtpC^@E5$ -Glv=s^)^3Ryj0kO0XBtEZDK;k4CrDdX@DbODmXmUo7NT3#mB+ej~50~NqKf5q^20EzdYS?0vC$%_q@4 --@|4v!1&kX|G=dh^kzme9ig5{A{lb04AK#Y%P^iJNAPUBV2NYk2{j!xZB?raYcS{hjJ6$QoPp -9dq?YkNzD{>D2xF%uh89|LQ*O=~mF`Xi7^+LrsIB;$F`5NY@hDHsJaGj`EFM+~IYCl6kF^aG|lR>iqA -Rt$Mh3zJ-?8^!2eyT^?}mqngP9srwBZT$f0awYD{ttkhaV1$}Q2^@jAAhy)CkR@#ToJCs!&Uk~=x>wy -*wZz>`!^_XG1+{C=$VBX7oaaR-3$@8|7`J$+Gpl}xXcZR=>y0b&pnr@wxc{fWRXs?-28v)!zxzS^_TW1^o -kq$+`Ppf#(a&RDljV6$jk!9ON!c-X$=Kj -`5KgH4I^*DtGQnE$edE%QFh;=UE1eUljggNM0xBM-o>gho4Rv3XdCR+f!?>24VuRt!Xue`kW! -!*+vbTh)gV_S%jD`!q8yVOt>?bUNUbe3C>FoOz2h?se>+CYE4}j6e%W=sNfq)P{yl{2EV_#BD~h%ze1 -zH-LK)tpjMzZF``Wojr30N|lGC4$Qb}LZWS!?LrSO#Kf@$@f4YvTgjZ~R&ie{5=8gE6Xo^XujNO#roU -Fc`8qFVrHlcQ!1T64X{vtNyQhwGQ5j@&&$xuwu|8RCLM(zDT_N-Uw>_61s;Akh!`o+lQ3doofnT)$;8 -*!IIY{Zp~FkFahQl&p&LzNZW`vKIG@P9=; -|5ynbZSfvDbZt)+gLAOFgb$GLRIFQa -mw)U;uL&2?jB@bz*szPG63B>}wOt^I6Io*jb9so7BdAEq4#_#=@r4ul7c1{t@J_ODVThz2^?$Cs@1&) -4AhuwF6l#W5GlO6{udWZK%n1SC%en{r&|bze>v+<_Ug&8Tg~~-;dH4kU&0ti4!{xIb4O&*J8WDEw&q| -DFpP32G`c}c7DJWSP`m9)x1k%6#b&sW5AmV_$({m>Y0_s?>m2HlGu4L(S_%RD*8lX*ltPLsWN`I&eBX -8A-j;!S0wK`sGWVb4*kK?1zLxzJOLx!QSNDbOSx|x&*6KXz?4K6j$f^~5u;{7e-IQ{K%ab9wiqhyZT1 -(iI#<_q;(H`aoqiiynBC%ygzT3=cAE@@xooW;t4;bb8J4$6Z@UL& -KZD;Uy?d>@htizyf19+}v7@7ZXLr+m(ns=r_enR7QqS&|_et-LGF0+vM%%L|_!kU|J*#pZPo6H7ewmI>7`E^Ab=>}ImCJR231#lCEXDX3CQ<^gRf -jQ4`D~(ZIyBr`Gh4@%YSt?=XL2S5TRr=|u~+EA?7vVbq`mTfb0lUJjex7b7W!w9?I(xQyy5`u1gDwB_ -nn>5G_Mc-dLeJAR!ui<gGM`gx=n -9?ZCTPpePG->FkTS0Se7x97JBlO$s2C|NRgl7DRYBq<}?;-xq{>Bypzkl$`3@c6JtujFoRhajOD?d!a -DyugNQW>RC@KxZ(n$jQna=F+1ao^LPiPCoYC@8@+nhq#csaz}enm9gX7#A=7Xx={z#qy#foYuHrh!C( -97$F)luPnxyoMa4$ChOT5qp3O*G6rLC>zqqg4tID|>ucWdKSGO!|r=%)YCNbA23_*^HmC1~>$b`wwkF -1*D&u{l>k)b^hp+zQ4QBSv1u41PsrEMCmUcDD~$bG(t@=Z9xPYK6JI8(yK60VT&*Ao6o!W|MeNT^5{HBdmKgeekQC7dl`zJ$dRu99$*gbzvhq=dU -AY>@Cn2|t(cTM5}9SzZ$MmvFd*X%gNbVUC2iOSnnGKS@|C;c*E+myk*R_mr@|gh>*PlQ2udUr1Oi;aU -kllu&z$WckdNux!BP{vAE1uOmfnjK$joROSizezkyYpJy9|`@z)q{o8xogMXM$kcLYe-i9tZ)^I`irI -$D@WreP6XGxyDw3M+Wtd!-k66R!f{0(RMtccN7LrG>GPp?73Jp3Uzo5g0bNo*KPk!fl65o`?0Viq3_v -)BzRgQc^n%*S;*Zib^6-%?jKi70GL_B1pMMz5m%%*)m?wAOi`5Kb+n9-mYA?)m -(3!+W;g4K~A&d!Bxe#&*a{wCgmf+Wo+!CEOo?;eE#HD=hR={yC;b9jeN2M_3;)gJvTe!V=+H1Z-Dx?o -mz1^U>tb&)jx7Rm^Fm8kEON$H{4NUKZ2LX|f@1Ab{L>rp6ZPxLa<}{OJso&v%JOmgd<3UUIQOWn{yXYNTfQav@}beB@vprc7j2pxYqSGIzhVR;cvOJgZoc?&bYcIVq|3rFJYxDof^hW#AbMO))yh{<@iCAuqqrH}-war$ -H8ouOzg8l^@tDTi`$`fVH5;iJ1trV}Me*_!*qvc(K)a;UC(Y{NhKlO@Bd9AcxQJ4JjUtXVxD#Dn@Da` -k)I~|P^ZSh_E6Zn?MUa*k2L?_3d@9QIO?Jyk;|36cIE>9_+^FOyuE;RhD?UZ<3!t2Qp-rp|fV-xiRb| -240E$O(uCi#`$&!?9{h&oDTx`f+Z?b4g%SAIVqe=dF(A%upxw4D;w@VEB&1-y(zYZD`u1NK6W!N^B92 -X}FDyGD(OlYa$|0whekDCT2&X>a^q*h8hgT-vpmNzxuA?U;{od$hDWq`ix@8>PLgwAY>&_E>4pmi8VR -Vbb1HS~s=`dz^+(+Iwl~pA+^z(!NgG`)cW(6?UVRp0xLq_MC5py-eeWv@h4vllI#+`lS7$w6=UB?5ea -MlXe?#`%XSDq4@@t#cV#tX}w-LOTeEIC4KpfG7F}Kh)K0XP!Z-=Zo~N>=KCmX!oG-yTq5Ti0t4Z@mDh -k}_`ROTYnzJ2*UI~G{MnabyUxciEB>y73%Ql@nX`EE<=;>~XA*x#pUZOj$GCeUcTbcqC<8v5XP8mgeE -7_~mlE?%>VdT922i!M9rvkn4yC!y0CdRtiGN5FZ;5U`hS8JB`@iKrJejheWWMkYKHDvlb{j8;JhWx;O -hVX&d=A%IJ^~NTYFb@Iel+?udNn#Vx-@z;Iz)QQeROE)Yw2p~5T0B8fV;-g+!uUoR@x>8?-%EvR*Xv1S9!*V==|F*> -Z{6t~NMs+dCDr2_i*_SM|`zpi@B}*@}Pt9|ckadRL;nF-z?nUlW5S!^P;`lP%#k}B64rd{ifi2gW=PP -IXLZ>^|>0?-CZZWqaT}q4a6K=vu_y`x`FDG1thvHKlHY3;NcIM}DJd>P-(geONb~{UbHt@@yUu^etnC -TWaTS+l@oZ*%**F|_JKEGsgwOhT42)4jZ1BG-BkabUXOPe?OOOgCF5O}1uaPRX*}Fg1JH^cgc}&7L#&#+&BNzxfvn -a&i~u<=YDuEiSy})}kfFC63#ir7ri4cr}9sK-SFpc^Y{DNS(f ->}-2QX(+nVpTF8p#i-&4M3ZAE3(oxfaHz238-=B~Rp-gEDLn|}4{-)#Qv@9y97z=OYk=;25H@aSWYKe -6?XfBN&2fBEa*wmtRqGtWNv{PrC?UwHAQzrVbzwr=;Hy|3)sUw`1$*ABk^#-WD8N8UX8*4ytKd-wQz? -|<;&M~xq!_~hiNPd`(dKKGtJ^Tn6VUw!?}x8I#T*K+>C_ZNS-r2dx&gpND>pEV%-Pv`%CI{kn9fZVbF -{|f%o_sVd;_Rt&lAV2%B+t}%fP`#i1t~U0&+t@d@vES3ies3H5eQoS?g{Z&%xi)tCj81FfTD;U04&9{6YFmby%NuEM4E>=K9D;d2*m0v)pfbM7 -Mh#F8=*a~5XK7OdiE2Y5JE7g_kDX9!<)&@RZ;!d~nyDuS^S{p)C*9@vR>3Wy4_SS-Qxj84M(ivk)`vg -d~A*|kYYbFxD)Tp4+G^*M`k@L)mgD8r*i2o}QhdOd9s=vj!ylTPL3cb2ayM*@L&mfyLyyjyTEQmCq`L -O~}bm1mc)L+BxcQ?lojBR&$z&L-`K#v$eF$`OkpnZ=72>-8aJ4#zTw;|l!r#pMRP^j#(%b~l(hYUJ?z -BDpYY<r`~v+E?38&VrBFgIp}vj+1g-xkW~c(^=v)78V<`a^1ywi|rI|p}?1 -0SY*#Px=M^C4tufDUS`j8yK)y6X)DQD#bhD>0u-n*x7e8LK)`&&p;ZP7dU%^~PJ3>?vA|if1Th4f0sU -yLYyt2a~CEv3+4K9E%Ez?bjIx7L|60 -XW8OHm%Y@Hn`bxX7P#zADZl13X5}j;iAY -8W7uu(5|`bGvTuW?J)*~fx0l!b1$?jH$*yPfS?e>?FBySTtlnX-9Jng>9%$c&pzNAEMZSZdc=g&qVEuW7ukp{lf`K5(R97Xm8MY7+VpI=z&n!jvJ@^DA~LiRHr -!X4dJ&${MEu&&ABtm||`Lu75Z<7A51yPnvGF+1MoMiKrNm{QT$USX_ -PzJc{h?#y~QW1FvP?AFkwHj3k<^tz(I?bn~Nu#Y8<9$~D9E0Xm94LxRtvmVK?J7a2l42vAycdvoPxjM -7%`H`S6f*D4IIXg85H3VRAjm^UtNb$phSQz5K$Ds??v2f&E*DZ{7OE$1>Bcqx-H%2ss)#@D~Ii0ejDX -qOo!v&ZhinR2s$Ml#6jkfXxQq+KcP0|+4^A*DLrG@zcLrO>@V^)p+*f16g+G9a`Ea-@xF7m4j(y<`W6 -5Xv6>z1zvJt0Jo56@plGWOEQRy;pO8Fsq4l%VSc2A``Z}wL)S94@?!yY5dkbh7tM6W2xiQWX2zLVccN^K&R)$u8)F-;s_o{`^@J`-h -*a_Tj`wcMi(s29kO0r@dZZ>8t!35<2YeBypdPmkfDtatui);oC*>#cj4b*_zYgv)Yn43>Ee31A_*`+0 -od8>442F1-06EC%_H85vz0eWjbPj8i8uwjA&C8Xu^fCbs7PJp6u`B+D8wx_cPvAL?LoBuqMWVmlklYcL|Y(S%1w-In%9a -eTJ}Sl|BMBU72>Ut{<*y0|d(DN)EP`WDpVxO;gn`E;GP*)D+H^(gaD)_o=*^3i={SBK -VL>UxE;UXR2ycWsPrFw{mmB67l$!Vo)}@S-1xLq8ffvU{^A4TCTL4J9a7VA-nC)GM6za>b&}bVr-Hiu -KZUVImH2QT+uT)LU8G!g4e`hh11Fxm)q{ND60tlOJb&bz9iwy`3nRh9Ir3pnuJm{p)nlFY5}`wa?e0& -#OQmBXK?j`()U^(rE35{tIoO+ssJxV-c*|z3AVlUC#_dyAI{;TJ+<tJ+en*_lB5So<8zp0B)W>uXB1{=L9XF0sU@|nIWL -16Sa?8(O2UIu2_VP9r@#Z)K#0%jyARSQM&jr7N6XM#edziIkxethHkZ89ML&Z*}UFQ*Xq3(i!>ZhZ9( -6FH$vu9t4A8_(FbYX8{Qb&5KK@8J46A3m`rw>Dr-J7Yn|Dud(23Ecgl@I -z%1JM;*MW-8xtw@Z)tl19g67tzVrU6V3)E-xfGfR}k19K4aXEY*)P>zJ7YvWu^grKxfv)$w!tP -9}lSA%6Uc{<{5FQ-{=!WnL07I;q?+P^4kshr7>L8{gBo<5AUx~-!SI%$oJK^Mva~?4l!nE!2078|Gv9 -3!eZWo^f2FP^ly)mhZ#%8>yY7}rMO2J+o;9wpiO;(FoWOpYg1I;Q714bY3|Y()gW>nmVH^o2t}N}5v;E(n)S_(LS605` -nJwZvwK_{$p*PDvO)RZvq8xh*dX1vtkqZG*cc+m2aKUR`m}C4Tj?xsGh;dZTHV&cZR^*p!{IG5v3(q~+v&toT}w?LJSVr%HL1ioqp)~UkzHb1f{?{_Ct|0U6qlA1+0$LlA`6!Gm^EF+Or@ -oTi;5+NnI)|RuzcMlPL|x+M4F#cnv}itl6<@G1boi`xY|nc-9`3Eg+=xoa+grpCjzI@&S1NFD-5)`i( -G{hm%HpUOXd{j+tU~4I@zye9)01k61tUe&v4~i!9UuY)Cb6YY`#H?(hHFL;imk2C#UB6z^szoe91RpN -F@&YWn5r#veV2gw-D_T`E-_ER8p$hrdX!mV9C;Uem#uJYr1_&$x@!l84i2i9A?qYa5?dxf&5;_L!hV^ -@?vH)tXXqR*%>@~pH>bD?C^h$PIM3(OA3o!py!9Uy$#2oP2pKFnN8^!i?62`Q!O?!N1&`gWnn&VqiDA*F)%CLhGcjhLeBOCP6u6_0JehY7j;swi(Me -B)#qNpYSa3`zfvu*@ -vXyH!%U|H>W^%r56`VR@;`zJxx+m`ZtGU}CjFGm7kX2fJJLGvqOcy)ViDwFqOHXj!7K056~Rvx9tu*h -KvB>yK_m$+}Ay6RL^sf&vs8olr#MbmThiK91WdmLF$k=Q;~%qjU~C&QqUm7nLJdgv*~=AWCOiK1`Y`PDG&JB=^I-MKPOBgC)nDh(x^SesMC1{X%qof<1v+p8ba_=YIM@zd -&+S8>yTgI6#-DgNRN8-Oxrd=f6moPfH#`qa!I%DsYw04r=%O%ZgBwQ=~E2Mv=#6$Y?=tRPU(*Cfd=TV -v76ViRVr0)&s|Bm!0y~PoJJoD#!+j97^=U*NEAC3R-#{ZAT|Nr$NRc8M7?1zP%#=i>wd!MKdAK`E8@c -Fls_TTb!e7I2fcYOHmf0pil<@5i09cc6XAFmS4*UI^RKoB3^9>oMAXYKN}{iOfn9|`#IV*;-EeIpk+O -IuaW;Z}sN`@``+7(2kf=8}ME5%MoFk=+PYcNsfzLUn1 -uBb?vPN!zg5~dOSn!#hlDv2S|v=9FkZr566z&vyd=|;@V{%`6+iy+eS`mP{y)Ww#)%!o4-)Ph^|HLfW -WA8-HBXT3w$qQN|9|EADS4EDHcy~@d0u_?iw{7o%2>pkx-mAikB*rEhs9v -LfO`_){diMg-a>A89SB44i|#-Tb2H$@9_X(ThG1E5^j&Z#xVE)VET1kL2@iqfz4qzGH7RoQ+8oU=_-UxUPUUn6D1sH2V`NB-__ZGq19e -@u`f*;%;0z5fM);++G$*9|K*8>imENGYuxIP1M;r=M#I~jtWlYpU_vfKgd@NPnwy@1v!0w=+uDIz?<- -)Et(qI3cK*@SyM;38>W4CuT8c|jN#pyvjGe*@s&8w5^*7p0lt$f?3y3#iLRKhPb#0<4+NSQgA10pFZ1 -@EiraW`-~i1>847#H|OMIaBb5pm{dJATGg6csJ7xv~w`7;+>B$1cR{zdI|1&z)vx?KL)b`7&TwuPrI4 -1oxc$E_9eis3uIpbSdCYYwy*&(HwW`InDcW;K3=quEWk|*8QXw*Lh$K@qMkep*ga3+p))I^^F&%HfWN -}K8EG{D_Q}Uw5qOM%f3c&T!dwgZrvikB`ANV_i)8<~n6UvRjJe=W_mNI;AYGW#01F%f|6;%|9U^=);O -0i|IO~^!Z*u`Z_@yY*M!+rWL|GGDP>ptqFgbv~sTOH%27I?#;CvtO -TWKb^*CX%{9KS)}Ndt7$h_*~{!rh|ZO9LFUk;WC23*gXuM4wFXBfJsFy8>8nFWMl?ivhjTd>Zh!`-Is -Ac=0~LD}s;Hy=*Yk{cPD=kT%Q&`#*qs33EK)><2_WsRZ2nfS|t-u-n6!%Oeay+rz@$0T}xT#znXjG(L -(rFcW<2QNfd~fXf~e<}$z?kI8-vaP3y)7vU=bd;Ssi8Rgpx@b*6eFU%_dFW^ms`6A%we+Atzp9Y-!H! -*e)T)0iR=L5d7O{CQbxbZ2B9dLgLaM9B!cbLloM?V7^U`_#C^^6>60S`YT>ex}h!e<5FiU3o0p+3QV9 -N^(yqU??WZm-1{3HKd9_{+jv(z-}Lkx{pHl|-4Bo9UKMVMFqrgdWBqRs25q%@U;nGZy-ICK!1_nC -T4tFli>3F3ofn-YLxlYowXrR%s?!C(Q(#q?yi2(-~hf6Qr}AL?=OqbSLPMW;)CHXK5z5SDOFx{Ns<1X -%6Jy2QfV@@67nOwQ~AKxK4ad^9vWk#GVQ(r*+p3K#GTERZjj2AQv}5w?T^*3|Hm!?J!B#7LSfgK3dyx -7kfxoy3pF1X+=2Vn4%~Ni;9lQ>`>_t(m3G`|pQ{D#`g<`(fq5sTFnRMP%vW`-_Sj#eo#<@hgWn>TM}PdxDi+qrWmtF5hN@4WL4yL9Oilb^QAd0<$Y$ri!xK3E>X4EvWYJH -)Mr)GsfVM_gRCY{d!+@TGe2;vx0mIe71fT@TBbhzt9&Wn@H4ME6KEm -(4^<c+tG38(^n{oX*lx^gBunhsu_4C -fzjLI>|^ylOeq;SIzag4ckzuS)eiO4jwCs^`hc$;@W6u^BUFu-k9H{jxf~apOkzr$7CPJ^SplypF&9_ -S@{#sZ+js?x5oVm}5D%Z4aT6xWjRW`h3AIwtCl>y9#bCVXIL@1-n#r^)BxM)P!4CfAQUd=V4g=;Hi14 -`hCI51E*%qnzgGSEBViW3vSK4ZK|q1w?JK0J$To=Sqs=|^$)wAn??9`-bMIVtLm9^=e99ki|@d^t#4m -7Dx|N<0_DqoEL4rB=l2`nioiH#pWo|#N0mXVRcc{gp^G&XzoYVR5t&0m8Bn5*_7idoA!~)O7YG*p|W|Os -VuKiWeZQMY|WZAth&0I-E+@9>^Hyp4g2kHf6KOP*}@)r=ppvlV~_Fj-L`ETd+xdCcwhF?OE0lE_iSTp -zf#$A%_@84l~>q-0|(gaufNU?A3n^E9zDvAA3x4M_}~Nf<*8%r&97DV@y8$YdePL>#J>IVbN0!3l{Gg -vvvcRp@w!4!KqAHmtfxtaRFCyc80t+p8hd9nV58cFjaNsodFnK_T)mBLR_|fYsZX(^>S3)vW!(_JKjJ -4L{&>XCLi`&Me-YxBsnKjb;%`CxzaoAu;vYf$lYa3BA$}U---P%s#J>yi|A_dz5&tmaA4B{P5&sj!Z$ -kVp5&x`T{KP?6UzkwWu{PgGYt_3LJA`#g^By$9BhgGfHiD@iOk?WF+nDIeDpufn8c{YE=;{1@#iD{?TG&y -#D5;~kNU-Domn>PjyBtmZDa{(wqw{K)cj_a!PMxRm^#A2)MC ->n4`N%5!WZ&H5iL&e2x`Z6RuC=gjzgxwwt;d`K{hak3mCZgO<_u+kzy9^F+5Pw5&mMT-0rv325 -3@%feU#4?o_XdOJ|BGHg%|i-;jP*y*!`GqY{Oh(@7}%ajW^!lbAyv7K4K?MoM0zUo@8FHmz_R+ntlD% -mwayU-FM%yAAb0O-E&T5Pr-ipd%u1*u^$FH`oMyAxiJp|-Es_ccVnP?oJFh8vJvXbY?@lnZd2c6_o(l -)r_@szznlHyccx8i#K&N}aVX-CLHsnt&qVxrh<_X6{}S;ZK>Vi>{{Z5*j&uK+r~GH0@_%)nqUmYx*|T -Rqf<3q*_nxr>6B85Zj>w+Io;_oG_PeHUROilpP|tewA2M*@;DL$JQHHA{Vei>*z~I3M8y(d*F(EO5_3 -D4kki_0$iO~qqIWn?OuYLoEB!-6#=_4JC#v#`v>h%#r5a;SHog;h3cE2VuT(1uc4GxaD+FO-9FfOncd=aHL>_|VMSGU0f5)u;=27`X2e@#L{ -d_qFM1fIWMc<4-Jzkb}xRvQ=_N1Jeg^6hAwROc6l!qdV2zG8ofWc_n^5aW;bvp?g}L=Zapy}>fo|BmO -M*(4@<>BxUf-@bi`sq~V{zd@m)q3BuoRg|R8pRNEvPr=0oG>-qPoDgwSWJ9k4~pc7&mTQqRdmQ#6uz!#D{-`e^k~qFn -1X=XwU#a;#KFL88>#`ym=GG;DI^rop;{38|!Fw*REaaXP8ceufD48+__T)ed>-KJ2>5^PMuOe{q$3{p`k%NeE2XAM{zJmAor7q^ZTcsdTQn -N?b}yk%4VsksF><-ILvq5byqg#)kGVKvm|uy-hEVcb@fdnM~)=k#hT6LA%yoIrT?36zELrkxo5zD0i( -ggyFUN?bG5d%*2m{VhYqRlzWXkx6XnBcCv#(Cqx#-^?{PcjNqaHp;ShE1+poU*ss;7#d*D90X4c-edC^|b?0Zq>z -SrNK-hYPvJ$v?SKsokDov5Td?B2bb(@*u|`0?Ys3_kw&V{RuJsZF2_bNJ?)Z}OMg29*avDmQ976z8?q -UgPvr8KV9FzxK{GI;twmjOPJfP_{Up#A_+K0l|gpXYtm;oC2V{xfIJh`J#o|B;cAHzLPO=s$4afNb5mRg~wVF|*NO_8)%i-@o5XXy3PQpV5`7sw -(ps4A=nV4LyNJ(6;G&k)%&VV%`zCW4}oIy&|{n7HPLja -^d`~1|M@?&@QS|@(^Uug~L5B_n0$#M*!4U;U9VgLz`*oPC8C$Q80-aiHVK8FG(a*duzu=>Mh2{f -eQF%KzOT1u%506zO~*fFVM4;eTEgX|wb4=~r7k|ArfG=%{=sM9;7-p>QDUb;%d|&-?fV&cn{QNBmc+q -hR=4q_<+YOEFX&m&S}AbS|lj{$GCiMbyvKnqaWg;t0i)P&m$=J16hGyj)gI?juXa$H;TzVhs_4{#WGT -zll6}MC3j&eC+76%1o5i=lubF#wOjkBcRW1w*_=sEpDj)<7?2O*|l(lteX@it0#7o62-7wF)$L)JzVQik-S=LYP -BEHAE%Gw;okljsApFAcX{Tje9L@_+_jbd==b00^a;|>OFQsjYvf3`O1CdF|5){u6|(68}*LC20A!!<_ -9hkooLM`Q*J#5@)YuicqE=GB)2T&Q#?|q}699#<* -!mX!@}iR{pzm>2f2yuoHcSZfJvciv`<{y(h21ZsM!(ZHyh$BjshqurarbR7~zFWr`tKpPyBmG(R;?3d -VMqS;;+Q#;6`Lbwp2@lQl>Ve^hBOxMPfVPK;3({c7)x{3|_$&<$N2p$WRt2k5pu!N$AeWBe}|k^k3bM -ay=@uthO!n4%a|7tm)glsFg`I~eAHVN7?KIa)D{>?ye;?lKq{*rYD57^CCf(DIMXz=sh>FxRYEBkR_! -6XGH}-8`vJPk#IDH>rF+QFhO`Q?^Z045|w+DF(lz&)B5mtY}%D5iN_-V`M>EtjtY`lR1jv3B@p7F-%b -mjNlmKF9$>0ALv(EF!HZ*oe!2Lo_Ipa%E}C$#^A76IJf$}m*my_J7t$**s2(eJ}U-nlGW!TwMok|Vq{ -@@tURL_3KheXis7#+N7Ise8bYy2ajqESrSC%9f9SXJ-?eMk8=(REX3d&agCQ74eOP|`?Yw-sdx`8V=p -j32M9Nl2pVzzed8LD4se|EZm7~J3aWXqOfPrCR4E4`_buRzL_}{lYmySQebM?RTAAIn^K51!bZ$c~jh -z{iD<{Auc`D%zK9MP%Z94plLXpgTZ#z3E+a`ZW9)EoamKfLeNs~3EbwQJX!*o#`YzbkL=UDdGmIw?Ub8tx=Dr%86vAztrGllW@e^Lm@q*K3JQ!~fByOB#_kXcg~9^ -e_y%khu@Lbn`2Yibes^`2oc~qx4R3ahKHC^$v7^uPR2Leef7`Zg*J-TtXG?$Ewrx9&962&=#E22!sU0 -MJw*=jg{-k>PEUEi6#CD|n=7*~#u -(Xw7z2IACN0Ui5Aqw#zoMd|X5V+;ebbO1mG$|W_jla5abub4u;_b|lanK=6TjA57{HnA+CcL=mR=!^jZ1f*5|cB`uz6h6~uu{8zVfOJ9l2Nc=2LW2VreTmnAPRPvYa_<@)Qd -mz!_CSv0mbv_J>C;htcyGZ+gKnX=c$wjNkMOt$?sR{e7z#=t+Xo6mzsgeav}w~mKu`Ph=_8XTPd0o21N3+A-dz$B63pxH01S45!REc>V#L?TJ#=zJhQQ{QE -n+G@ueRjr)~#E2+OT26Tylsjp&!{~B1_bpc@4U#IcH^M8U5|iqld)C#!9<(?F=7ypL2q7gpw_*8}^7C -VGoci>@mEv^$n$oc3Wfn7lwYtUC^mhr*PfFeDrzy_U#4(`$OGO^}}FDOiYx%eft{OTDfwip`X2G&v^{ -p=mGYJ*NIJ$A7X9v03ARlxGpwWX}_S?$#2=RWvueOWY3;G)t*4lr+H)PXI*#vWA~Lwz6d?aThZ8pyNZ -oID8EJJ&By>giaoaH*m@#%ls(1%uV24jd_JG?fquW=oX1}4K2@u2dsKboWTpL8eQl;E(D!I;|Hi~`#6 -Q@7)yG1fqi-Cc1%9KmDyJrvNKH*OwpDfA_&00~@@?f1JwPwv3337tu*vRth{s$P9>EX%x#Hy)MzEXB8 -cBN%{cc%x%dlIvp$%Nf5V|mP=1ftWW&E^zK%1SMgPp+^F_0f}A||whYsHEc)EJAdx#pV7rM-rJ8~^Km -=Yzr8#ZWldZ)}kIZj&boiRZ|S?H{PkEN$Af=|4hy4gGGv=#F9Bu}ytEVHOn?ne)NL=fMWO$QHH}ES7eSPgh@|yk9~;w5 -Wa=`B(p)Z}YGEu-KZX?cc#0UV{&|CkM~4Mf4KrL4gI@b&V^rGxgH%uK(Cvsy?0&-xL4voW2|r+2Z*6> -#rNT%O0Sw$UF3-2P%&%uOjV+es}#-?La_$RXR2ay1r}OJm -F;WnKGFERL}S}A8UynkhTC+8YrJt{)K`H%8MRRQd|jPd?gu}Ky!(Sl%pSA;_w}s*Uuwg>y5?DTKi%GI -V(aHl%zr`WPik`ys@;5r3)3^D-$Jad=a* -qSe7`2_jt^Br21mNkRA{HPHNipcc{tJV{2%AasbUi(==ZgWAl6RT-GIyW>0L+L)_#Z^vec)6X^HRE8B -1~ROzDDI1wZdW_|a)55f05uqXJh<6nOwr8DA99kbA>0S-GZGUQmGZ`%K8pzmw%A8^Yf{UQ2{QF{Eawo -JCdr)d6~sBvz*`#uc#PYet#_@AwdQ%wzwdN4J4yFT+T#{zv%`fWxYs{*|?dOXM@Jt}&g^a%&PWlr5^* -T;`*zMnQ_%9Jtgez%QhITBm@a(kFsBK2JAi0<|2z0yA(5G0SWPLGCOA3Y}I@z(7D`(W26{&Dw8gZuB$ -!l-ci6W;7VJ@(XzV>0cB+B6`K@j>z!5j9O}<5nK2i_>4BH(`2Q&iYeE+-=sn$0d*5-mcAmVz -Sw@XD2ROwk#f9B;O}K2RF2^2OQt`CY!z?{VM9J>zvy53a7t-PS78rSD2U7vt|z%ZW~SS)-C_&)?<%7m -ZtW9jO`DQ7m(YrC)ghL;#Bp=^5!%BOrL=IbcIu2U+K~b`b=}2KG{@<28NMGU3aj~&wj40|298E4~hBR -eHM4`j(uvVXLRsYzv;2itIJm!a-CkIYg}UgSbxr(If>6d|9m{XA>wi9uz9t;ZxA}z3+xf^G3ej=9=oU -$q2$peYa#7FG4|xilhbC-oSDi#%$+;e*gfnnz7PA4EU+KQB5S*QPTwE>K#m_hq&g8Kk126Ay|Vh&$EQ -%|pr*?nlV7PW*3{C2dtsJ0@M7=$esc1w59Gw(4qcST2^ZyYY3@Ll59$B)`q$X?<*Hvo{=8g5^_TEapJoi1<{{?^Df7aIc;~2d}>c+i$_crxcXc#|!yxDi`f$fo48L%~ED-Y@?&Znf -Lj8a+7)EJ2`c9ZirlvA~0uXOL{jUGMvb$UG1=pTOgVSyL)*VHf9^b6Q`D~I+T5qnf$^{ejZ<9e+Xnx7 -x{1irG-&}e0A{9J?Bj{T;Viwsgrr>}v2xcdn30K33>(869|x3QVfxpU{vxt_-LD2)CW6ci*D7Z=CdoE -ttv!>CcC*kB4gSW|K(RB -}KKNWwQGxs&*S(nS3Di%nbnso17FRgYuZHYfo#T9~yqF_RYJK;X+0b`(-QV4eRgAZmcbj{{O?pQGKcpwMf{Np8V__WSV*e3J{TJ85cDz~ka`Qrky;czE+B7Y=*C08I< -iO^hWvC>7am7E;fk$=`*o7>yOI6r9)w)9ls8sy^SUgUn{edL{yns*Yz5K}=nc8fs{L%jIXv*XP*+~d1 -*ftn6=1ada^4?f|8>^-PkpSa7!Tf}?Bdc>NI^f^HfdfsS8ym(x9NAMWoASDue3I -CmxShO#+-XYEJ!XymUx=*HeObD6sfh=p@+<#VyEHd;a5v!{q%dx{h!No?v*!RDdqf)_WT_?c9>Yr- -j9RbCgy@4*i$zxJjR#1<)6=6{@Z(!xE6kr_3=mK30xbytiFG$`+V#)baNf{f?g4r@Y&RpIF~#d-@Rwg -o?7QJa+{aMwb1El>P$~P(|)G8Gq2x({uht=Oo}QVRG(0OKdB -P;;xxSK-G(JmLNgB~FBRM-cC23SrX3d-Kx9`f)Q~8&YeUq}2Qo>UB+bujXvv*qdxUoqY;bB?Hal`r7g -5APLCZ=R2g-1uVujAy4i?^?P^{Do>XpL%L16rR)wYU2j-7l_RT+g1p@4qJ@!Flle^Q-rq|GQ05`$$Q3 -$;pzlB`v%yy|cXYylcv~l$P06Us-I=akPXUsS%Pd`tPh@~ZOc^0Vdquw$i -OBdjQ*sHk*JX+`Ol(p{zdN-ImNN{^IQmxh%^=-IJ)em}j!P`yUFUL{YjGf%Hnq}Qs@tL@V3Rq7Rw=rv -DrRo}I~7QU9gFkc&AgfG$;>+9v~=S%Pn^^NwW`*M7FzD2$w-x^ksVcEM=J8(ZW!J*IZM>1*SZ^@6aWAK2mt$eR#UY87O)E`006(I000~S003}la4%nWWo~3|axY|Qb98KJVlQ`SWo2w -GaCz;0Yj@i?lIVB;3U+&PNhLC4yVH;L8E4l?oNnLE!^cj#XU6d`lmyv~C6b4vtSGzt-)}tt5FkNHcDm -=zJ$HDzV~GL^K%r2m7Yf_n&chvVoK2GS>cCr6)19C2&DQqTw)Z^CS4DDlU3tOtfw#ZA_ubC^?*6X#Ym -&w*?>zpL7&yU}nABWZF%8S#9_x$afcV{Qh-oJZ$c8)Kh+}5Nm)TRk3Ww^juR -luj*W`I@+1F#k&RuzXvbMZ*w3HA -iW{dAz-VDa4Lr0^YdTNL<|&4AAc*5h~Q&9z822L$N0-6%8LvDR;!_RC8~&CMu?dq%xXM|#Aj@|9MS|% -lEMiJYv!}Knr6jZ^;^!P>iQe_DV`-?#N=20Vd%Y2%SDbq>UYOQkri88NO-W&4iyE40RI)e3!*5E=dff -3TU*psDD);v1$JWF8$*u)n76zZsh0{d5ffNKQzZAes)_(eW$5|&Dqm?TOVVlP4-T+)Uc^`P_`pjuKrH --D13h_t8l9dUzdZR1wB+Zxza?e>5g>(Xz#&i$U}%|C*Ma{jzKu7B_i5#T=N-Z?<5}Ww3MHXlmEyqh+Z -cY^`t|rMI)DF?U+MJ{(tOj$=r%3_-#|;6O|r@#Ao9|z@@fDFUT3GQR{RI$gVOgN(8x?=UmWaTZk_%W3 -KK$CzW2zexdpxFpNwHx1-6dPp8o>Bn|w&1c^_MEdt;RgfKM5f2oTw)pU& -HbQQfge)j%n-+-y%hH;*YbQ1Uz80$vhemi;hOLY3z*GI2jy?x#fbG|7|=;He~C(qx$IF8Ow{&wsSknW -%KfnFG(3jtea>iK^7DNE9T3J#1JvG6LfJu}~KZ7D_@ogG7kLeRbf{52?i-`@)U{q)c2{OQHtcP<|dws -!FAZTj@XoW;sU>LAuRDBTwc%qv;~bL>kUtSe -)IP1`1#TKvG?Ej9cbX$+jH0!wy4wa_59@Ro8PbVo3woX%ki_LH$S%;9TonE^5OP37k~dyd`K@J!bGW} -FJNCgdF86{_kijI-(LLv+sjAa{`ZITM}xtK@=*{z9Q?_EaQfGGzr1||)kpsJH@vp|#*e>Qzb<{^`dJy -4A}4P8*k`(`vWR3C+*;!KXcBwbftMLe(p^BWZ3a0+fPjsy@hmP&?==XBu+;|H=#v;%17mFi!%M0t3Q9 -4X8jA+MhOjN7><3#Fv<}Vv*ZkbrR4&1)c2czA(60+G=C&E0-16@tm0uJuo482<~Neu`#BY^3=!QXX14_aafu5bPG#m!~i?P-!uq -Iou1%v9f}i`k6vhut|u3Bni#G^Pslv{{I9F{|W2YZPcwYL&xQn*57@th+kP;*PHN^bdAeuF~1Vy2OR5 -dD+utvjN=x`w?Hn3CPhWB<7I-2b?;MM>ByM72>|0Rhny-3-q|$T?WDl2^o_WluE`yJ!SPI>#?|igi(Y -5L;?C$W0LSuJFJ2c7IK?Laba)J#K%miO~!lb?KzO3+RWqe4eDIjC{@5czhl;;>nyv0a*Bi0JmoiBFg8 -c|_S_G>DJw0Yr4Wk>JHQ<}&b- -e%m4}MLb7E2?QN+9xR1;NZDSLxwPP+Z|N08geX{$|jw8U;Ql04f2U0H`NUc@XJXc#9H5e$cXmTlj4<( -2aynAVdw=FitZ3Y5Z`VEoPI5I|C*7gI$L3cuV>QY@OCm`HJ6PkidFr{|G_^NoXmI-EaO@jIZg(I9)Y= -10{-9)*#6m3!#ziVNH-!mwff$}7UsyrY}q56$JO{ -cP?cNDX5L;Ys009@d3aT1u-PPISabfMRm>bF=?#85H!!K5ZSPDF+#Qb+eT#I+mW_()=@ml~4|Zd+2XaN`~yoG(Mj?VT%{8CLcjbA}JI`KC!G@J -~`&ryg|<*t_qn-SPOUHwWH>g0_Xe-brM*``2Dugrs;>;@?32(lMLn6gg*ydhokaZYKqPHN7dW#pOW7z -6|x=*3n(&WfE|*Nd;zbTq7>*8dT*!lwrw`!x*tmdDvv<&-A7y_sY1YeA?b{k%Ru2WQ)>6WditP3Ob?_ -80w!U?rg#_`Th=?A*`}Je9*mND69hpSSPLLqQ)WP*)9U_i~@|;MhSRX7uohCi}H3nG)j1p7ngvib*!( -ZHIww(mXnr_{h-`IYtD}P5A{@BtAVEUp#nbC$mz6qwjaXL5OFi~c&Hr&Bt8T8DVyYj%iCD+HQF@qw3u -#de5ztnVi8hMI(tyXSFQF@rsc_3ax#6hX$A}7K1 -ht~<7yba($(N~(P!@%?iC-=bG*;Qa1Yf2$D_q_}VY|$QPr~<(v}T)3v87WWA`J<=TIA5(izKarWI%*K -w6eTy=r-F*K-6{rET#80oostk(!?*iToMlVMc1m`?(|b!Hu~q5WA4!x8}Pd7n|05vj1zo7`4qJfY^mM -6N4+tZfVm6}{RtFZDbsV~WBJy-tvc -XC9(!#hwz`7B>^c(@GSoeW*bj-2v3PD+u7EB&UUknuk;=%V2P^vv;{ck9B -q5HFHRE_K+S?4XdmYcVv}Ej@jE6^qGQT -+`tgJuGx%5iZ3ARl@Am*@e){y}~B?o+o=G~4KIr%};&;kMzydc+kN(=En^@2vd;EwRRWAJ(Q>hr^;a+ -d7Sgrd5}?k$$0zwIm!YLVFW*hQcRg)R57UdOqbge^hbsjBsrBT?Qm041^$i@$4!qfYr{GQOMDr=B?aI -=61lquS8h|j$#}^6)uWd8Bawt`r(O4agz$T#2_p%>?a2@J@f$%MGpmjT#l2Ztr6?~3|stqiD5TU0rd; -ztm7}G`XHlqutUZa%0lRO1Ox^0>i}5kJiZYWbC$1$xXa8JXzZ-#El2G^x-A2>rxjvGRGG?598c03xbL(?s?CGu|BLVNLWy^#N8%K-i#xN%rYB;sB^1l){BPC;E4{rK3C(uu{3nv -0@*#MKSK2|wx*-IQ*x729VS1Hy6~rq!E2_rXk8pjs2LZnx(x%8C9bs3StuWR -*z4ad@)5FD2Z=^yy+;jDF8>rjSUIHKu(GxGfQHs(dwTaKYx4n!dNHNJ!>iGMUbmmG;Cf@5^jp7su9~( -rtxkKA|Dv!L>8&&3oP!Y^Sq0Uy5zhiEDssC82;_#6!!m@ua4dwzi>)_o014V9PkEgXSBEq{NIYK5}!^sNwB&bg1d{boEL4K5eF;+Q6AYwuDiK@CGn$^`gN_xYfE2d^c)X26WRqVN`$@W{uj)ENO&y1tNqC -o?xTr_teu#pf>b$`07;AH`3?bT#t7;{oA(pGAW3ASBoiZq}K@=lS>`M0X$+y1}N04k>YQFprJtEQzwD -^uSsn$@nbm#!Kf;mVSgBs1GOaR4#tL&@=3a|O>NR(&&VcBH{jM5dB6%A`PipAcC7xcwx}lH2RnU{w7vA -|aa+vevJ`l(r3WaZd4+lD}mETg&CP(SwebIWJpOUMfCUUb#pr3f!9soL1 -Pc$C`XxwibZ07k25Zr-b=k9<{OWDxewpFy~)qfSYZP#=2$HG(|7)7w6^<0GBQCR{>(q -XZY@TNfNpjL(=#O|VmiQ70Vd68y|tLu9GG71lkQd)nqmCDZfLR_b6w+)mw1Q+D2-cyXJaD4WE`P_QHZ>J68|92b1u@gd{OLoDs0d`ltDJV0$+GpGC^2s19gy -fw4+Qk9h#iu5t!6xN}nOoU)Q|AS+1Gx-NWaO=#0auYLO{oc6h*^|6O{@fEhUQ%jz$fQcIAeQLVXxl`| -c&dpD4bZC8LfOanjcU9wMC+?)hLszMOzfFEw4%@iLpzsyd}nx^4`5nN^{#D0Rwnnas(o5StF2XG8**5u%F990H7%C -#9X!&@a4N02r9XUQT~c$m~22GxDU5NNkmW7YG-m|848xNF9FIL6rHfDSa6zXH24w@9*KdXEmhy -e3lQ^%!B97AALFRdlbByXSnN>@e -owPIpi(7W9G6>q{!$FrJve{ddUsrXgHqO}>kbdagt19|c4#_%VjqF=pmMz0RO7o)1)rKOJ$|9u4m9!Q -CfX1l<(+KlI+HS~FL$YkFZCpgwu<|JK8aGLVovTxs9Dtn|z9p3RIDQ>ZdODli_TB;NFuFU&1Ji^+SdF -P)A$*u}riSMFSqc~&l^QRK0=hL@$?ua4xdT%JaY2ha{05{mD>D{V)pAWxZjVdF37Tyi)kT6B3Gpy^jl -FH7z;#vS<-y~}S5R;f9K^1t(6krN^um-=NFWPQJ%zam=;6t1hi9x<9+-He_#Xu_z||>dNru>LsT$o -0*t*eK*tj)L#dQ46KD;78Pkz(CV^^MI|L{i3z^9z8ON2fP;%$An1QBB5cNuxC;&-L7t^5hF=*OEh;)Z -!#3VMAVBjWjm~kQUI32H4F^&X}LJQElSw#axZky`UST2&;elA}XRqJ --moC?A8_Z&_EvQ}uYNks$$Q&@7GP*daF~y2CKSrv^ypWp=**et!UXsL4uuCyk&5JwsmbTxN{e4}W|#2 -)B$G6KXBpVx$sJ_5mI6Ir!*IIPlRp1(O24Yuf(!F_Satnl*9}AYqSVDTOo}{`e6s1Rp=z47QdYid9rB -P$Zxg8_Msss7T8Ve&%(|Mpb@k2o9=ijdJQWa1TBXRL|1bHLs28eI!tW{Czgme=19_j<;GDw$+_NLeHN -iw66u&QWd1*cWpiG39MV*AavK(NbV53YeuIzhRE@0ONTwrj+k@>KW-YAM$m5SV*h{`VAwqZtLit0E{& -IEAX=ji1kn4SU2SOfYOvL8Sr4x_gVXk~Xh+*7u}-bJ(&=_-_pSE*8%L>Ws3w&dgxncctA -ZXln@Uh3z0MCuQ>wtU+wE4+p%2}~@IjRfiZ+!p$AB*KOD<~H)Asr)_5uIjkoIq}1hw^WG-DP>m2!@)t -iE=|A^)Cg9Sw%LRjpFeQg(AyTGA4}C7%HOFf`_X?tEeK@8@1p;@LpsKJP4hNE7-1X1<0_kvRg4*ePaX -5fD=j!;l(9YL{gG=2!x9YQv)Yhg0W86gA$KbET43Qm|7M-%dEKJm86m}h4Y&Uer`Bf01u+_D%gSr+M! -(r{cXgPbf;osf@l^m(lN{^iJ%ofF1$n8^(#ZipAEsM3n{G -D<9h{`V{o5IQkPJtgOT%f~lEmb3cPqp0VIn@L7laz|=16kEbOx5FkSVVP8@{_Y{cZ6aG6dmB!JUr9rI -G>@eZ37xqPp3PL`uHGsJk*>`gL5-Ma!D;O_Al9SR24yu?ug=(aOdM9-TmnbJERb)c~Vm7E|I{UPOjqP -vwT=%rEeH}J$>X@LKfB!Woach9Jb{K^Rad})sJ-tM(~j~_R(55Yc#AQ(AsflL(h(kbBtcrhPk2<_IIm -mx+IXVn=pLjr0FT^nlr7;P(YKKVlx?3p!U*!0?JdGt_=}-{of7)c;s|B*2$;>b#&ckfNWlf? -y-!lwH}DgFozB4K+JPN1j7#8JgPGniz{-Qs_a3vR_+hs7bGrk&*vVQCQD`wEHq8DVd=_6S3SKjHW|1i -tahz_D;waw!Qb{#{(}%<8(=?(I~0NYK8XNTpmi9ptGha9NEyI@321?@wnnWM+dDcapfAl<0yon`3wC^ -lVk#Ibf0=gnM!D*>~gsbK{Tqe90{ikQ&ByJC6bJ1;_>(2{q)_FC;R(hd0owCJpj)$FAPHvd(uTb14gC -B*B*cAU4isTM#wK#A2y41(JkGNCi-&rhBo#_xk@tL4O(;PU* -VW8V?As;}+Z8)8^qAAyR;Suz~yLeDua)J*T-zb4?-hs)*ZKzV~b}Kq}lWm$(#Cx4k73o;5PH -E`5TgJV)zPtc(_XP;Ljj8tG-?dSVsaI6C6G=j12dCl2H!W`@MeeH*syWxH+Ej`5I -hPC;IR`RD;_b);*EmI%T2k={iKbPgin={snnQTiYHb-|+ZokGnfEo}Q9pamcrDc@4_kB*LtwG^v3MDRxG5s0UVqtA7M6#`ad1sSP=TLPrw9=l&*7o0dS&#WhF+kL2SUDuqVCBjLtm0^7sEot-Oxw$r -T_;6ng)0dc-gFff^QsU+*^`X;@`!ea6`Cg~;afAa+tn-^#WTLuO25P%ny!g*ri)-zH>J=Vj4z--@63y -Dvt+`-)v%#sVDIMxGBe1~mF@lAy(hc2Mo)K%HJE~ZpH?s!QVfGnW7we|{K0yfz(CRNtCVb%t1GeS(yx -H>{pjB%%`kp!tkz=f|8tpP>(G{NL&Wi4-W|Weg!Sj<;kP@al+{_JjI2U;3{`ciJ9oB+Zj|WLx`oA3V% -#<;Ni&=Wy}8KTb(o_e?y!(l-lK{T?zrKl)m-FSE)0vWo@0o;ZfINRt-lStEK$Keh~W?A9E6wROS>+7a=lNgfs6xxnqi$7oJh -P;OdX-B}sx4;WmH9{}n+4|Lfz>JOjH|3VH-%J!-7*l$^&n2WBi1P{sgj9)kCLXBMm>eG6#ee)^n_G8_ -MM393Udh2KxA9*2-I#0`_Ow(o8S(~5N%FWx6r8FrczLYZ=&3e=O*9QlasXp$Na3+7QEe~Vi5tc+tT`i@L+qh& -qQ!^VhmY$FVP~i!L_ar>*3$6v<4k1q+o8-ZC#SwDj3uQIw`_gb)S^ze{~zFY_Nt!4H -JyUy=P?H6;DphICaUIF_eY~$Uc1%Klw$kRpf~Z1PXWsuNxpG$ny%=)fs1F}At$fYgi9sHx!raox>a>S -zz3QRc3r)p>aCmHnu22MNJ`h$ZSrTn>q;e}!VAd+1_C-$&8V*GB5JGzq|Ni_b%@vF)qt{hZ101d%-t|aTXN3R8N2$L47}Xziglhfq -sWnHZ?s;tLUSWtE#aZ9~sMKAKN&TTmq@2g2NOtCnp;;}RXA|`77Hm?$&0pq}UX$tNJpo~s0fbG7u`)y -3#AzS7llryS&vs5S?a<($E|1^>T$(dmwOlT!;)yxk~q^dG -q$zSA!BT}Fw!5en@+;C^x%%~PwWnS7=@G7TV2VU}6s!(@PR`6+Om&P8od1Bqr>3{ybbMofpTgu5OTXT -Wz1t-U&3beCR+r!t_s9}XevOFv0upx+ZHXfB?154WY=ZV)-V3llOU(v{>r`vC-B%aEG?RxU7-GXD!^$ -t}>`Q{$y9mq(ocy&*Q{HnVMGxKN>q`34G4PTu`cDBjKYRRHS6fIyYD39j+7A8m -+t}r9k2WkG?<0Oq-^3Xtycf(%q11|q-zAtm#}D>i0@eK+uqS+f+62>2exluhtXlm+GM(t;ki_6v5PCdg!1#s4rjd0d-26&uqKodD}euyeZ0N}ck8dYTEhX?s*_MTIc6t99Lap*dpDBI9c9q}i>Kj$G -RoqwY*%Z2_p0UrSuAt+w$hkO8T9=$O1dx`DPgRO)w%32tra(8?>Ig0WuPl5P?-h*Ta1PJ<9Lr*|yfsa -K(@CJl|v2*iZEpBKBFbdee{j&z!ul3@}Jn+Fo@7$K={4S_Lj`k(!gG-eq2}Wq#PD%4b%yfvR;|O~Jv_V#5es7F7Cu*L=#WFGP>YbCHL*B7m_`!$eqd^pWm>B=Un?J$85U>@9(YSe;g?KqVG&S;4jzi-LW?LL0>c -IO#D>;|;&)6p(6vAoUWTtNX0QvX3pMIcw1@p1%T(AVB5jl;U=cPNL!Jp4R{Sf`|WJ9>$^#KNe0O+nDv -Ecg7ZVnPH*t0u;gilfAi824b{zWIi`K9AGY3{=_AE!u&uX|BeeP=oPU43+A;NJp -{?{1X-e>W+mh9d#VmR5nbPEZ~w%W_TG7*$I&q?stl&3>U~U*dKb|_lcwTL++-MxUccO7iq^~R~UP<-+ -TN23?1L!-P^rK-+%lk>HCxQeg6ZyQ+LJYZESMiy&Ov8``+CTznKpYZlL%RtGsDOOz1H$%Zb3ziFCZ;e -0=rbjGr{4N`~ge9Q8GW3Juns;u~WRF~Q1u62 -+yhogjF^Bw~3k`U-V!B4qODZ2j6)a1%mYd1vs~U9Qc`fg|ZVXz_a{J~v`_ehqBJ~C1@^WU{)p;Z5*Z4 -(k^~PKH=9*F0L8JPjOIt_$KTt~p1QY-O00;p4c~(;+?`QnO0000I0RR9g0001RX>c!Jc4cm4Z*nhWX> -)XJX<{#5Vqs%zaBp&SFJE72ZfSI1UoLQYC6B>Q12GIl@A-;R&JYB>@drI1@dqfIc)NzIQ^gL|{yi?e& -5S&w=NRKhud|V&^ea=vI{J>!!?rFsK`l$oqoVOL@?g>@tbKsRXh?3DO6by#6vA05|8kw4mX=k0)5}<= -6yq-L26=gU#)A5mzLs2mu6f%9rO0N%LTQ=u)iph_Xe82$iIO9KQH000080Q-4XQ?H6IWzGQr0Luda03`qb0B~ -t=FJE?LZe(wAFJx(RbZlv2FJEF|V{344a&#|kX>(&PaCv=G!EW0y487|shy;b5Eity9h5|XPK?VdE(x -FLD!yw2qooG`eL!!6%?!#Rr5l!+vJwC}(SiD<+w3RZ4J7}q1e2N)1Wm8z$rgQ3WB*<4Yxc%_)7 -WPMkZyg=2ft{`Ck8lWIY-=h(%9w?Y%!c?$&*zO-U_fPwW$6ZW@J~o+5?uGo-SVtae ->p+=G{Z>^gG)OJHN8e-X*2u{1i-2HEogxCPPm%9DW1I`EIfo^D&!mt?FaCftQ(LO)^EG>rsK8JI1lBrJuErzcg|-6C@u{4EQfkOY9= -!XMr1a7ZgEJhGjJh;_YpHzo#qNWDNH)I;)ElW{e04Djf0(O&Q*eqW*IWMEq{*GUZg0mj3;4aU!OnYXR -pk>SR7@$&5WlMNvV(QD|tR~1Gov!Nf$*ExuWk={oeIuo*>BVpTFBVVk{~X0dS$J*50V$?KN -OY%XM$RXAZo)kb>f@aU0dp{x;Konj<`wpOQDAv-sNg*A;a#!6P)h>@6aWAK -2mt$eR#V0~2Gda-003)b001Wd003}la4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mb7*yRX>2ZVdDT2 -?ZyU*x-}Ngx6bueG^u*R37I$9g4vyDGj3jHrkL+Fqff|xcYNFu`GY`x3-v0NiXFq0!lGX`uh(6dPr@O -kUy53zqr{?iDkIkxD=jHa9>DtZX|G-~PPEStFi)w$U^X+wOk{2^`_T=fA`EYH`+e3R@mF9i5X>YUInp -agIK45Os -s>#42SA1ef_^ergW>W1F=no4VS;<8+_3*G3*>%Lc#dxJ|js+ST<*TOXd -C82F<(tEwn0h3rygVC?Dq6wkX>igTZF0-}m{iB?-vu42INGw{M%%1+*kd)tJdx_?y`Q?|Ia3KT0{QmHZ*#DSXm -OqOrm08}3j}V2%*(;3Z?e2-2m`P6Kf?Z0w+$hon*IZDH_-ENh|~48-IzeflTA~v=4P{Q+PNugxoxk{p -MLwz(O$Wqr?11eW1;;`mssxYQ -AzrJI?{?)ea>QdcYI$YO{mOXm&KG>li6mb%a=<}iw2fQz`7EMO;%&e~M>V|>FeJ -NG)>!w0cwa;8NHO>W1TCmHL_E1A^T ->D@Gv*z#XH%NIG#C2l@{TQRS`b4Z7TsC0DId0Drv~k-*;OzbCB6WfD^OLGSo;|iHyTam*@1;k?gPUB? -Z@j31Ei{JW#2;!+Wa@rS*>nt+W=^P8b`f-t)lFp*N6XxRqu5!1#s6)JY}?F^Q8-S(Ly+ -(Dro-hz-T+kkjVY7)f|~%tFSBuQ16GeQYJ4J5i~<9yua8KKU`cs)hf32rG>lhbRGEuM)Eh#+u)pbGniGDaTaNi_D!?6*=OCKAEicBoZhOvy>GSQ@yoq+ -Pvg=W0xH;j>&vxC<6l>QEQ(!Fer&F@>(T~M*OYHMW^+bCAbT;73^3}LY?k8!l5>Ms?Y8vyQeNl`s05! ->SJ@j)c0ck$xJ|j!>IuyfFY#56@YBl8~gXpmXPQ}e7`NK<>RJ -36ckO+AeuqzpueR>z~F$o6y2KLZlkQDV5^Tl$4|(TCSW6vS}*8#Of@fk3$-$Sjct<%&MXX^`hIQNV2reiD&w8buBLeRS?K+TMG+f3 -i4(p9-DMSSaj6Y&2aa4-B?t%@$cpjf>ewEXw`Fl&9^4uPf!9HS2Uh2HohoINzv{##SIUG)3i-+@$9lw -C;-6zHa1JaM+Me|qCJPbDM~iHBOwLQm0fqe@2TY%t}km=2e-@IbCKN`-)86-FbF;2y@X72 -6K8bv9Wmso~MKY(XbVP&;NkJjbUhs7U=4=<1a>f^%1_U@21lpy^AC+4iTr$z_ARHJx+jL6+hwh9R3s5 -O_6}xH(#6wkQk_sKwFfSU|U(DNu^ouCO&SkN+#Sb3FWgjvEnLc}H^XZG;HEfK}N$(Y2^9!^K@=GoU1} -CAdX-<0Zcp0)X?P94xYIyK4)ZizO129>?t-xaJy5Gp;zabb2+iQIR&7vdSNLJ$&}dTg`vW?<3{2BpA{E*KL}G519hUG}mNt=>7IANU%G$u@X24i$5-O8Wum` -Dl;Lm%piF(UGh+Kr9Yvh2)Ad}Gn9e23=Jrp;#TTwdNpM)M$(mp@|ORsGvEz)F) -LmDnLpRp!$r2;)GRXfMm3QUOZ#t}~1Vi55k-|aB`i(vXNPy@hT?AKp(Z0kAHsoCur?4f6T@W@EGzHf%Sf^R{G8ro8lqLT7P?AOQiG?`znff~(0iSh5^-`X)hD;>3-K1W{OE-2qR&@bDa; -lR$k-soXut$6yRA8W`t7&>Ib2>1Lo#{6r-dkAq8oAw8s@Q+F{2VxM}^uL|GCItytA=A=Qa*8$Q -F{LKnu)uRo!Sj-z$hjA*Mud5jRJ|Phw|C2RyS!32~}%oQOL@#!XigVWkv1(sDF|nn;i;&*VsG;&mj%`kah8nbADFZa; -m;}gytt+Ai+Ini98~6Lne9Gz^YZIJ@Zy*i9Mx#34vrTP5BRTLbGCbXdZQ9osW=p;66mrh4!9XS}ZnHy -!Z7`h5XANr4ZVxDzt=3q?=%X&7fCe@qI&2*jf$3XKcsWIG>|PH}Y4e74?$9mb5U?B+1_Ub~&{Sn5M_N -U-;(jt9DsQw4qse!9k=2@)^@DjT_P^og-I`Y6TcFMIxd00W&N{xkVva9L5#72)#Pg%S6I?Pag -y9%zb@$uXva~EN|+6$-4bp^*n!jJf0EU4fN=Q|*CW&A*7MRoKtL9Gr;H0iLuunCL3 -B|e)GTI$+q%;nkR0dc~-uS=-&naAyMfWJ8kSh`hia5#;p2KDXO%+upvD;-yjeC|__|i1x5n$r6UT=1v -(iPTTZgE3#qSUBxt-NVE+i=k?-$CSnAqX$MEvlx0XUMOfqX(`R3qAu`%dz%72PXF&@Uu4lXw8mVCzs+ -XvFQMX$buYEc`0CVZ|>gKSX`8(g*-%79zd(pXrSNZVLOJ`VPBF9NF7!X79;08J!VMOtYF3h7VT^JzON -F~N6oY+<9O)qV2xYjfVz0(u^3}4^X=h@+s9%F&yMZifFKEg#CUmALUdur4QOU#iw*sd_WL$MuICE|(e{GRDjbINFt3&e2w)qpU!EJ~v@c4c8J$e!`|1)fsA%t5+#lp$4;!$Ug-A2mtrDU -IO7^2msri?g4-pi~taiwWh=HtZo@ZKq>Y(Zu4~#R6@u#NhR38vpLt^@k?EGdkVqQASH0Er4r5?7u)s2 -d%=d9(gva}=RoHUUgBdNk?cR(Lr=8bBnLC!xD%zZ0;!6t$c?8>#}D#28-o{IOIDvT)yE8wJ<3X`zgrs -IIw+)S46>)C`7end>?6Ae@H=~JR%8lBo>+6fMl>O@R0cQ}Y%VaTb66WWlfr!;(Ewe0eurPDCn!F^qQg -5YZ{XFw98c(??WpoJ$W;aN=A?z`4=ONVg_*wpRs)ch8>p29dF*)e=yblNol1nmKvJ;ds*bvuD>j_ObJ -DVe0(FU+z@y8LqHS~pawT8|N8<69h6r(pd>2>aapJjLyqJ_Bxlna)|H&2;(;4cDOyz#$q7c)$?Vinwb -(=r4*_yO`3RPcH!!d_0@Ff#EvQsFa3D9zuOca+AZ808HD~8AOwGlw~4GU4`K6U~>8Bt^ToA~neGxPJ4UnVKfOB9s$ZE6F)Wc*I%K; -)(cC;svCloH&@3au0B>m$4{5W);Wl -4EC!IA~;ZXKvAC5hVwnGGI2wClA0g#But<0RiooRi?58_%>jo^MPbh81=1xf7LJC;!(% -ARE^n^EfwZ?4! -qf>>60m9vVzCaUJH}7ZL2BJ-Lhb|!pwkGup7=all0bKZX99Xd?HUyuCu?XL#uS+OJ?nXjiQM3^In}lF -HXc|r#DK0}U@npmvD^)Gvmlf#uaIubWO-^Q@Ss{zVz*T^c2d<-W>OIk?qw-jsIgBxbxXaD -HEAE=4C_wo-caJYkhX>lsCV;imQi1?JHM;s7`4Uq1){=F -8^mrfXr;vmY;@ks;!X!#zdUXhO@x0~;oKDa(ZBXmGn!Hi*#COJ_o%{yfNff4z5cnmO9U^tJoLML4u6> -$Uq%rK`zUp6ZAlvEs9n2kk8?{<$(H{cFC*-j5^(Ik~-Kb^jLx5a$ly>$i*VU%vX&bI4}`jPOrRHTaxE -w4#~2e2aQ+LGo_1;>K;pND(kRy>oNNdxEIW=`|5Ano*4`uC*#kq?^MQC}F5CVv-pjIpghdS?&ljY=3@ -r1pIyC)BlUaDG;8?*jk9qSkx8!s>SnnFaGfQFE1Cb-n{$s^AGPaZ{^a$_6sIM&rA~4VCLo`33D~hac7 -rvE?i8p=#bTq$w$oQQuTNkBu-=r6n`9p#gNQBu*iE}(#>aIrEz5k2aMBm2Ta5nfZC@h_o|DZ15bFU-eJBtyPP?V_djd%dhplhdWXGuocanX{DnBnp<*8Yj+ycPHE}C -6w$t!6Am6+shl{RhUb1q>6fH$69$b#Z$4eWK%l+@VkjfMRlZz0e67HB3M{U?%7Ev3U)!>=yMG?;#w9) -xZ*SC5MZVrDq?4^=5#R&URqqot;nk!c@@s4{nhs$}S<|ZNT&q-TncdZ&wQd+yF?Vt;2y17%|_@a87mj -MIfp6MP&JfV*c2KL}N^zqF6=KO5z_;DOfnZ@l-;11x&MJRY$4I0=}97M)+Oc{G7hE)3K`!>Skm9;I7E -G118!5cur3{IuCuBsNp6|n{yy~tOe-3?89knVleAw*zR__Y_?ltMgAyRur#++kWbW^3{=f1aCNUUq1) -4np8#x){EZi%YJo{+*ffa|blGaL){#JZ+3rA|_nKpO8yr6~zpjqKk=Qs;r#$TSOuoL@uYo7}vLuZ2AW -n-UIcfn<1PuJgyns>5=YCScVQc!xtn$V$Q9Yh(`b-qykeQD#q -JQ{uznDOy0d(iKR1W7nCbq0cM^II{NXeEGvk9C4J@utBMY5f}WY9nQ@&(?O?Z6QIs&dM#4z@ox|wXWfVYhMWPS%x0O2l;-_x -E{;7NjMX~iqfWAI&@*~2`vw7gwH(f=2jfr&r?|r!dv+S!6s7+Kk)}Sk%}^K)?x1bJs(;}eta2b<|9YJ -gQK3VyeVOudzOMZjP8-X>T5Ny;rTp73H^aJ#fx1c;Q81giD^#*g{0{%gt0fQY+W!>MLOX%O9B2s= -isK@Jbmi$Y17BIp -iaEX2K-=qt01Fq(BPs~;i=PDip35jpwrZ&513J@LadY|q2lk`J;{3O&4Cv -ln7dUlFsuirSB3$|1y84%`BlKO=Kd+4Nl@^@m#wO8ay!XwHJli1m9qM7IHP}Sk$Snk?GT}8%wpOWs3r -jJMr#LV`w5ut?;S3`zmjjHK^jP2MKrfGp5)#z5E1Ytrjmf$$pBjvuuKA*O?_5p##d%o#W+;ETT%HQ{! -Ye(o_2(kQ>{S~aOyDbXSz|HjzS5yrD3@2k@T*9o{*jX|^&k40$?2COU&gP)oa?J3#h8$r)+}rqI|V(r -$(sSa1DD_zojh{QU;ZGP;&+|Pi;N#O`9T9_pwHCU08cIOGuq#zch(|OKz%bl_zXv!$}2!*6RGRLvK=XgerDKDsdGo`*<_z3?)4%gc$IodmVQt3_?8Q5_M4?I@{ -F!(tDbL@#L3F8I#{T{*f-mK#I!QuiNpL#(=&rf`Pa_4id(G(A>HQJNyY?GTdc1?(LYVrx -ecYl>2ehK!t&Pwr}Y3&u?MT>nr?E-$M!y_3+Z)~XHW5dN=1UC)2&>VnbN8V_PzP_hB|F){>MbAFYayJ -JBeIoYiS5@mlO={rI#}(W|-S-q(YJD%hF`BW-}#U|0 -|Iwd^-X5oI%M}z%9@)~6rLLM!+&ebmEa=|8dlR(g{C`kO0|XQR000O8`*~JVZx#Tp_5lC -@ISK#(D*ylhaA|NaUv_0~WN&gWWNCABY-wUIUt(cnYjAIJbT4yxb7OCAW@%?GV`gXVRm+Z>KoGpoSB# -WvVx-)2$|+I~kv2J578;;|*5YY2J#36D=iAdf)*v2h*khE7<>~I~nkpCgSQ6tUEFGkHIjIl&E7=sY${ -CMjb%G9JPY!_(T0hYlG^N_-z@X#i#NHXqa<8fKeM^?a{SxWN4offCpE=apNF^nw@mv;g2J6vg4MdZCI -Q?QAny3K&s4aQzNmfOmD~6=MNl|OG`sjeEaxsJj#qCA;bWjbcOzAH=03WNwc+(#%b^+%?t_qAsH90Bv -#zS8deqsE@a+Ozxy92dqxC$Bj6CB#F!8JjVk5Sd!;9$)eZc6qgvR1~fkzu$s96?$8ob0u%!xwNY!y)J -7{7sdG@dKbaZ2^g|n<)ZD51&dCKbs7=CEUd}!K00fDuIE!FRbFPCc?BZ8Fykw_XPj5Is@p;}}cXaI7toOi;sp7?%?g8PUAEBMew0gkMaU@yWa*|L4zsV5bKV;3qNvfz(0DT$94{=aq{ogHvM69Tz2%wjKWevZ@9S*SaNc0dW(x2v -5O9_Viy8Apr)Z5yV$)d*Nyxi&38nT_!6d0{`r5Cd){R31}h&Kx6h^>rtH!F7f?$B1QY-O00;p4c~(>Y -dF!L89smI5XaE2z0001RX>c!Jc4cm4Z*nhWX>)XJX<{#5Vqs%zaBp&SFLQZwV{dL|X=g5QdEGs2d)qd -W-~B7FQu2^WBsy_iU(T)D$FZH(*Ch61xoP%nuR=+X#h4-BbLGcE)xQU%fzfJm~4F3X(2u_#%j` -AUL@{%dQ|DogVM(d9Ftf2Gy8oa=AmPOL5RbOnt;78XmUo@Ze`kMbe(3}bx0R^)fvYYaXWXhZ?6GTWlSvrIlL^~lXEe&YZ -FhD{h0O)mN*8rx&mjbG~2`5ia;d@4Jux8OYtNjsQ -P7FUmY&c0X!WhjBDE4#F5grwJ6Yz_SndEuWP|IE}gNI5|E!`mck%)5)9N@d2EOB8vcs0nzsx*x4WkgE -bxt1*zYF&t_r5rkwFrR@^a|KFlw#&t6=xB~Zka^MuWoVRFgm#0iRbBB5ZF3BdpsfMw(nzlQI?Vu%RK^ -7JwfR}2w!E~0@ntO-tKb~E<@VcZCKfMeOt4(laFj}Yi(vuPQ{^Bypl4e)IAdix@X(;J@q1NW}AC!|gM -l17Pd^&Kik14R1lcyImV_&kxb7f~_?`K-H5!`T%+2Vpq73NJaH7KyaUB*;Y;M}_YN-jKZ<08>|^CK3vCmS6lPm_gkYP -{WNzz_N3I|M(ur5h*7F=;74^LIL`tsKyPM-4g^M9I^1K{x=cmH{Cl3#Y7aa6Dxm!I!V}PHW}~zbnyOQ -9N=CpGG7c#%COGN9*Be{_-e+Xg -5jXI){N?*Tw>9Lps7-^EwZN^>)PAa -0o1Fiq3*DGG%I}T;MItMLFz<=IS1aD0ri3bJzJ*JFO7YYx{m4A;4)9k%zrs(?mzVd1S>4s+pQ#j)#D7 -uT&y{|G-i8YAnhay=So}D)p@KH+XkYX`qh@`^M={`O=i_HVL&Qu^6}tge0cN$Hp+V{0{WlFo{|lyEaC -}zDQDZSF3kRu@lQu5r`0I;kPXL>$%muK{*fAa@4%CTw}+n$Q#Jv+y)s^~lTy=Rnfb;R0L1KaB+NstW|*%R^;x6CPlWD`P#- -*snc`pC05TgO6%Vo#s^C+~6V}8$jS7d2$`)Y2pM@H#dNEvZ2w~$u!azt8`q!fo(`072xxLKZxIf_i)$ -gyY}y7>lMEv1yuJ9@R6Zu;HSa*H5|qMwiOGVNx0U)A5c2R#And`0&t=wOx%QKpl~+O -#8Ztx@~-$c&j-L{P2*`7}kh^SeW2%dB93YVBEm(N#h@7J&Jm8`H-{~c_KJ;5Q%1<0VfILr0U6=)MfdW -a#&?Q-7Ld`h10Ywn3swTz9QnyBXD&=^8#OGmt6D0=niyYQ;ljc{j0$U{Lb9j5Q`~Kk9qm!R|7R6TsTXwR0arZ6Iy-3 -$WVIN_g@1kiQ=64tj&{)9Jr}NVHu^5KZFJpe>Y#__0vVFj;Zk3ROQkg0AtJAg6> -|7k$lj~oBT?Le>rS4vM0J-;a99KE1Sy#?O3|CgTxQ439?4QgaAm}6r2SE4l+Et{p2rz* -G>i#2k3JQCzdOofMSgb9XBjxPf?&Zn*wB(@EZf3ZN2o_3BLp*56mFW=P;544J;JV)(d4mhPy%zj2J2aWS*t*Tx|!%mS|JNK+ZMMw*Mr*ZMeui|12E+V|Vo57wX>^qyL;-JRh8kXFKqJKX^7!Bu?Q -G0QAxU4FBC=+{Y;OIkeTklC-EF&=WFTWriY1GoBa(#}k=o+3_ -gR0`iVyEL-gKLaad5`hJN{m<$w>!w!{X@M8GG30NbY!h?wv7J(7&-c^ -+l{fK=2&KKlyeSH>v^q(FUyV+p8L!^;FVLoKe=_mFMa9wxS4YfP+`KKuOSiHFA#0=fmyfoDGB161Buw -iqi`nFOR%weP`DP$y;pNFmsJLNQz%(H4B_IL{I?4<8{Zt>i^>T~PvNUptbeG8rFHSTvFu_WFUvF%oDj --r3Q>xVio(jzF4wAq_xUOV88Ft;gl?JWFGloI~G=I+@{#I1+`gHc^@PmBRm6^&=JunFr3V$D|pf10`B -a2bgLEQH&xBoHGStiXG-4psz!aQ-~9SLKC7uYeIo4mOyKlOIC+X)$nn~XAx}VEL3ixl!ryiWKd2*sn$ -z|45}-Jst$~Ie$5jFDkAUF=mIiX%5Ab%G+Lp;DNt4#HqIyJj``l++jqM^j`6hK?N?v-j*sPcwS6~B6w -)9_{A%!kea~JPCIxV{wufbuukPt~2P$H3MQc_%SgoXSAb6Mqw@dRE<%cP2?bas0t)v59G=!@DB&Kv|6 -YBz#%Q|A)Y(x(LmpgYtrXJus!DtXKz9McHKWI2=L%P@z3^i23aM&S3Ux@#`1jWp9MA11!BQKL1-Sc*N -^YzrghEXBaFmlK?TZC~;xeH0XG#Jf<`*y*M3S6w+gHMZ2J2rR0eC%lH7v$*y=FU3pK^$`SYWu=DlOve -hSOwMrxS9!lDN>$4;yCeRpHBzWE@5e)RFPLT{l&nwzYojZd)A1?KjP~K5iSZ!M-dO6t>B|g6leTr9r7r_ug32r3$vVdfvasof@{e9MZOJ5oFEuIL)g+ZTy{v%H!AK2HM%Z;t -G|GAZ~L|mj3*c42hqInDW8Fs81A(d;99B4QhmYMYs8J?Aar1Gq4xZ%Tm+|9aJ$GEy)bL9Ufc;tS6Q8& -XaX;%GNXG#((Po^raL9o9AhUVi+7J0C7X5A09mBVcZ(>h*Q4rbo&%9&zQ){JW62=A_J>V)S*H&tJU^7 -!=sTv|1bnZ{p%qDYY0}t@9M%bzV*yuRfQtTxDeVjQ~CGW&D7SP2+C{@z6;4fD~^VZ$_;mxEcvzPeIiH -TL{zYqN&`##r-AlnyVKN-FqFHVL4oU4S=VJK!UE(s7>i(bh0|yehnH5uRmEoJQ9ei@F2NvQqoYPPuwm1f3T2C)0(I>9LTn+-DZkCL125j^f6+V`GsVei1NQxuM#@4RHC -F;;90ltgsnk$JSIWOBr;T8Jm?5XI%IqI2AM$+_wfj%s(!1;O&W|9-)f1k4+^Ph?VLL+5C{c92pBk*Yy -);H^siV(EBYY~j8j7HDj;p@r8jji!S%5TTQ*a1xmOQBsS%IQ(pPGzZQ!uU)%71 -~h&H1cIbxf~Y`cb9>Jjn4+yC(V*d=uQ315(r6}{4t^M;j|V60$ihMD59BOTZMR}w3fo}@Xh|g_> -|CVi!-C+o5K(8z8F_`^gsNt_wJYdgX6z8x<^($k?cD@qE23r-OWAY`N5|H)xdAL*`P!_HFjm}ZQS<{P -dqb;_Sia8s=Ln0X$(8LdweM1w>+~Zk7oCVueId(cL-lUHY`g8H?7ETuP+-IMSrc(@ACn};PZK+i~;bi -_@WJ9{p{HV>rM^W`3T< -$iJ%c_!t|>hOKscgxHP@_&Xu`WS4~idk8q|J_ZXlKrFowLTC3nkn4kCMDyco0p_yg~1*e62yk{i*+Iv -+)u?9K}%(|y7JZ*{SvNP34tgfs!pt2^Lh5zjx3}yJylrB=i;UGbhrnVu~{4!uE(!Ahv->CG|#c%_2`u -Z}%qbHX-+Mc@4Zf|cvPH{Xs@aAs91Ov99z6y}O7|Avcyq>uwx0t;SG2jk`Xkc@DnxivFa-Ai-c)k||T~pEw9>-_**Q&?T*I -wV+^)x(6~s+=vESm8J!=#lXaSx6YC=@lMlnMtw>XE -mIvh}XKDRAEyJc!gQ*N0DY8CbRTUcNbEax31ZgUn`NDlN$~noISi-|QB-3Sh& -A&B3!KxM^R_sYPCd|1i)PwZda57%WI_N|ei((XtDO>238IW>?%TA+yFu_$?79SLq$L?#7hQd6e> -=knWdm0_^dzDXF8gFYKWvG2I!W@rFmEDlpv#h99S -UIlGxlA4p$Tb$OYsTfX*ax`bdprp#Ab7S*DihFb;6{jM1(x13x)fP -J-sQ^hG_X*gp5MXH-k8mCZkEq50rkaBAz|ghEQPXYE8l^fkTu@oY+QlbL7@d6AzFb0Wwt#4s#(J_ewj -q43c%JZBD_9A;l>R!XQC$7c)I-}S%zqth2Cr;nSK*g5H(+2)!v-8w_dD@&XS-C)hVUJG?IH{?QL$v+hrPk9r%^iKv-gG!c8y?4PWc9}=!2)}&{!xd{JIYd|J+ou;1m2&xw>O*{UMt8wB8+ -qK{QTzmxxfh-{Kv=`J^;Y0$cdf(jhz=4&mIAzg(?OnErSj}T?zHwSX(S;08my3C?k*N(s#i1fIB`bu$ -WF_OElyn_rxq)st(yj(I1XZ54I&;%i_8ag=3L}r!j*Qp5C$DETj5F;KENx)89Bnd>`3@oEaD*Q1OPb% -Dd1N&M>(w!%Mup83|>~&MAfFfJ6=?@I3m)AU@k@qcOC5viFlDrpHuyk(98ivd5ZjV0zZbP67gWPSuvV(frmyV-;smqi#kD5CFFUtH>41Uy5dOh%ogQo{`pm0p -q|q#(^*0%jV?+>|nmb7pMq*0$P*su9a~o%l;_UgG*>2A2lnPO43;EpL!2Dz@l=M0)rxEvr!AC%hx@m_ -1Pzflcys*~#D}N1gXd(Y!3q&1-D|r0<`PpATT11kfJ)aNRC`#Qf_1>RkoOw9Mt(D|{GwPpRi0l527s| -1{yIz_!*DytWZ?(BdSaCL>sB~(}9PbR=AC5<8`=5>@;${fmz)WLI|AqUncXwCO^`2$Y{rVsvJMgSd -!DUe&MTfNw>tUyQi%8v9=m_SY))E)J%P5H{h>WHYKq@1#tVWCk-m6$dvqp -p?B1I(aFn8agHnq?v31Hg0ESkv}ip*0=B*D_EUW0|oQ2S(BF@ -sI#5!+ef!)Eh`egOj|AlJb^jxUwDI_8gKM^*oyFWaNl)1QoH$97x4CG0riHzInZ6KMCV(88ZKeiv2_i -6GdXQ5p0b_nQWH2A>^Y_-~Xe-#zSs9te!7O8yaZZrYDkN^AWJE96V*b|V6zmK=2G&Y%=z>$ -d3fb-Jt;rVn)`v;KWBhXPerxk|$apaun4qH23F#2FCMs8E*H;?=8PK9~E5Sxg6DShl(2iFaH8Cn!@D^ -WMLWNowgp*~GRS#fqghdQX?8in7hb`Lj!-80OIX>PaCI}uz9Zc-JCCX9FVVOB7_Up9FNnT(smqw&QCefFvD2CeO9cocDth@s=OsD74p>;! -uxLCwFGzi>yP?tKO&;z?;6~oSAQ!qGvHi^j#nVZ|_DrhSE%0?Tg6@}**k30C05 -A%;r;7=)JjLV#EOs<907S1|Z#9UZMQf6j*!Xz&L=wyoO@d*)r(z_lbbWpB3ISb_(OwN`gH#x+i58knF -b(=W^!TQ?ZE#$ljmrec;Ts%1mTHA#yWop_P%b00j+ -L6|Z&!-I73OcbE37D(Zt!O~#DzB)&vK!;p%QgDOF_V -HWbC#7k2x%@bhvvb_hce?r1~FxAC($b%m1j)Hk@74y+hSmh9$aSdv-3!E|QgK?Z!9ZoxprJnze<#h$N{& --3ii}PqqGG14MnRd(J_ZtZ0iC>b{n8Q5JA&6d*z$MXK$<_QnCYL4>r-#ymfA7wH7-gom;+49_{E!XtW -z!91N&IT4>X0KtltQ|KZ^A1f5e?^NTZi?T?Y_+XzOIR=-2BEuxSyv!FTKG7e-+MSt{wc{th$>en?05D -VW8n;Evm^Z{jb+;JHzDyuJ7I%^6r*4@mll8@XLQZgh!fz-qSNnOn|6VUBmE)0| -_z|r?$nCg-T6nN{q^(IRPUIp>xBUFj$(bE#_v)hz5Nt}eOLI#V -LhpO2dWDGh@Gc7+57Zk*d$O|Z-p)%CoZ_7m<3a0@=ZLlKXSSm=w_vi<%3BhZ8g&O4nR0yYtsbgVm^#>tioG?*A_v5Z#;YUYU9CUQ|EinW>Er)A_~WQl*+-clPb92B8@& -fjzANJ(sHN0(slF4$9o;Ix_U7F{SN>1C#0kKBiBMtjvA{h7~AUw3^JUL-oEY)Wx-s2=r>ZgvK$96X9rnxF5(K0tOsY6n6+}- -`|cisTvjg*|U=ic+vZexiA1_NL)m>CRaHn+e1v#}XfadDZ>lfmM4WAl%J_&i_S7U}G&in`Bx(eBR9Zg -d;=Ftyx`2Alq^?NW)euR1l^La$|%BV=nr1+dn2OFF4;!RRqr)8PuSrnyZ6kR1nGQN#wM -VwX1v>!#6MUq5Oei==!;$oJ-lPZs*IJ=D&Nm1q*z{tl{oMve@i{dDn0ESpRs;&TDnO{~nagkt+X%v@b -K1pK$98L4d@;b??xJn5IjuSNBt*)SK=Ul3FdeUArP2xFVm?2y!ajxM;H)(a1FDrlnJ*$dzf)M*yb~0a -15i3S5>pnio--%;y -Xv#m?|37})lC7_hkoj(Zi!AeP+Ao2&d9YXFS8gE%Xf%OV2=VBDzO6#gr7fCf1HBbikEl$yHC=kxpqdQ -Es_(-eDK?$KOdK!JFie@>{EOh!?bSI`T_3ulZmV~UV>YF7pAMt*)0Y?T4$cmvqx0y^+3DYoUL3xNItS+wJm~bJcSjdLoxZ(@pvc+5$;B_x>C5Qg -Y&eP%hH)n_E=h5j|boBbo@zLRnespy5{P^vQqmx%r^bG2roL)r7N3V}A0P@9YM9m7kqr-E -60WhQ2hiA`!f+q*hj*gEmet|K4d314tkl`Buj}D^f&B58l(et;*2kG>hNIC%juPEJpbPF| -itqld2#Pc8<4|49T-qv-H&@H;yH>EQU7+l~(2LZ{9EGQfR)`sSCjqgOv&0F0kbk6#?(^Jj;E@WHd=Lv -EWo{QUUf=ygAOaq#-!)gjkC1!#Z@7GXz|q?Bk;q*(<|Gg`YF8=X$)EQzNP6-UavDbgxIA_tBNBwk#{RTLMKtMqeH4zT#f#%S~zmJ(D4u=`Q -xKhBcRDgOKqcqeZn{J^`N!S{om4%CR3z!wUrfKN3a{dKvslRQFR+Vyy8535aqgJC7A_8%|c*B-Fq;c$ -CrcYF77H2iMw(eB>PFnX{93_9w*EYf}i&tE2ED%Oks1EEYlm+6$iAe5u&p0Qm-PoF-1`oseIn1TL1x` -vK#!=S)wnkU!eWib+pMM`tN=^=WjG-vA%WrUMwPO>@6Fq6sR^h -caj7ZK=NI`WrM_sqaZqWV!lLrPoPas8~*PF=R#Rdy#BxRK@My29B*fn-vRSz&F#4nze}@eep4cu0iOaYdNSCFia0HSah>E9K!kxzaK9t48&Y+|Eh6F -UN8N);I6sx?hbs1J^dGIZn83V4=($C%{LO0+>3W0~|NH%I8zs1sKqI^1MiZ#?E -iA$2sgc$AH?KBK-`MGE1;e{EbTcm_Gp2O;q|lHIOIqbz%(T0ybwHI$Co=iE0k+<64=|iL^d+F5==Pya -Xsu#Vi~CAnL>pRJ0GJTqKDQ*k4EEVIKR0Cj3OWb8)mBy1QDK?%R -pFaQj{OIq8Q1$U}H&AJwWV7lDciu&jPZHS4K?EAl^U0@jDddZUW2+1oW`qNB$K?DmK(25-i~1 -JIeJdWB3~(@$iR6MIMOVgM6+y3V}lYBh##;?9=&%f{`zGHJ{_~#A}`Y~X2p%;!;@DRKaEZfUL%&nogJ -@>*YTJ1dUaA%8Q{HfV3rTigH!zY*snlPhsk -xWqZo5H?3Nq|mk6dwhv5=pw%UL4geJ`Pr!ra+igUfgcHJbHPm3qSD-qf&91yDgwJq-%W_ss)R?N@vS_ -S@LM(3cwnp|GOGwBjypd$RiVLFRn9@&Jm)sfI~HQ9O1S{BBoXA^|MWt -4=Sy(JXeADY+aQb#qvT5k`#fwuS0G>kKY9P^SXZB|u0EURV;D=Re13Y2(&Na);eE%fe4Yc*0{?ZJ-FS -O`ad0*QDfjik1=MpFsnPE6`=?K%F0qfE$bQ6=blqVHYc*C<;myJOx`NL&-EMXl@fY3Li^G=(Z;vnP`g -QByzLn56`-f~NAipE5>UIHgM0EucvI1W&7bL~)ZJfV-^9FQ)!xtl*uXB`Zg!A;H1N}|dOMk;3r$|@`C -?vd6`;l6P!QyJQ^&{aW{YW@Nu(-R@`jNYkTC4HW>1_nYLWy1#$x>*F0H$v;)dBKs)bZMe#&5jt;2KDv -J`ZcRQhSC?qyr*r-TIx!B8UL&My2Rc(DWBse~eBJB3XeOL>!4A7sX1-9q^JlIy`E%NqFMaK)FG!0utf#<$Vcw7~tQOe)3>N2Nv)9(TiNx-FYcqZu -REaJIZ2gT&e=D@P8qRq);pYn0v_gv5eNDM-D2{p|3pM(dw6DxH{yXfL{)=W)d&)tLmIT?d}wIPL+a>i -5j~P18URHvavM9O)TokZ1}c`wo)mbv{kFWcj3u=Ywx)>U?zc@|gC;oqV{9Qen-CZ6O3p! -usQqJ9bG+D#C)mT&UpA6T2V_8zKT=L1158@R41x7B8?AFfA_SU|5Si21ww8TzF^~Mk!=tXgngjR7?vkzsX>azIgl -Y?C9c9Az+ncgN#x1@crR)-LzGd5lsGCCTOuS>b$BEfmsM5GAr`s0!r41?CdrmqKejtz&xx#MCzOxi0n -KHj|egVC}|+Fvl~W46*Umq8LmKt>wMeNO|ukjuj%h%QvZ5bps?rV0C^>@^=>Dtka=VNJ(WVrWroLUF^*oDt$EwgWP8Rxss>)^>9Gn5cPr6<+RHR6%WsybYhdoB; -WAq^EipIH(D0a(U4;1&4x5tb;NRNp8I!L!6%3a}Z*+?%4>B}^mc1cHNJb_jg(O>qX?ctu@{;7K(_7)$ -lo(!f*Lw^PTwA+9!Zp6F?F?NgF>IzRM=!K0W9de-9fIIX}j@E@T9fK66S}jr7a-jBspDJrx3N+9IL2; -hn3{(e8G*>|%Cs#$6SYfa3tG4*5589((C+^C8bmDT7rX(!U*;J>8XC<8-c=EKTD~d_(Z@9r4HaYbndt -r2x!3La8eSX3ZWhS3wxj9vM%CmmUNkrfTD&yw;MnFupd2jo;}zH?5ExGLki?L{ -AiJC#`A~+nBhs;3`B|jr+pD__iZgIG||?yswGv=k~`6)u%UhDp;8iyJ#Y%r67S*uP>C}^m$;U@RQ|E!gVxGXWO8dry7eQS)i -;S2eSiMPT`(M-QCAe++M2%vl9XB00l))qx<(GDdP}XNm|9do%?wwx@VT@M_I3@n4>J89*`SyI-k0sH8 -~IZi>a97BFmy5LDzr$q#1{LZPhEU+d;Jm#P1tiB_;u)%3?H&{oAo&ZAS0kzmML5)SO;}_$oB8RRR@W# -h=rBS9;0_EC=rGOkKgc#%3zr-;>o9O=NOoaJmLrQ4h<@>u?=hdkrLvf -h;O^SCI-A4=%CE>VW5@X)b)zRc*D^CWqH(W4RIehNkmKb>XJ7NV~~Fp|o&F- -<{NE@0`(!IEr)q%tdY4h9s;5JUw8A77<2kVBzYs9S?{72PBT8n}_F;f5`3nF%g{vhEB$v01b#C$kyxV -tjz}(sd|y_~_{~pi#i?!ybJ9i0j=c^6>E!Rb=@1$=>7P?#CX}&s|y>uWn@=Zbhin=~UltT}U`z+@jD6 -5>>Y>Ca@Wnl@crW@rcsg{T8?CAc6TZ*+Qgt1htWj)K7q;7yr?vs+X2@?%8M88=Ov{fwUmAVg+CASxI% -h{e6>vasQkm0=g~*6bHi8MX^k5ZU6&@e5jZVP-E&2pWGkr>^`#D0rJcokMAz+x>O&keX^cnUrDDTCg7 -q;zn{H*DMwjDVDb^RNYDAOs}E(bW7sW$-uGc~e7?@7T_xK~RfdZUU|h32u5Pe9r*l@_db|iv*c0S+P= -Y&^Ee5t+v;mTJ7GT2Sns#byr}y1*ZYqB%2I1Fc4*Dn`K@c6oZ;?TX2#&k$f!> -Ioz3iitpm5a=DmYdNR}zCj2rRmTH -NiZzU@bG`f;2;VPJ5n3Gt1C^MvK -fU-SLKP8G(gRPmi0lzhS_NYmbmYWCu@On|*nqZCZP5nr)j(`pV1^FRE6?S?@z?G`k2K?~!$zi?g>U&+B^s{(udAG^MtyxWp6LZjP43)6uh|lY_HgI*ukz=>2P-VRDgL<%V7jF3~kIXYaUf-9uEu -pFDZes|RB;&&z~H!e|02*3SW!QJ&;Y9ymOM?77+m*41U8suZgMum~v->^AFHTv6{&BW9@{eLbhQWJRO -1u96IXpjc1i1)AE%Np+JXnc|>;>c(gGQYjca_du1(O06c)Ll};9lB!CkCk>nRN-SDvZyw#B -`^ZEsI=&P(qGMGrrvn!E`I82Hh(i%g>4w~+j|*6nBtO{!#4 -?}}H6Q{7|U4gKNKdsF0JZoBf>?n{lPs)h%8>S@<7h*GcV$PhpKG_!Ql;YZ`-a1os* -r`@IlML_Wdv)q5XF>th!)SX%1cj)NAmzbUcdj14h~2W7!M7OnFI3B(d_rG56IVU0@aD}a{ -bn+8UMcf6>|f%{_v>*n0J_&wMTQxh9Q&Cy)u()Jk|vS2G9P!`yx?H2C%yG=v$zkhXC<7c?$+R>@rQi9 -drGQKoa1)vQ!-wj~^O*b0eB2bJ>TC{l$>Ec!%mR#7>WPaE&TI-D^OgYlt`wQ9A9xMs=k8Hh8-h(U*<> -Bnudvzh#Hx;n&M{pr>;DkM8$<29-El-Od2my?aci_r?AC`-i>lhqau)8J)d+KK%aiZdAs%B_6$8FUtxgs_5wabo=Sk$K -P)c^(oa$@<2CE#JxV5nDHTMTIK=<9TAFTF^|!khYwZP5XXVo-jREg*w#vQw#9-*m@3s6weN?vU1{}%3G~`T%k^qrUuVxcpUj1r0?4B6_r< -_#+V39K|V)|SPGluC5X2-aWO4d5MY2;o5x6IS-rE~bco@vMNsC1D8jm&FUzZLZ_O6GGqINGX`BytqhD -_U;)kz>XY7+eLLaWkl+3CTM0O72@VMm^u0?p0^N>1~1l2)J_PU5msmlMcV&! -C?N0O7ru0bZT&<8;TW`EmZG<{8)VZPc0P`6umB;W?p`YbLD?T8_wwu_l3h@<@ZMA>n;Af;0#X6CnLLP -eGM0KGFF-kYC1LWd&c|kACC5=r@t*$p6XFz!+9aV_>yOXyIXg{v|2>_CrMBZG3ug{xjP7aw(r)gMhyl -Fyyuh*h~2I96y+q;#*L33#wQmw>iR-QV3ScPTGWM3r+x7wZ6XmG)+tiDkT0mJBA@{8(Vu7e@Zf@4#a8 -j3jQJU6FYSv4aM_H+4?m};XCxicA$SnzMy|g}3gDpz43bX>sIRS-H`2&7MZbOV4vz -v1X<=!3Sa0Wdg1vzS7Tp!93UO$XG>V*f~fb({>E@r#1LbIibsYMNn;Bf*JwUUr4QG(eaT*!A`B^kuWeDGm5`XaBiGGNDeajrARr -7f_N4cQI(r6B~ky@DXXIfb=Pa7XQWZOTKs!XN!&a>Avd9K}{aq2cB>#Wx~ -`sD*F_x$(#8X5Qk-$`xKGZQ*WE1S;JRO{Z6T6FFlqDAC_7Ht*(y1&(1eYC00)AsBks88RT4zPmLs$;d -Rb^5M%^boAzR&1>o^N7#4kE`+njL1;2<tiGkZlMFHW~$1JD&H6AE;Di#u=LR$o_hd;FtMy#HvMbjVn&f0oh85`DDqI#9ec&<@LX-yStH_(; -~yW3nH|ViAK(mhLmdeMKysyB4>2>3Z0NU3`2O*q7HrG|W6#VNwif=r%N<;O@@A1PX*fG;Spa=GE!eVk -P`3ta`LbfS`V!o&iuk5ww^Ec7+^?YOTGs_Y0?~U5_0bf107%22!A^!%Z5xBy-|&qg`hD{bcV|)9)@W@ -$jBtL?AOlk^WNkg8QMc9175aLk!e*X@&_L}q_LIjkKnyZddYYLwaC}c6WkMq6Y|@6tC9I{_u -R&YKi;BdUKAI+7(*f-|Ua%#X#}MY>_oT>WGjz^pq3j5)D_?vMBLrhv2Y?7FcWZ&QJLhM{xahYi5tyS+ -5`pTB=W`*0`lD*7!Eyw6DGNcDT%Y=?LOsParGd3$)g? -xSY%uObUocgi!GzP7hpXUz|-&`F$54*%N1_<*06m#fRrRVv?<2P$Ph=iJX}mJZ3+v7<((|DlVw3Vl2} -LYV2DjQc&|A~_1={l7fA%pITVn94r;N=Eme-k6>8F5+kUg_*b!s4Sk(G?bAzoWJ;i-AC)#!fLlq1NV_ -fw*&ALoujX*pB#FX+_=%Q*rx`yU9>mqg^FNwo0fPeCZUX}ZLWqh!?)<+KrjtxS<+y3^%{6H}8Vu@VgT -vqc=;0MECIlb@KY}OD};c37;-q420b@8Jv%$m~${qtwa!3Rfqg(n-W63xZB_AqYq_y$gIO?LxJqGnVb -3tw<-1q4Q+y)QlVDRg^3_{zfafmg<~yry -+I8xo7eIMG=TiaAtiKa+ja8GR(V@%;q#q?I&KqCnnA89~#_z|tuyVN5Jy;B7WDC!8_qD`wZ6xP%#AbL -1xxb|~>me0=8);k$!6H~_%z!Kc7H)4D>j@T8y5ADU~E@|m3Erkv5?zU}y-OYB*tthUd>%6RjM;b&s&c -T&Avl6TYPjWd109&EwbKc;VS(VQ5yh=RG@D}%1I7@jUQjY$5YB*}a|7Fg_*`Q^^GVFer?hW+Ty|xBt) -DQDki%`tB&5Y23>T333;AREW^KE5M8hQ7sL$+Wg5gC*@!F8;0xOBV8fhExk;Fd?H??k;^mK14-f&fg1 -`a%;^5j}&kAiJ_f_wzJMOxoWb1}GnW`6N~amTw_@p>uOc*dIIFR<2+5Ij-`;B*jdvE>jPt(V=aeEu!f -CHBMOnMO(x?sTiaG$+cprc1%?|)>=lcxQ<*G^z*&*tD<@ -BMuMImy+G*2S_4*^>v=SbbyNr;JhH31)Oad41Y4Av<^_IUA7q13d%aOGKj^OyL2nzT%MxO(r4(}Tp*I -)YPx-FYEL_BjSD_L2XQ;IxBaIsOvCOxrS&K>uMCQ_t{*{_Y2#7BaLVSKX}&?dMwrPgC9uh(4UPtuD29 -PV%zT^re2l1-BRyd0b#9{xN!KfL&M)%(xs7eHlXap4;H28=a^WQgnMI{wn7&hGRK?b+NzgRq~!J@cdv -HWUTehSx5%>8!0ta9Bgo2=yWeZT;RHp1fEwQh>2;hnSMrhJ}5Tzn3+);sByf0z0nI5fs3DwKrtuSnu9 -|89^XOthOTJI2Di5Dk=QCUlg9CA}N3za?zmrIVp65TV)%Q`-2#ozb|_X?v?`7VQHmZo0@d+ID!r$#Al -c8$tS^Rxpf2>3t5I8CZJotKzrZi!>5Ft_#fXa?Ui4zr0-T2#7T9E4zGy&v+q9sJsB^s`9d=gZF46q5m3hw2wyRJeQPPU3I|RLA{v5&RgN{+ -hgS;R1P7BWzh3N1+iC9ognrh0Mq32b^MRKz*6`<&G37mAR892m|eK7&!E$^@x)DFnu;!J9hF -Xj~?mLEDq>|iKSfy2q1D&n{1*!RYd93E;$R8r0C>cQL&)dq0KLq71+TUY0WOY$+;~tf-G(jl$Xt_qr) -if7;63O=PP^2dDX6s!l(FFOmB(L4K6@0us9wp -6)3${w_Vj{UjsAw8N5_@wjrgni(Uw0N!8gQWH0tXS?5AugFi%MV7fSZmu2~6-2U6C#XE<_CH04yj7zsP9knkzJ6PAhSYtN -ovCpHsp&_)r7!?%-v2Op;_=U%DH!f&U<%qcPRN=SYdUBb7|>$ESC$+?{}A|LEU7N>R!k?3HwMG*#zeHydKG#8M}RV%FF^~v{bTZ^WH!)5?JoNI79Et#w0nc=nae%(CTIHd1GLTiHmx!8wiGO&wPEAs7c -!+zpKx6HY^?nwX?=y+u;s44grY$)kY}9ZIr^OC&N9a$1Cdra>;N8N)}69bn|qTr~oEi$c*n&*|>AW8| -q@@mj!u7NJGS88tf}`}QJLw`eGFDCiz@P$=+~4L)o+8_A4Vvu@IVh|t{WG-&giCliCin^PHaAjnCR4b -GWUott&Vk=vc;*gS3;8U4nYHAl|`7)VgpG@4&S6ziiDLWeWxwV+cQ#L=6qkyvUTwhpNn94166U%TGvP -ErM;b4KSXmiDU?cwlHgyh?Br-}dqR49($^>&}T0s)^2o!}rsd3rTTpO8a-`H>!-SQn*^i00Y>Y+tg-s -k(8C%{4OO~Am~|=iG;6Se3>NFSIJYS^YRulKn6I>y={z7nnefa&ySAugw#F5Rye}n75`BY@iU2MZhmU -pkE9$lh`85}x|Siw$V_M~zn*Vo>DD$M_2uC%ftxwDAbbwAAw>G4lmEp?pJY{%yNhqL6iXjcorY_S#u$8{fk7Ir8( -g!WCexyN$g-F;?%t$J=Vr2*b?6*#|3tc!0gcL&^AyJu!t6tw!i3UFhNxWfydZgZ#d7(SY0da9Hq0SYp -;$8Urz=!nMt=b5MF$jWMWy5MNwFSE&ttE724S$8CtfV^l)nfI1~M4ofTv|j3413=7fSVcR0HmXCIKis -fsV0K;0Cwd8)G`3(`M4ZyRD2-=H}Ws^5#`!W1pH~X!dlX_A8@$KavE`>q*FgI4EmwuC^pruYDb{j3NF -P*&)~uCA8yml4=EdGKUTry;*D1#gVphU9TmcNjIUz^+%TTZ}%ya;#133?g_X-BNm?KVi8POhHDPRAxS -KT$mNUTvIqTm25o4NTEjb@#8jA)kjxNw<50Ho0e#7*H` -`fxQ9xD7F=t_H2{4`=Vr_Mu&t}_eWZkcc7t?pb^GCyN0d*tb8qKK{_765$}mEC7YXXa$KSOC*=Xl=i} -)7gqiI+Ew#9-%eL;#@$@TbB9lA!FQ^C=AbUdKiGMX3Drjy_H++Z5Veq=;k(Bxv~;PS59D7+u+M|HRc5 -E<*BN|LVE8AYkrBHA=rJ4mu&@tp(buepJ^b`jheUDHj_CZN=X@X#8&bz^R0$>Zr7s{i?K$BdwDeVxHy -2Y>yqW9aoUa4*-|pzQzlIWxZ_9{o`~IBC{REOUHofTN(DWpp(yNPbSLeNL=GsiHwNFY*Zvaks~LctE`l8_ -32f$z$$a7_8(Bf~_kL(kcOt?&y?3qX|{le4Dh0Kfqj~5%&VA(_7o$xkxbQY3bXY8H($q{$|OM`0CMB{ -s8=ZoXpZJGm^ORpx6cg)h7C!cXo&VRQ;M63g}l3xKx3T$v1nPJvY8kf4R?M9xD>E#jw6vq|ncpgU&gL -m$B!+5uRJ2IJ758GMy(Sg_$nVzRMwXuj)MKyl#LTCs)Si>cir(*te;uW>5ZkrA<&lKi42kOTSg0xv*y -$U1Wp!lPcnmht%6G_}_zQYoGr8HQTa4&@Euk{?m5T0c*^qCA7vY0{1H^zw_+TF&@F+gu`x55H^p3LapduI-Q%-HpyX2er3nvZhh$#RqfOL8=%`N+&kwc3JF-w?$IH -fuweSM8*=7v3Ky?wgI;A=N(8L{TQI(Uq!ZDfWm@qvu~H*VDdE7aPXweC#wevMb2(4T-dXZW&8h->za< -l9rRAqI0S%N8$bGcT-vZ-(t4UIM_5ncmNiEUbb3Wxtl6LqLNG06T7aKN~nk3Y+B1tmrnq2`3F; -$4~NSG9#@}WmQUg)bP>Mz2W00A1xH56#JJnWh%GP2qX>cjN_@+?M)>3_ApR_;UA99@=CC-SjF4f1I7J -go1Tf4#9vkSX9uf2{b(0r57oL1^m+KiDKoRn44>=`cON-r#3@x5+I_S)eCPsF-l)3N!zY}E+bUJ7wpW -&`DZUd=)3Idd9zOo=qxIf!l~aQSW%hQV+^XSPI`wv+Jl@;=es#T>Tkret_8#tjzp|d;fvUy>s_i{|_} -xdppg5iCf)AmMM~{tywSiX9fY*DE8>qzKnx`Oi{p~yQHWGuN7DL#g2pkOLm0LE7OSVN*OxBUiV+^pE& -MEl?u(>PiD59Rb{pdjX38UbxsJqQW%b0j^^0E%jaQ}i{pd*F&wMtL&$z-`mfdf*Ib=m5c+0w|q$G)(W -L|uN8dn=TSm23--6inv1%fh_= -<*5JdZPaI1WIowBHTwy0V^rOJhRAZhi?X9e+4R01wAf9Nr`_Y$_@Ph}JwAom-sD@OvIEMO&I#OHO?;> -nT=XLlV6*8#2KAweTFflVEssz3Rt-uOWf8xil-om0hI -$0FrfQ7zCk&N4!noaYtRh@4raAsmsl2VzckjuQ;UK~c1m2|%v0&>fn>T2S;;VG{nw1weiSz{&FF -`U|;TSL&98|_a*urMC%S|d`5$b7fkX8tf?uiGU&6#R#S=&W|GM3bNjf6WfD&h4$*Mrlr(6 -8Rb}q0G9f1I*p^2jBjiv>XHnX$1J$>f(EY{t8gy;r;o!vLm5N=PWlQ9m^%_$}$yeF9 -G}XTA`+!A=SOpimp;>9411)(D(O$*zGM!hLHNavq^~|gHEmQx7XBsvkDJ)E -(;FMIKviwFK(ULb2LW0HRU&-xFQ*>PwS -kOF+ZgXo5PrV!M5kAFk;HNhsxA9zm3$1Hb>B*H4RDNjkX**TPFL3g5W!q0|29ve=*EWQ3%r7{FV+U=t -_b@#6Fc+xhJ}t8^U&pOs&B+h>S#k=Ci~`@iw#gW-H;x9Os;6BD%BKcFD9wKnTx|C5pXF)C3S)R+mA@r -H0WqIhrTVQzaG8#SoSJ1ir>fY%g~a*qV90+D8J-`4)_?Sq})xT@SK#ti^K;Wm+a%yI%O3#J!11*zbkDvj(_YxQyr-PI?XfDw*azW`5W8BnJ&ByE*260d(80n@vE;_$XADB)F-4FlIui3{3y!Q*e@ro$iM?L=PVA1q&U#t3%Jm -k5|E(AFm=u%_T0EYGp2~!uyzn5)KOsZbYb=MB3Lu~uw?d)>2rmpCP$BYNdou*{bFgF0xp^*<&ppuX$r -q(J$6RjUW26*+e?QIPqQi*oLf1|pcKMzP`=rvIG20;fWo_As@?|*+;(JMbKv>V92Hmfgi@#f1ATzdSA ->ZXS)RqSDb9*`iaF3!QIzQ-U;)#)ZW3jExzz!tm~m3GVqxQoe|ML-s4CcYV8h~z5X96RK%TdltjXTMDfkZ2DlmR*p%@qrSIx(2 -tqez#c2=~+QV_J8#DR86IYwjHg2mL0!Hl*_dq%7=xACb-j%6a|_GC|?mD<&uKsy5R-=Iw0q`BfJ)n1y -&f-$H9D#)XkM~BBR&i!SgsJHdi5wPw9*=kJO$S3WOKy -GIpH1WO*`9*bx973zLINfyN;%?^mwYoN3t82qx*X)A7i;mTJYeYBWRtraVmal`Aj#jc7Xkh`?FZa>U6Oj`; -g>v;Uo4FpyoF|;)WOUDSdN(nVqJ=fnS=~^qIj-LiMp$u&(w%RJDVQDjrhF^z|0?>+)DzqL-H1*1KhZj -z|zo!c^7nYX2uz7ZeO|$#$6FY1;eN-5Kz}|h~Yb9SCzVN}_JwFcUc6hhb@AN*d8UlXuVQ+8uV_(|)Fv -MSNLosY2^fe9<))|vw6S0dSh+|g9Zb?zz_=lVClr9l)`kd$oRpEZL> -jlK%v^;aOt^{4}1L;YX>C9xSchbps=>abNE+%xuVyTH}l{&371%ea*`B=yXfkQ{Y$&-Go8bsY2 -6f0Xu1;NFrmr!=D!X2sQ3rm$Pmf>~j&94Y_JW667H!rxk7bWdWl8j48e- -40Mqoc;~)dyPrh%UA`yp7WOE*Iznh1Yqg2%6FB%jKx#sTie|CG#hP*+8}M^x-4W^M0i4c6YuMPpz)J4 -euO*voDR6sZoUt;8*tzM^!x_tPHe7QC7;~+2O0VpcJxRU{ohVNK1XuHjEnRExoG2I=HM+vAn%rr{ei9 -ubqnX?9)0Gk57JfDo(D}LiPDiE~?}CS}i_1c3Zqm{n*W+nlF!Dp1Q+RJDW6AR(7KL$jh9Tb5lLrRUL+$7!%@1J7>Jag(nT5Z{XleJk-)gJ=*{D!W<*rfw$kwS6)cE_&XdmxUK<{|6#^wsI+zZK!&KNwQPptOhmk4{wSn28dW(YW)9o=Hxu~cJ^(kFF?n)8!x_D?FSMc2 -_-a2tAMadIDl5qv23W%-F1av{^V=iON6{g;6SAFQO8YR|4(8RZIWxnx%ST6&Rnlh4bCQZCRx!=KPK+> -W-mzdx5!=eD`ofI?Ht~@apkv!6Y!hL8>0mCi*NVf$vszB;R6391t3Ys4hN_Rc`TXe~S)Xhj;b})*XusEX=v~ok0CMx7%mfXlYA -SF!SDx|KP%klV`ku~874bY4O=9aM%R`G8#r_@18M%2ec#}qwWlBtz!7{01@u@4e_*MukcO{PhOnP4(> -SFA_RxP=0;n{Kxx`m)fnaKc@Y^H*^;H@!Kba|Jzbz0!@qG05F8qVObBZB^|Q@r|QT`0?(jf(pC>qsMc -}v(s^0(&U~o{;^+-n(_aj|kY?r7LM33iHHp&+gm#A;la||~0Jo!@9RqZ%3{sE{Cy~ ->$qX>6|IlGHzygIbbg1ja^OkCmy%^?WDi84nnR=?eg}Mni$L=%k^KR>&Eh#3S0*6C96AN}R%>995S|a -ItNL$=uA#E#J(e%cBe)-&wvAp{3|;Nn{Kc%VcDr@}^`0o-Oc163*bc@P6^&meI7^Ichv!Iq -=1&u%mND9)8KQ{iMa2b_h#``d*@kJ6Fq$FKw&tKY`9r&9qh&b-D*n?>M8fjns>xxsz!pGp_-cuE#Q>V -3Z~em~W!#}cchx!VMN49K$&yEQbq!L*>ryY{(m7;x5}u7P>Pg)K1G${XGwdtrHUj7hFV2sXB;(A{77+ -W~w884!^x4#zTHVB^O1`n8qB5yx`83fft^|w`Omz}Zu9RDxl+FDl$37-@%EMA$ivV>knbtGs8LYf5t_ -WIHX>f{C@*BFxBrWBbqXNh6_Pq0GcW(`CpRe(9x@a>JRmB+K(9!caxl}=~*qMq|L??NP`n1JvGP>j@5 -@osxlSr%4x8}emL$5U?o7=n>eQ?uP_0f8)WC};{}VfZwpX_HMDTKL^KkoeT3&h5Pvb& -oL^v~UKuEMx-I}jy>i(=t{?C$menUnMGHujh8dPG~SOTigDT69mu_e9)Zuf?DYEvsX1cfjAlZdP@->b -Zz~ummiB^^X4o^m}4=;X_YxJu7c*z1;t9z;1JQ2B}R^yPD$&T(J_-k3Hn67_Tp0qGqscB5BRAsDlR=2J(l -U14W|C4^^%`)7yGPtRU(K;--PKi$N|tPEK-EGveg!1uNKj#IXfcmXPOfw@*0ug$3$HEDjbZHI-@wG~sa*8V0j+`!L*uhQX@t4}(39|GKYdGH -3EEF(AP;NlUT1RC))>_?JT&6bGRuf-rbv_8e%=@Wl3bG{AFUIAat$gR%~gn>D*-jK5QV-HtRvSi2h`5 -FInkiEQfn%3GrqE0AI$=Sx?>SrH2-d!7=hy2K|tz-3&6%VzX4MOhp#Xwc4kUp7zN-VCOOeZV*78ni`l -EP;Jjr)(IrNON~zKDmVH0=Z)*E(XNittuZBu)y`<^io^J8uefGp_mgRzW=jAD{K_^6P?W6`k{S}J=3+Ze)M1Rf_8|5t#g2%}v#Xp{)mk2K^ry -5!OO6KQB$Yvyvu`sFuhR2kI -D#rHK%h{;ohH3}s~i7OwBJRZs4s8R3P1;5o5%%NsrKL# -;_hA02u4AfpzqdnlMJs{=WOB$iLjyr~S5*f-k`uP7c5C4xqlnD_ml*ezlF&w=s^caE?A6x`g=a%GKkq -X{JV4#SZEtUP>+n-yq@wJan4bw`{*Yiwy?dNl?XLEsbx -mwyzDl7c4pVL>TUvooc;@-q3`N~rh-5KdCIU=%g$3*jzE|S=85dG-q{{23Oc4wC`w9g -W&)#dPmBf;VxhqR&bLX(OOR`G7}((>oB3@{oa{=9MnvE3y&Jh5`LalC8j6x76>%3Vopo)U4URF^ -7Rk7~l6nwEISBT7Tl?S;vn59#;t`6j-(NunD}M}Ri!F@}q)G541R)rM_NKAWch4}~aVPZ071a_n=V&t -_C2bz`?~uJU=HbLI^ZX~&u*iY)Z)Qv@$k&VmWkb40UYuaD5gP-RAtL!lW&(+kg!bmxO|YtW*vrfun~3 -F@QOV=3vownN8^U#6_?Y?I0hJHUj*D_o!B2@A^WN9M3d#oEQW%V*?yIk?2+u^0~t83=*mSd)tigtS5Q -OTI+DW^;{cdU4KL8`;Y{rR=P^NogA&}$owJ4UjzQLWjfcP;i#;PyKELsJ%*a`1V! -?x&S64{ALwF8#k&HGqFKNU2zf9r^ZByksFGY&mJkIone$xGORKvwe_{5oUUH^d{c2i#?i=rvt1%h0rv -dG=`?=XsYKhf|*P9eHF -?$OeC-#k2Y=K|r_`PrE@PIL>5G$ji#6~M`uklt{8I@NtN9hOcA)kLXHS<+=7sCfVhn=KD7<792z}0n! -4%y1%svwU~)d5F>z)$#a#;O{q8So%BYY?wf$}~xiS#Elu8R5UpbLFzSrkHOG3fdi6ZuB^?@M>^AIyyf -)eAUI`ZrFj0k6yY440zTzVNR3J!N`c7e>pi`*Yz@rgpYO;eBozCtlL*xmUz)(akVJ^vTwfnKz}|410h -j#Ku&&14fCBMajp$i5T?XdwEx>HAcsxng;wiBKYqt*9%WFrNoFsO&O&77?pf6kY1C*^B_5_Rt3d;keL2n}%*t?PU -T25=RFWx&&9BD8Cc;FS9xrJs%A=J|30=W>bGhPBj1^us~SmA8_@pCHf`2CVRlqNgHNB%y@Q)Z>CRqqU -%!%G>+ypj2U(MaH@U}mqZ%}w)V#bzG?LpMgr(%h%L#Z^c-sIDclvoyWH#>Dc&yUYbg6v(3A=4G@!- -`Ylto~6kxtHqLvE{c4*oHQM(*!BheIH19L6E3|9P*K5wxY$*qYci@HUZ$SzZ$(>yL=aT1bFcjCy^`W) -+v~2z2-jM3Bv_9qW^Q#{8ZCI#hN%I^^XQqCIZ{V}fXyxpcen?gd(-~CX{SNjaz93w)hg(Fq}^*nJuKY -Vn32uQZqY(Ckc|HM+9KLle#@_p|_vE9|@`m56wf{YcQ2(;$hLULVebP ->+*95GMQY~;8ET(;4w}0&Py{%CC+Yjc1YL6)YaLFu}Mti4&y$~@w^Co3s_Z?LD8(EmyRr{n9)rlP$pnq|mQ -Zi+!y%?vCapLkvG;9LPS!_F@yLVAyPu6X$@NxIkxZ`=m`h9N?F3XIZMtl{i!Nh^JXufx6QW-TVcn`ks -)Q@Q&g2-2=cH&!%V!5@dZm>+bgH5E|)dPxXB&&Z)k61wZbIr(`=sWh%c;#`zQ$JFOs8@x>NzPUXN)wx -j0Bx+ESB8?e7$R|y>J`F%p<7A;+JyHkT1$_by9iKof7t_Zf(cxPDmErx6pZtF3ags-C7H5&5ognh3o|#PnbOzeIM%LBq39eRlt5?@ -jGGcTzgOoL0uOdP$J<&TN845kN5%ard -Sebbv(nOEbd#Skrl@E>bPg7F@_Auz=teIHy4Q*9?`O2ly%`EOFymv@}lL?&_~wOIBI4!_MVLn;_k%3C -dq98I(cA30f5v6h^(XSu{`Fd92I%4rt8OS36nQ&skUP{V87S?o|&%lf(r*{j?rFk8i#V@09x6QxCwa{ -ew!Nu?z&?DL&>@S2O@g>Sy~ZBH$JbPQ=W{p6UG(ugk7w*#4|pHA@qRky~QZi*Yc`Q=6vN_R_Icy@m+Y-{PfHdnVaVIxd7zywSjDH^m;+MVmr6=;codP$yM9pX*T7 -)zyAy0E(5=4X}Viu4KhOaivln98iLn+~mb4=cHO|Hxl=zit?KbV}H|Wcsh{`b*WrETwo|Nau6+@)aX- -FHMs&P35UecLQRkv=*{ka+u_=-W;xOW`(jC{c2IbSFjzu1^F$tXVx6V_LFNSGa=aPa -1;(5^5ciJ%U4V;^uj#wjW0*{51JQT(9n&*Ng^$d>x3j4zXh_+m!sm2pH`xmrVEWzyftzuU1RPj5yqay -jg)Mrc$mO=SA$$AaEJsAx@dNpZC-$O8nnfXrWZLKwAx!s#G2_RLvqIh77b7t{^f!nz;&l9|UU0^A_w)A?c@2q8EY#iD#%GmS)FK{Rf7Xy-Scz8_9)EZaF -*v0S)kgdmA;D>^ML*b=qFk4l4a%HgjhQcb84NrGlEE%Pu8KJCxjD`*ObiI2O)K;SrvO$+&b8q5vaADu -Pz!e+snB?gj<~1KzPA_XR@tSv9f-8$x74HML;axzm=PhFpk^8mbc-7_QV}hfVB-aN>Ud3%-nawd*$ZvYs>w}Cnq`bm0)1 -)QZJK}7Tbk)R`mFws4>(Syv%75BCIqIrh)=q(zEL0dAwzkH_ED?9z_)|hB&b9}6stN76xCnNRx(ciqQ7>C*Cw8%3wP&KiOAgc(7jy{_Rvj{X}4fG0VG!oQ9)Wk^! -c+Hk>kNgqLNvsat8f@Rdv*(XoJCj|P!LB=McV()EW-N>8j{KASY>E9TzU1&Ri{GtaX{bo6+GM#dk{Jb -R!fJgxAt*lD=Bo>=pHoaEmPG@Q_&6c1tC_HB2s+4;X@LKN5$T#Qa|34~k8}C-F`kj{gNLdzCHD^d=YOG|_T{Z#}S#Yi -nn*+ca-nAXSi4L>Wn_2NjaKA_EQY{Z4(8s0V*o2V_gKPp1Q?nDz|XCIyL^5Y&ziqVk&jKbIf_%rH$ah -)Qum_f*sg+axX}#2X$4(8&)9(*bCP8X8AkK(j|Xu;wP)re!_GHMWhUt7Dtl&_W@Ld`(TVr&3wDM#2e; -H7XV&22*paxlLY!2Q`2#t3DD0N{!4x>~{e?4JvTi^k9UU`Ai8zE0Aq#$}&-9Ia{gXvdkwbQK`OI5Ez38)&&il00bCb<*Z2_BBo<--LU8A2*Iy@EHTAsNF@>~ptn%(~oES7`_uuM2qbwFD>AqC@Nn# -eM{Yu^`=>Lzc5XgHg#T39wTajB>IME03Xgo>sR;<{h%P%wKo-F))~(&y)&gd=H*gDmi0X&RxN;~;qbg)#wT`QY&I{n7bFlcH}8c*Ph71gsP*N8Mt>%5obVKfCV1c -EUEHxu?Us6Nx;JyAMQ3U0tJdp}9i}*2Z)tfKAwFSk_re1)8zr%vYZ?|Y9>S%Z1!_mI@hx6GsFL -fB(`l8das58mN#=4mJ;^MExh{02los{ -5~feRb^Rv*Vvz=E^Jn{HL=T5t{joWfsjTDVB{-=t{+?CC}D-0rr%WKzX-0?*=Ze^_wKoCPUJBlGHMtZy*AE91S>h^wi+NM}B?=mS&Z9nR(S|Nx*i~Vy;@_WJ-O -4Y>}le8S|0O%h}@i(J*sZKXkeuuuk0RT&+glIqNguhR4)E$*tbPYnt5+{+y|4r0P`{3qx#tnW=;x5?z -3~4lMa}s%2M%zM6#2rSJDuOX7jhaqmakw^xsf*|z5DAyM3N -Q2hFZWX{$WVY1~OABN_hhVR;umP{e;Paj4zqBV{p8<6fWioW0HHw>dIHE$6*?obwX${cTGVN|spOpmI -4J<}Lvx~=&&Hjs74*B+kEpJa){$)jv~t)a!RgbuA22rqWw+rLRWc(vGn3;vwu5 -*P{B&H{bzH1s+t4CrojRLfqp9V(HUjC=Rsj!GAc3Kr>s{M^q4BgHQ746<=CUQAiuiR4rkbOf)g8UUx;n+4wLmqcKsah0sQw8nEdMY*8$79E(4aA6?t`6W$kP_^=!yr -8H)Vz$DjNhr&R@v>MEJz(YSKhGCUtb;bry4E4T79^e>)cf<5xLwKp!xC589jtrWIs0pnZnXzm^7RfSG -%=>M>=6pkZn^yS(byD}|HHmp*Z%nGw7f_$<76}f8iia5;5Wg(tG;S|C=7j=F6c83)o)7(go(TZd`s+0 -+Kmv5qA$m4+2v<_X>&C9nBdy2~N0@D{wWNyZLnGYM96OIr|)pwZfn=5rJM!DWF6%RSrm!Y|}8vjvK!~ -8mMLm{l-*wBp1UMvkRvTxQ1@B)zt3~xT7cnUTf62M4UDDvg(N@7@7X<;XWCZ%tl=byAh<$8vx=?hY5q -tIZ>2a<2u>9avhVV7h(hi@bAp)p{YO&oA@3BTb4bj2drupv&C72fJWp|Y>SG*+B@wIJbntMsmG(k+>C -k!Mpn@fBcF@GByp6N8rs{TQc*j-II?ZuH#i5gjm(M(_Ge^Q*})1CxjeWNrKSu~ -V)a+p=ZOr%jn)n7cB0B3dM_HLlem%{At3NORwE+{95wX$4$(6kw`v*oPbk5zC-^py!ntTKX!Ucu2)VK -wVQJYdy4-_%)z1t7_(wL}h_jp(G-@E@yTR-r+4{C_G-yW)!oQW17_g){>%VNfm=PD}LBlaZv$?jUES2 -f<`$oztN&@g=PUL_gw8be}OPO&@d0PLgpv`DB3BVfPxtfw(ygLJRXCi}VS -JiuFGopf#GV`H~3+1vP=C7O4Xz#rtt+vXR3(y7-O3sJ8RVZN1)$pclSaMEVYH7J;)oqG=aisd(!h+T`$5F#d1*-`R@z=x7A;(?-EDa(eUxQ^0tr0kDI?45cQQPe -l3Zjt8A@wMo>OC4nksPjfo6h*L;Bh@S5#(05_q9aN_jT^gatyq&pS65X`BQsZ)%ZiQ2bQ{L>0F677V> -Z08q1^s4iWK;;><<0yJyMom -1*%0N{qcL;9nCpsFYE8$U=-F#-GP#o6?PAmaM*y+;CqD4>&wUW>sLrfH)1VYSqm?XX779NM^~&?Q!o6 -~<;Hfa-Vf@-rp#eYXCpCMkPaGjveU%s4O9rV8Ajr$6v8BZ&u}N5j~0?jYl3b-&ia#74T!iNMuh`v?_f -fu?i*}nWJGI?1&g)WBANH(g^{8XwnMeQmV&`FXmvF7=FLb{6;#$tBy3&YTdSXj6IV^Z8WJ#) -HP-liV~6YK-MhY<1XsERG-!;Wu{m=Ee3RC`5hE(&~>r-lhhyQF3j|s|Ni=HMHgKsRIP+_*Ra^1AL;Xg -DMqORbt+b|L%*i=r+NJkZW8M44ECCq7bs$vLdoBxcl_KiPUFf_E6TDBK3FWvr4yoZMg@=Myw0GJTj^* -QR`_^=bI{gnzDFR6mC&vWDcP>6TN*H^Kn1d$fbkqrhw0Mm9 -4Bo{EGm(lCce-!%$H>Db8UhMdA@6aWAK2mt$eR#VESE#WN`003A)001EX003}la4%nWW -o~3|axZ9fZEQ7cX<{#5X=q_|Wq56DE^vA6TWfRVwz2)LU%|}AOeQCnFCJYt@Xpjvm|il9=c1SAdxql>DjM) -!h5>+ymVoY=}Fq$l?cCojOMT*IkIa}^pr74*!#Sj)^q=$WU9;ba*&IV_YVKZ(Zfegb#=BsER>*vn5!G -^NHbKlQ=MOFg<@U@V^yLn+mTTo=l*P`X-AIY+O`*LngM_ylgQ#|m7G9s~yGAS9X4%7i47ct#I{JwYBA -nLH*=VmTRctRC_rC^Ms{B+bB?WN6-lifK;h!=5nIzK0!H?fhlcBUS};=rGtI2c84ZzHn -==|B({q|;#aK|Q5_&R1EVFAcBg8V#XwC?+%p#T%VwrDH%?Po~B9{3ParEe!uN}-nOl}sM$<0C<)W%pw -_+&|HIar>Vjj@c;pv-KH8DqrJJ6pzbBRqCDdrL+;Tb7zh$f1WoA#vvY0q#-gCk*2ZkraL9g6 -mn0qiaw;1MxVQvx2DaLXjMtUMdcy3|I36|V0y_{glEi5^~lG|v>DOz$H8#%>BZek-Zbgbqj4O*HFwjc -)!8zcoeSXc)OafPT=Z7 -t_!*;G-iYW^n{_-qmNNps?Zd9x+(H?A=HIf7mTS_QDRC;j^ZT?ed~dGpNps9j5zDPWJz$(HTCsk!; -rF+ThMqa3P(ahns&tHOUp{N-QRg(qoKgQ~eOE8kvx|Ca;f;J0+In>H=>fyHo1IU)r*C)9Ai)6^Y-OB` -a?sT5-60)vp*^lbdN2sHLZ`4NO8oIotl$rcOrHp=LkfrVx)7R@PzHD?K(rMw616Ny*HlWHc#RV6#B7ISXv=NH%A|R`N%gVrUMzETP0Mv -3khVEH`LmIrYj78d*+RshNVzOhHaI=k8XrMQKxD<}?o?;@A|89@ldVZyr+G6q*&6lc{-PLMtazb9;*C -WNL1sB_~sJi)vnw=E6K+6fR{{*5-n2E-btS*<4tg3$nQ&cvUHkb!z3A)Qa*R-=w)EO;Z`ze8tmQS4P5 -i11V_(DKoR)%RHgP#kfP~mi%F(8I@0`uPns6z;vP11s(hqJ@HnFnVl+$h+}fBiXQtbHtw{t6q_bWl2Q -|6RYnT4SPN%k>M*BlN&e_V#VtLvW2dxZSLB5}G1$1PvlM%HyLhu0w%<7U^ft*p6-KmeN0H^wb8BSZDz -J#2e-bMDdw&{>7{;ac0#wp446#E_+#nRh*NUcYv{ -!?9>QC84aR!e`F>KyC-DZ#EC&K%(9V{&&NbgDVGZx80$6-;2!v`^Yn8-N}x~77FW4OJcBXW@mb}-rVu -!f>s@3Y4dJu7Q@44C8froJCtn3!dBp^s?^%AH-l)cz|m+@E!L;eA0==p5&W;$RCEucav_T@O7Cyw`jP -eD1BN|w)Zbf2fpUn47QI|-&AQh1Qg++Z=kK_*copnV5@EbW57e@fTbWgYCl<1k`U2g@#sfh)#dbkrCJ_tK47izs7)vMz}f_=IRs<$`F -wx#1VNNcyiII4fY3&z>%bbWkr$aOJ%v;GQum)x13L-6@!AgvzbUM=X++x@W8){4I2_ku>1LvE&H*V)W -QwUE2=&+jk&mWQYF^5||Sl^*TrnSn=mCYPV|*AwRxnQ+kQsrD8E%xufDa3jwr4HR`;*oN&W-5IvW0XX -*S??toV%iJ!5*Upr3fbSLgtswXC!NN|qs* -Mqa^NqM)@`eIlYP-6UP(#n`WE%#OR_k6SKKC;B!R4!3+F0Z#tM%%;yXBWMQ;|!ZVA)MAKE1q>Y2MlCt -BbQgUR_7@M5T#98C5uFOBINT1{DD{NR|QVZ~mA`BcwsMMI9sAiyGL37jwiVEhN&>M2YLb>%%4Nn7P>d-o7*}Sn+5?3ztcI4FMy@e(t>9;b5 -5tr(nlgqdV+~o>qNPo!f}Smg6l9-(<_=NET@Tc!N&s3@MfKqx%;cUjlh2iz+(#E5vVIH-D>?AtzOs@B -q%nv<8C5tatmFt1j=`3$bPiD0DG2bLx(DR`ZZ5F(_%_5UXJ&0aqiq>ITzRH?X3C%Y#f(hx`uVxtK{P&5-PO$aI4KN>9Kh5%^*NJD@$0HgsR4UlMnM1yc>5C{$V)4-nwjx=zjA -x9c4{D$_^2773O*k}X|h}dYzw-(Ezg>NlfC+9K&}tWNWZ6^Pb -$=6m23@8dSO%6?OP}o|;oh1JDGNgSLu#Y6VDIgM#IwTi~G~Ktq6r01Y8Egg$6Q73js3$seI;GzZ17Q3 -ML1h=igBv_mC^i7{kj(#S9(qjt5JC1q5B^}L}9#A+9n8pEiwPg2<3kaq%I1a%Scq9ZRl*n~6CYk`+w5 -EFn^P>VoI=fO10x}<_CXbGq#KubU^K&t~XUC2QjPz6sHlTZvHb@)lzg2Lsl@j%gl+(iCBwhd5e(b6_l -L5m=<4m~@xcZxMfRLtF$D#8U-D{}75L(rHi478z23)-Qg%#@j*LeqCuC`lTDCSa)?#dm$85m*5#EJQ` -cu%tCr8qf|E4zaEV1q-3-%kgC;z?`s-E1kSnl{V%IflmHie>!uV7tU(^4}P+hHsb}~^SSeW-u&-Nmvt -T-U;WdwVKo0+^5Bn5ZUJ%>PzAfQ1IfD&d*QG=E)kc_d$qA* -=JnpPeZ}v_8xZe|;+0fQ|{@VUG7NN$5TRJ(Z4#$fekg;fRNbFc -qmIj{;`#%{3p%$Pnz+eG~<^g6NhO8Qs}(|LZ=D8B0+fJe7(54Uy6-0+~R*d$c*7H5A~wO<<*f~VRHHX -)uS}&(Jkb^l%M@ydiFQ>j9p!weL9`JJUxGP@%j(H`^{%B{`lLQPv{=6S@OPj{K?liu7cR*T{0X4;EocjWbEyIJ4M1ZdTNzHT`3px3y(drm -YRhRv7#lXCYP@lG0AtRL#7D0qLHukV!QAMPP#(_xe@SM8JXBOKLwb5{w`+PAE))m_@t1AHwfa4G)#VL -b>;Aq^lgwl8NTO6P}|NVpG~Pjn)5wtEm~Z;&TZl0BbnJFa36wk-vFEqf+B2_vHGiMHcz>?3P!*l#&*L -(ZF9h4K$S+0bp)V$%o(-lBdO1Vyl;?&68|_p;+_b?H0Xxific4%NM#>UZ7`d^7a#7sKsz@~C7kCVLmq -`oB1xtHN|*Z0MbauhdO!E#53vj#A-3!{l`Te?9HqAdfHcm9bm5G1A!S(cqD$?gg=NSX~88ZinIi;?=A -7@86%l_s`dxH?QWd>%M|1uYQ3u>dHc0B}fH!q+jOK!41=ReWBbO*5k9*R1GrQpXzdn9d2u)x9|NvUe6 -ftqn=j!a3nyUTw{CS;Uai)b~aHMDP4!R{(AtMg*?LBtXBhf{4w`L$` -*C&5!TAy%2N(2KibBZ>cg1H{$#19gUK;F$@BB`8=R5SWU$mGRv&4fhdPq=$azA8LS)iI(TQl4hJfkCZZWb}V8@lZ_j@K_1 -;px4&moW&LH1Zu?5uChx+w-Qgpl<1fs{ue6NnohiBdc@d_RTsWWf>T>})@1F(MjA_t)7R4hb9v)kVIt -}OK0IMAt#n_r7N^jH$cCc%h9=WJDt5Nsgn&s_vyshn@is-Ley-%X@@vC?Y?fpr3xu?no{8QFZ_`}AXB -gTBEv7dwPCocy$YzyNHq3dU&=tzJ1ZbezR2c7L9apC|4j^91hpDFtAC430ZaSgX{>T!*P9zGYg`r;SQ -NZH4#(X?F7eP)h>@6aWAK2mt$eR#S>L5j_qc003cr001Na003}la4%nWWo~3|axZ9fZEQ7cX<{#5X>M -?JbaQlaWnpbDaCz-LX>S|HlHc_!IurttG08~sEm&Sy2lpB4*g`Dt!$Q^)!(o$}XgD*OL&tmj{`*!{AJ -f-Rv=U<%yN^H|v8TJby1MJG9xlp!#ir9mQ#U1_PFb=l^0H>ptjg1-=2P)~$mYpSQYCpdWKEXD`J7MFq -~>LmR$E&Ogi$2LczVOLd0vjIbjW5X~Hv@uqo3tnQd*!XE`Q4DLXhAgV$B>BK -=IA8{%|7PYNK%*Gzzop0V)=r#AQ!NTrs6EgSbfD=QSu -SNF1H+1i5%>Z!Bl_aS;g!A1#H)9Obh4z9--bF5I}r@5FHSggDrEpkAY~8SKu_g9t}`K0FZ=p^)6ec{0 -Q*A<~4#0663$jnP$DWy&;?R-t+(n9J1^tN|X6io$V~2R!v#v%c$l6uXpja+1=gU8`1w>(Z5IZ?$car?VWCWr`wKn+mUWN)@{eS?L@bo=(bbccBvGPV`NAcWpfsn95=fi&Tfd9@i}a=LeFq;7aEbi)b$yT40c_DL -%|eMeuO?c%@BkLlkdYI;FUFU6Oar+8Sw2sn;n(Cn!6Vo+e!mkeZFf3Vel`IRqE@9@Y0dRuV{!hjt$iv -=(F9BdyRt@`=wHxNq$aq4@$o_LIs1;JL-sR9`m)oh-V@VSa^IGgEiARfWINJ^I9W?Ka1{7OA%30XENU -xAs%=OBz&1X8W4$x^v$vg-A+fyZXD>QiOuGR*S;Lfi>;L?&alybyN}1lS7vBtWV0ed5d+pClyM~9&?R? -h_2yw|;HiQ1pbcGa-|bGhmnuEkh47!ss^+Zk=dBoPHc5RT+B0Y`V`DFEubTZszQS}QR%gH;N8lP{ -^FEFogbxVrN%!#~(i7!v5OS5+=&ZmXTzC~FUqvMHl)pnA+Q&Dx2oeiy=?wv7(WzFUElp-=2?`_ -$Y1gARl}IcEc|E3$XuJj;YXIF>LlqIK}|&oHhU7X)LRm&>0MFpN0C*^&h4Ct&tq -kSFFEBO@F({>%CK*{{4R@~q+mmX~^=+w5Ih_S@a|N=1T+~`l2++@faFk?GdA}Zja!@*EbRCtmGQUaYyfW4$ZFur6R6LK>Iq@4pJ -$TfS|6hs{~54`1GKlh|7^G0u_2NgOaQRnLx&^+mrIHrN1~0}{qB- -=7m5U6jgQ{4k9oNQcz*+LPLi7<)S|cLzhLZnb`Pw+N}r>kXr~gLf!Y~5avTmPP@Fh53On?ZHuag< -V17Ogl^Jd!S;0SGP>lSRVkLovo+CE -8Q;dXIm(y{&l%vsG7e*ax#qcua1t2W9&qg3JGIDGR2u|#_eGL9Vc+nM&@iC6X0akQ{{F2L$5** -BFNWjQsqKPii((!frvW%?XlL6GIidlStdw!zGgc^wyPnTVK|xy2We;AJhx9IKs^FaD7KvC?Ih&_lvwv -9C`rvxhitUx#R&7TGYFFMjU=`eI48RE#=7yE?&N{}j;i$DNh4-^ -lB+$}+o>d}hXWn{=arFyWUfqfKA5#}#~-l44B6s_dFWMcs}l6Mw&Y!^&}3no|)VMH9r~|9OOm?_PTkVJbC{1&=LRk*)Sue$kk-V_ -6L3dMA8x#v1N0c9k4Q*jBZhBJ-;w|IgxrBr1=f@&yx=b*$xN`{DpMTOFl%wY=?(iZ3tU}kL?wk%2FIR -*UWbmv?&{gAuZH-Xc3sJ?*9ZG9t^p*mWS}ILkK*mBWET0XDo`#yz&&hI)c7_v0~F^sk_ -}1lqS1(m@KC#DLs0Fk4}yz6Q32kDWRtGhDdVLbIjy#c^ekAtT(m5WoX?JW9WBtm7q5~efMDE|Hi^i=i -pS7U$Y*V&R}?ev?MWCTU7FAvtV3=;j2!rv`HG`TD-!Y|QK8(HIe0KK=`9^}W5j>t4Z}caOT+7=uwr~_*{i4q -=QYU!Q)RpxR^`-KB+z{(RdamVwo2({{ba(+5Tlfm$rf^GW_-ehX3G`0=9SxRE7 -2Eh=MWSd48A5I4aS>ao!vDjEvg(%CVtxwGhG8*_HP3Bpu!noFvGdrm2TCGiip`aOpYV|N-@M9dq39aM^toC<&u9y>|4gTb_`d@gF(uyT$}fAYDLx>XPDB(|w+NGPlV -g!!ntCP({k$?pmZC8N}#2}}NalYo{CDWJN%cdfpt6waGfm~+t&?S~Qh=TrvF{*B+09QK6V6s*Ttvz$) --gG3c1Na=QpFzbn2Z65_+jP(|K`>))*-IDUlDm`}5`}+0n$KCxq%pAyJf!Tc11dS)P$dDf(r}@txD}+ -qn{|FV`(yJe<%O@JJshw|}7?j9^qfBwlyouWV6cikX^GTY3V$LOBBsRJ$h3c4k8J79bv#dQ@t-|huK; -u2p@=BCO)#&sM<)LS{QI+i=-^3Bd-)}iu4J>ckiYvdgZ5x+_F_F9o$#u^Q_+h>0r#>@~4A&coJP>23Q -!xwH_a(eqtuQRE@{~*WRj*uHR~-2Ko`@2FA>-797#oRLhDAnHe9Ncb{4*9jsE!&QecS172c|ec+R^&G@*OM*tx<=O&*a!;_M@TbNPzInG<@Bp&g>{Zg*aB -!-pf8BJZ_$(#Q;m_(0F+R&!v+yejD&*4`{t~7LcF3IxuFOdA>mYj3TUYsg+R&EdrI9T^$x1_We%D$ne -#~GZ~~CXk-f#j1~OwYy$zcf-B3(rWgIUlm$Q2&Ei#nVRc^ -)9qK1qMNG2K+)3Bh5N2Hs#O}R!>5)UB5edd0wGRhcV}F+Q=P;A6YE5hM}m$PlTk59J!xU4`sPRtQg{^ghFygvbdGeblL1st=?kqbZ9vDyGEM-2_2vMdS7cmVl!7HwjE4a` -DZIN&$c26a>WDnhmiK_qu>Tv49W$(sPLiv0&ctY?8BSLNh^qenmxM#Cuo|Wsmm67lx|bHhYLK*`J~H1 -duF;6U>GqEiRhA(hec`)nPW#qY3Fb7FLk;O*tV}T -VlsDM#p!Zs9m%rZM`8)DD6h5>!EyvcQtI@mFIUB&Eg@++7Ew|ob0uyLL#84AlBhMT*vc51W2Li&5JGfZ#gE4cQ-_F -4}@`quYrUH)>O4tS?f!)^9ips$NylUbPT_)jhf3AQ@Z?UBa83O0FA*kStI -r}-m${B&>{NOYNGV`y5g)h`i4+WQ8gG!06S$SxRsd%Zpqfm^W=q!)%lNHo@vm3=Zou9prX+mLY=H5pZ -O&nm)f^PeHkEeKp*@6j3dTH?GMRC#n-PDf+*K`WF;u1P)(A-i2TSdPscxb!%Y+{Gp8^#zZoz$MSg_i@ ->k`3QW(-bgARe5lg)Lo4Yk%nam-o)7eoxOJ&fHvRtsg0hA+oC-Yc-F`z;+{qdF~grzN>4#Dr$l)`eB* -`ZU3m2B?6dolgTLQ1%Vmf9(LoRn)DI1+cIw}4G^MaKT1%Mn@B+0ZBbxybTMy{IC2D@7}I;4<*Sn<#wg -6+EsBzlU5#O`>6i1#s{^(}Cv#`Grq|w!>4#I<6Horm(C={6c9`+C(4OwM(4OuGph1VDwgW)Z!2V%bKe -8rR~t0WtQG -7~3IKjVY~5OP_fy>sZibS9S8P?~XpjZG`WVYqI&UGS8&Gsn -aY{&^$>{)(g=o*4}ccGDSkm&(9Q!`;EnNDsnnzgwFjaaPX#14~Ly!j6%P^&qk2G59nfKb) -=+zr*Sz@xvcyL3EqV+I(>Bv^IJy^QLNucs+^v`VH6(acgLVj;ac1ii^|uF2O2~F>hk<3NGa)D(SA6cw -L;RKNIp))?~CdP%4U`u_e7aj!M`vL+W9P;S%PeN*vUPN}0nzf=9Z3uFj0ebb_|l`Ieg~wL*FR$)FzI! -B8(hz}9g9^h4{oC=Lol!*~S|F2}`So1NY7Q&n|ThroUY{I4Si)m^r4L-XJH1xHLLLn5ZipxVL%*NfJ9QDtJ-J{*=~k+p8#IubQi -^Bc4SY_mj~?FfTK3t^bF*1fps!alj~G~E~w^;jHln^+*D)N!_{z*^)x+6)9?J+x1P6`d+?0!Dl*Bjmf -Xg2qgF)6)O9W>uW$8_-@?z4}|AtGpw&OfW*Iy4Z0HbzMoiOKS;EuN@$&OJ9k`npE?X$w5N~OMUOc)C3 -l3X=Eu?nRNJfwA=zYhjr@5y36KR)zYVM-_-L9n2X8*K}xsbfI!h8MFXs);JWfY=k6DDmFDzFgs1U-A5 -fgiRDotVyWynO;ssni_cpA&7HB&Qf%gO&9*NELrc>mb0khs4TwqVhZ$0M&N|`v4T#S9vRE0M5x)K^zUve+~jz8PIK=oG%nD(dH{5)C)`u||}Cjk=Z_Ai@t3s3$Tg$YjI -?ux!D*+J54&I>%GV$Zx=W5I-<1~rzV1QBwsatU^_7$%UO^HNZV?lr$3s*6j2xV^tV5M})lLWXVuFoA; -l&P9a@A|?-Hth$%*Z6pX!8@}w;J*Nzo89=&-9dwiIk}_mhi%kL{9IZcmydtJnJ3PL8B@Tw(A#^Z;@Y)cdWk{8jA -DQlk4lhBWX#kcif4^!oLoiKSs5)?k+?x2EBt{SIBQX|-(@+W@O&;!$1U?2cIia<2kTk8PHm`x`b)j?lcZz9)XZZHv`s?N*zgu&8i9zPuD@ILg -MgmDYr09P0`@i;Ll1Fop5wLe!DVaqz9!9WzlKYS%<+mLXn>M1!xL&3y-rsZqO>srN%gu1b-9HE9xZ(d -0pjwEQWP7iu4 -N|$*FL|=KJiZaJ{R;d*88`?LkR~I>ac2lp)!lGv1gu%~-)`Bovl)=y~iZ%cje1nX+d29IEwn;6E)S4} -n+9W6u6C$)+HeAK4KQss@cR=ip|)~a`ZEU;2uW3k#%($@1 -oZ*8z&Zw#qKOm`&px|oK>NWX99WYz>Z#YP_)nt}Ht*H|#?SgaXxm6|bl~>PYyL)lz>l8 -f0jAKiJ`c>=W#jt5onOt7lI(3SC~Y4)q2I9FO_sTi??XKAuY28k6$i8jy6ecUIhU67w(YcU6{Ks=*dq -k%CWq+Hm#i+eRT9H@m#sX;!^sD~;zqp%(*Fq}GjGH+9-D -+6talk}W(U(9KvQQF65^k@fPe8|;uYvJ=v%`=|66gzetdwpT8j0X>iRh(u9UKVZ|8!5+`S`=DF|M$g1 -(V1$oPm(7>u3uFSEdBD2miq -1GDYx9fJ0C_AQ3D0s&7ZV(i6^h|pHQz*?%vHBKY0Jk!%BiMfBJkZwjoq)Ezr_oga=TfA)#LhqNo22P) -h>@6aWAK2mt$eR#P<&;B8MG008hT0RSQZ003}la4%nWWo~3|axZ9fZEQ7cX<{#5bZ={AZfSaDaxQRr? -Ol6w+cviUKc51nXKJ~TR43i-BU^jNiS21-lQi2nceB^^_|haKv8G6sAnmAazWY4~-~%MVmqfi{aypGH -0SCZ&{LaAv2#h>;O0KU*fggDE`kGkNnd|vPzhSN&`1D%*-6y7XXEDol`Xq2H!!_x(ZTZyGZT9HV2x-i -$+3@;~I;QIlne{Ougrz4Q(HVh%!lH(2+tl!Z8jFgV^!I@F$hA)b;d0gacgQ?IC&X^kx(%1`LW)9~Zj&* -w#@efO3+7xNi~V!l4Ui2p3A;L({4qnkqKr4fdufi1JZXTC@EX;k5-i`TDS;@9u^*KtwbvuIT20zN-_6 -!8Q9B1inoUi96;qes>##eH&oM0(%!$#)0O9uatSA1K~{w!GG7P}@Z5QJYcaud;|AJu+;aG2T%=e(MkF -_1v4^Ks!R(nx>J9i3RGzOZxgPh0nT2i8~T3wEnQ1F()y6#H)No7_~=3Bk(*J1E3ikk3 -A_X*M#MeRAcp-61zxdvs7nrVt44xgq-=^&I54978$B&R&3{=_x$uNz>pa|aEX!{Ly8`0yb^jLSq&d+S -;#XsDEt84=7FM!XOtOgaLawH(X8zV0ElJ`v>lWTtzJdPi?u2h8cKuWs*kZ_I#~VM9c29HfzHt{0JsYo -_Z7>QGO7NT}|SchnDNoCDY|OSdEGKzW&QAtlNTeMZl1eGJsj0Y);vgS7o~H}Fu?85Fs^%8md?!uR|SQ -}^|EMQz0NrOsYJH$$O^svrb!!03Hyrpd^P1jLZFNr3=Rp_kI%KCg8!j0JR%5*iq7D6~++Q(6xH-A_QF -gevJ{%ODdSkjG)er2qe+#B>QThXgFpZBW}#VQ~ol#B+o3gcIOQ0xfk+r0S4+3e@f>sI4zq1wp;@Kztr -AF}J8~N=9-&0R#tu2MkC_4F!M?HZSMUkA8+CjKf1nXFeK{G|V(>d8K~`G(ahFAJDtJI*_%>*8!xGR)^ -$2I1qqU(S&YHs3~j2y=B|cVA5)}2Z{;Z(M?2-3lEw^zF_}t*PV%qAe^b?0G*`IFnpUzE%YR@<$g!w8B -))4J(ezok6`SdnhzkaJ(KYNQhST$bcXe`t3J_CpP@va=y2xFdWQ#}ih7Xnv#9w@F(hcykuNJLI-X!k- -vrYe>Ybq%n1>)^v7hM2wAym59R&_|=ON=6p%(DfBkv!dpP!!+*W+w9BBw=ebih{PA^<1yF{xoq0XCpC -I8Z(M6TF^B8Q7nWEey#^@Xj`K!OMVkdw{{(KQsakDlC-b2S@9 -Or_?P>Q5&qEAwBc{ylY4QW4*Eg{>Gq&89mXd$GNy5$2A~UOK -wLBhCd)r*427VI>hg?%ekn^{#>B2tbrx0k7=3d1p?Wj`O*+=?0nP|!7{m-lt~YRi&7NfpcnCSy`t@jG -yLZqP=-r=MJS_Mt{`~1vq7l>vPWF0-A5XtOJS0CJNnC|L;A?=M0|1ju+r6aNF_e08c#h=`lnmE?4G_P -yhsIcz@gCrYCvJewV@5%%d6m|8}OV@u*!Gm$|^q;RyjXCRVmf^>6yAp%A{eM^#??3)bCyL{yy(x)${YS@ZWR%_X+=^mH_b)6? -o035vixqH{Ud+^-WasZxOA(Mc@3rDXqUpHNTB$eH(rAT~k`$MK!;VXnh}j^FvcwKSVW8B3dWWH_w~Wd -LGq$5z%@PeRJBB)@fApETVN5eRJNF)_JT=hY>M>XjfTC!)IaWs@oF+d0heVa6C(w#oKIh70_K>%c_p-t}&of*eKy7LRwy^Ve^OO8g)QH(uh -P()|oO^FO*B2)X2YeMe-#8gONRr>wEief~vycjWjO;?#o!@;LhCMfA&;0x}L#!Oy7(!1qd=?WG0+LH7W9G4Ds-r+z+tadP(l3}@h%+L|d>5p^DCJW>f1lwL4@Vx(a8oq!r))k -f#q~(J^PJBGSV@NmPXueVkI2;zn!-1ThA}Oogp10yGn`1b%|!TkBSOQG<*_hXo{I^w8+?|S?Rz{eJ|M -p;%@r8hLq#a0Ag1Puq3@9spV(A~PN5Ral`S6j&IKO6ODeHQP6rb>%~9i8BoG8p6NO$;TeDfc&hw7UAEf%MCQa7a~B?KkkBKC= -|wWN#uHy;NF5y$1)}FeauzKYrHNr0h54=rB|}PM+r80k0uhL%-+-oQ(ISeLcKLf5N>Rc_N+fU{cBeCJ -M4-5UbDgl@26%?q8e9UI&KFI|UrN{}6u<$1=Nbb>3G`>_eu^AUFJnt_{zhR;mdLuv3rfR -@&aQ9ioL!^KyoNAgWPMa*SQOOoxdHXKZ3UFl!cUUnAuo7pjA6o=iOda7Z6%RpsPW=ceJ(hM870B_Ofd -)~QEVeCPBsI(nIDNsr0IG4JOeF=p}=(rh;X4RLl0#BC2-i{PTb;iYlEhxrVMFCdv8E1z<5&P342s1n9 -FB~UU^d#6kkzi47@r51a*2zBg)|QUvyp&x~6)^S6H&HK`J4+e5C>=MY+DJLdNJ>Pv{su8i$qI0c1Fwo -^m`KAX1TFDyEhnNtCk8xL1@{x>d0je@g0yUZ-hIjGvCQB9mXhFW^({8?rUj?leU!GL_3j3Rf%W^y8y^ -e^Werc2y@5Sf4&ieqK`Q^?P|RkdzfDLVS4v_fm}W3N7gpj0yNI8Y>AKpoy?U`6*1EVKIxb`|r}FwHhK -!)i0{rK+f?;X=PCi=aaPP7sfy$?tJOQ?95L>73+WDo`-OOf`wBc2#e51i5N(x$#WQdy8&1!Kn(q)pV;zM=6}*djKx -Ee0|88#(45v1);j+7kh!DWXI(nAw(rdqBx6Rhf#raIF$bWF1u;_Cd+gy0+r^zCTQUf2ZeWNm4Mw&;9=OV}6m* -M!lPOiO^@$hSBhM*-BcGb0x#LXK4On(B7UPL(+=CH>i{OZsF|902h89$^*l&%(LfmC5(`-_wg-1Be?b -_mbwwTV3m2`lIKLeY}i2NF03cRg^alJ%wBzw!b({0O47B`d`;+p-CB7vs;2ziiQJm)+DmzUd!0h3mtE -_}-kM8IYyEuh?ACDE*il5p+7gV@oYhyE{iuaP61y8&S^KZ2<+Upfe#7aviE6ccfTnB2pRTukn>iDtgz -h3|OP@mEf6PV%$s(BXn)W>D9s_I$Xo#Imtp!yucp@^Q}S*n3Rl^fCkN^VID@M!$GokOyMGK;dCXdSfN --HS7}rCqvH7a^gH6ks&7%CN^}&obyP92nL82(xmyWLriJ#N;h6AG7;o-XHI_nVlcqu?O<>WV}5U&ap&}h3}72JC@D~bWWgi0=1n$F-R`y4-2TZAA3l(me8-aO&2itb5$zkqMHbvaJ%&G+W#U -V%sA_zd(~vw5{0t@2#pM}LF?>4~chUT_9h7OdV>n(ShpzHv^1)5u55}%P@Nlcy;0A2MaZ3qCf`NvPCuNZ9{p;ay@to+Y -*obDk&^xgq$MB!*>tStUeNoUHTjo;mRGM*Dbd!!;kH)*F7D9*I7OT3e%3*@PeB<(T4qtC7yax3F -;AxQVAf%tf#|(j8-7F{3S5YFut|t5hPN;^LI<&7TLO5kF>0Q|$UwB=M_R2bsUUJx4^iAosJ@2$8;@xEx%-DLy%`gt7H( -AF>mXz1_1)*c*(8oKV|2f?9|5Kqm;(mB}D_}e0uHVc*jZ=(!(rjKie;ey5UEeT0P?{+dKk#<1T62bOI -C2~J^Ei73~8eL$r6HcWIOuE2iSq^o9$$A2l+CW^g-Dx|5lca;i@k!+-geBeM7u7jPshj)^Yi{zp6Gvq ->+11sVt&gU%UEQ&OYALaFVbxB!#4fDr!m4FC)P+@R39A-`<4i=bTwpb5KfoC*7~h=gY*NdcZh+Hq-#! -O!n>d;+<+k~GakOTm)?{)CV_UN4<&_fuS$ -1Ug?Q%zKb8Y6e%ns&)fz)`FqA29)Oh4O?&h%R`{q0Y$i+)yT*=ytnmP~0w+*2f&{16*ufluAw#-=--q -F+J_H<++*ny;5Tyt8ThrA5^x@pmEt+a>Y4B>u7->XP_tPU0^D&guYSDL3Ua?v{+HLx=~15RF*fIAPg=OSAN83AdZ#(vD!$X8(DbS9vgRLEB2UISBzM8K2Fv_v-^2KTIr8|| -BHy3&7jk1+XHmeTvIFi54!9oS(Z|S6eykn8aVNRzl3!9}xXvB*d~uw}2mtUq-UjIf4&#lY1!X8jhw4RkDw^Xb<>|z>Sh{6d3)1RQ`{E~sy5s#%4K_Hm=-hS}ttB -nb9m9xE16|E{`1YK86JD%80Lb9P!rW`qw>8tAFtt%*{Ox(Og}5gBifo}QMxLaxkRRFUxxhfYS{aZH_z -j~O(;>OQtKKG@w=BlJ3^j%Tz)uY2JV$B8*Sopo4!sZK#~L#quPMW$?DY}FOXkpleRUbibIpjn0!QfYTs2-sqEhEv}WMV=BP1!a?Me@kT70Ioj1^EB40Xy -Bi@4Ay4Mo{MvoTX(VZBK>NaV7ZXhzaRrv06>m%oye&gQJ+!_7WDDjU4`G%4yoI0{oyTn5u+!y50ST?Y;xxtuvlYUzU;p2@hi%T$oZjk-VF}g$JKcil@elDPG3G9K|;b|KElj4E;TT$r -Vlj1Lt?%;jBt5!m-?>ZfS09frlwFmDRhM_BmUg0ahdOa;y7wJVI)oQW?)HUBS$A4a%6f()y(nQ|mz-_ -izAeI}9hASQ2Jqa=jXGuvxF&G)Ut2P+$8*TmhdybESs)hRd$<7Txb2)mNxN6OK3lH!Hoqhyz}%SDgP=)zL@UkuK&#{{T_L(-%_Fzhu#xI6bv^g;2y5Abw`Qi6P -<}23PiVUv8O9`ClaZEUy+JGk1UV*MiL>H>mP0VvH|d<4=0vyHnk_44^DsfV#(<=X=vEe^_Osq -2Er$#$i$XXXw$H4L-0JdFFl`ASLAs)Fves^`?2jHN}p3X*6fLpXM7eEf2H@_jio=ov`pDmsTu=`-x>t -_%;y>e)Nbgk;9{7aP{9o9uCuEP-9meTT-gfRQGO!*(o(_=(%sXPT?tA*dqL3+(HYN`L0~aI2xD5piKG -7Vq7AUM4n>qb&hJPF)QjEtD}&trhQV=OR?67EiQW!n~K%ZcO351L*NrL6XMMM)BVg$42%X$$yE6jw5< -r7IEmiAi!^@TKj;HJ13$kIpMCs%0iVl3ueU$Pg8vnjX88H7_)PKh*?#tgorNT*lp=1sf-ywnQKNIhjP -&L>`N?mku|`qFC6wgUHTbtR{c3ywlUDPBc%sdM8{0Bono#MD>rgy+Y0i8)O}?o~$Z$Qx9H@mVQ%8|aF -}Q|0+{@eHB|qxwk)l1mH_bzwLrLRs%1aJGf9TK!g*vk-S{Z9v9y>MU*LLH?h&(diSz_c1T?79uLV?@i|$lcDm(~a(o!zHJ1LnNA0mGdb8&F8vP` -gYecM+&wxTt2wQ!>u)c+eUbl#<)p@+{?XPx|s#mMdb`8zd{s`t!6ioTxrxLocy9DZhqd=4&cLQ7>aRk -!n?o40U2mu+r})fajk!!UATO>?Qh10OsaQcRa$(}s^gV%v1#qPwR)yG6d1;pA&yq!wI#(M7TO3@s5T` -p0B7s0PT6NVV+uvyhIdt!id_Ao>b9u#E_+aMM;odAF09!><0jp1EUp^fwop`M9(P@b$P^5#@VByaiz@ -iQ@xF#juepP`?Y?%PRK()?Z|HA1Dv7vW#-w&|XeSl3KFVbiLd9?A>{1lfs&yA?bz@zs6Z5XyR5m&1O& -#Yk!&SDo+4nB-DcN<1Pkz_079g$n8gA6pZt(MS@!7`Due7RNzxebt3k$y#6%G9SLR0Pb#iv^LRl}~e! -#B-<;!TVAm9(JwP!dK+bmGhjS)bpLoid}}8|2I*sFgW=htP>` -dypE;Ivo8Uc~pNUM2>!oM;GW3U(@1U`o)c;Jq>C;MpmERwHDsR58ADQHlYGYZ=x8a)$zzAz(QLiwxkP -b@RiUS9#0)yqUcD32EE%S)oOJ%H5K-DQ$W&Mpz6x1hXr9~INU&M2BHRr$CA=W*6QF#W@3i`9yLLaM=pVBmm}}6aJ#UuOjT*!W -YO9F(LFX-TIP`dI+c#ImGH;v7q!B>1C*;F_~hTS$}t -69{;%XM?9#%EIcm!PGsCh(`>?E=#BWhoaQ9;cpPC3DrbZp3QBD+@6^h(_!{}n>(>)8n8ld& -YWcJ7@)opTb`4i#UHW2?`pW>jPsGKRvqs6cV^ -MEz2v3BTIpec&M4OCj9V>U7=kW>=4=y%^8skiMc4Fbp_?|%1wXsOj2(sWtbO7k=Yu`ZRiz!pXomF1~_ -k0BSg!U!c}sHDhL&eK9zu32G%U`ZHuQ*zU=zg2lv@Jl=j0;@OY0pOn_%T!VD%p#IW*dl8gSXl~r9q{{ -%mlj^T(^pz6y_FI%MlKXX$aH}~_UKM=JrM<-8zzp -Pk$*RJp?lRzo;hwLxY9~ncAPTYUGxW!G3W79cMS-!K!aN+@g)xM0xm~IJr7$J4*@|mA6Xtz|BCna{@0 -h@&Ry6*Frhj*-E-=`o64Daq+68vTGOP5aer&Oqq`pZDC3)I{neYl#J%D!wvrDDd2Bfl36JrR0$e|;L= -u`Fbmy?qte9UIE37D0tiX0XkK{8fduPiJ!U{@TE3U!B3Ha3CR60W(V#KbFM;I3u@CO1&lvJs&&B=f`N -?CA7o@~N-}xWvS!W3_Cu9E{a?Vq(-RTOlE`zS*EhWdfIkkN*pf>KNXDArdc^?t&SfgOW~3qCi}NL5$A -M7&Xg@LUZDg3f4SZF)0kzl!9fY=?=KbSjii9|7Mb|xYBzJKfh%~^fdhBJr_Qm031NMDKL2FxiD-|@F~ -$8p)$Eo|D_89mbNsO$&5$oecTnyq)W;J_{c$Wp4WaAb_G=!w$`Izb7)D;z6R-hIzuB<2*nXNq*hT%(m^KLo=Q -}Rq;N|sucsJ-CkJaBaXNDxw3!K~}8H+*BzzYH5Nrb8i&rOe})n@WSN$^x~Zn@q`D#Gzm)hqK0-7kKRv -i~^Qr+_)U4#HZ5208t)d*T&itJ}H$J*Fv(wVUW0^Lfqi-;tTd!&>V+?G>&pjHWC~LgnZzH=ID|wFMNx -_rBc)^7EFheb9{UazfWq8xx2iA=@i_BNBX1bX&xT#RsyRnW0EQt?MD@~!jgBYG^EU8Z7y&~C5_8H7~b -PVBe$k$M70=uqo>iNm(M9h>y<JG=ll%#LNzfhITw=%lpbyl_wU^jKWl5~yWqh=+R -?N+74un^MC8yY^f1}+$eI{)WJ>}SgCeTm(wmbbV)vz%)h8uWSda}<-76-m06#|H=J8Ez)j{tfp3TxBL -HyZ-;|So0;0H*BySnxqa@4-bbX!`UY@-xq5F8jv&$$UWq{9t|Ba*&0YAoxy?vg!7 -dAJ`VNB@;O>`mGpC_^;<8eMw!xRL5^f^2`%Qj0+~R+XM^!KvTnI3PvFXn(;k7f`s%xXF(Dd4|*6`(xLqMzr -+L+8v(cf?-~vN4|aUa&*8P(IW0mh~bcFTIUZBZ|>;Vuy%VBz59g4u0Vq_O=I_w8-?L`meVyV5T;K|T5 -A}>6X?XY5Dyf34YGI)O3UbtNZrPP%gOAo;sC#~#lZ2}|0bI@d=)E507wo_IX7t@!h -lG~G)_HEn7%nK-^l9T-J|Z|{X!HPW0BM?3C8jTr2x~}%Q4C3D=s#=*#%-@fTbBDg8UvCty8u1UBVfHl -cY}Ld_rrVMM++H7ZCm7H+14uGlPf_meD5l)DP6u+;wz7+vp1beXt$y$yP)h>@6aWAK2mt$eR#RIt113O7000O^0RSNY003}la4% -nWWo~3|axZ9fZEQ7cX<{#9Z*FsRVQzGDE^v9xeff7AIg;k@{wp}1owdDVxp;__Se))zl_lHimXERJy1 -Hs>lbJ~}OUXSd -7j2)5&l;He%=eBn<$UccpV!q!$&D9B!GY# -!lSWCwHJ{hWouRY2Kh3hxFXH=yx}5{n_b4cHuIW<^>OKADS&vHe8qQR;g}3b1dEjRUNq8GaNq7 -(@MO55N_8PL@%a6T=tXGrSy}o#b%Hud(dhx++lmzK*elQr0hmOk6DoC%)+bWuJy`)^vm6)G$dDYq?%) --PEpP6=iFH!ub$xWCQ2dYq*MZTk*cQvNrTZy`igXBrkn}@xouwtM{X@DQ2@KcP;=YRc|r<&#@KTnEh{ -o}i5ogN$FgGHJ>oFyg6EX>u9yyqoG45qSgnV}9Bh6&l+ffi|676*B_R`VfFlN}5MQ6-TuX-4A7?1`y1 -yxS-RP)cXjKUZ}x)8ZgZ7dvWhOTXy7PU{BJMQ;(sq0-2_2)$rmI}fP4r&#GFYC888X_=tL#9OQC9wZJ -;`E|mey0_LOJFhu36?ug+MvCt+KZp`lPjH~7r<@&up0Qd*3s1>PnKjd26mIK#l85o)z>m{hS-68m>@5 -#cQ|*%nR?BttbRFGptD;rc&FE**a#bA6)x^09g=ITyYt^mkBDD~n{Q1fDk?+)gU50s~jQYXa%dQ?$`@ -f_1-qSaiZ!da58s>U9X$s}#rdkg+S?Y&*)mvRCD}CKo{^HqI#vZ^zp02}UrRxmi+-P~H#&)Q5%@3DlJ -2={ucf{E{UnZ$u;d)R0?VCS4bk^0THGADHeTI(E41QPx+E#s43oye^{^e=l72dP_i8Zkm(|XppM#ar) -wfwC@3{{79Go_>gTH@Ivh~8OeW>(E~HN9qBHuf;~WzAjhqLte2`i;NA&Wx-%r=;P;MKk3;)!x -esG|9Z)v;r=Nq+G`y&lh1$Q+peYq=8#ti{~4^4O=%k1;)*&O`&!S%-9$Z<+`z*I@By`bRYl!S*CVHFs*viI`p=|o(pFaOcWMvH>=_)&*7 -%maA6_R3B-yH%w9N_w_SNlC_6NqXxZ@+?b}FfH@#Dr9q`2Vk53q6JYaj~S+X2V+~? -sD!h_!7#$>J^h!bdiFfapZ$?`904LrWgp%~T~%WxqqW+;sRnI@*jamHKediLZ7O)y(>3(MIFyTrjF&w -i0m)7$&yo>TeFA6I}Rzj!W_VpvPqcWpT2Rc --#Y{dG$>d;qcml_pMIa^+$XR+!@-GR61&7Ky&MuFRU)=?EbbIIUsjoLL@R_T04@!VD3DYsf3^F6~NdR -6^+*GP}TgiTf41@-57XP$cjJ1w_McuYmn@ZE_WGN7H-cxgPCC}L0lSUC6sXX4PU2SQ5eA=dqG(8BtEVhoII;JfT -L$w7=(_TePcBkUFZE<_`wWa&Ip1Ew>gZtwes|WPU9xpFQH&v+JTZV~$e=vA4*i++8l*UR4bEOX9rZ;% -b1GG_#>N4{-tKOgr(y5iAZJ<{-(DUM@Qv71I-oe7~AHu@$?_pu|4`E^S$5}Xharjj%9ByIZ@Zl_Um$& -^Eja2t*?OBq#%=P6e%3FVed%IG#Bxz?N63?wq)AC-0fYk1K3+D~F%g-^#2C8&#qgr-`wzjo*Fi(T~zq -3(Y#rBr#ok+9f)otFSX^^I0;r8nGNMY(`_3>bMr~QcPc)qT*w@w2m8>=V?LcN>Wl*L|YSD6#l!K(Ifd -v)15?nW)UnpvdTdSCUL=0TNpU4{q9&*^ae{qi%4^Keewdh>Vk;fHbjAnk1`SLAMyuov|e+N_&*SOUGT -rxqPur}l1FI@FMb*JYGJPI*u52zrgjxpN2}(EG2Lv%8%At!5)jZ~soKp=#JJ=C5e);qC0))hH|OSB9b -fYun_ikfcyeQw_XoM`$=!m4;;3n$X&&oh-CzT{}3~V@vk+(NtZeSIX=e!8)7b-tOOm?rnVgXv$W1G*v -51vnsp#z0W?^debiJ+w3XvWNi6X=}VqE!uUFl_R45KKh>gKM^|z!%Mj87Eag9_-8S&L$W%4?)}VV=plyLx!(S4`L!vSJ+x#jS+wAO^a4jjb;0_ya7kgXIwJu9mdQVN@* -%{%W$+brwUorAuAq|>Zd)nVMy@-ce>%M{md)hr%Egqo!U%`f)fe##u|CcQ2iThx^#9VLmO%>@T-RU!u -ZCPXw59RMnSmP%CNFBgJ;Bxx-S-L*3HB0}KRtUv;6LF68#>^#R90k~X7mXHoCv-> -SgwMlQ$)T(?W;e^3uQ3cM{aRSm|pM` -b=pN-Gza+*8lbBkrryjQI2ag_3-L{i_N#+d~OXHU -+A{gg(p_NK@Atpa*oN>XNxp-X3jfVxEvMX1H)_r{+Ng!K$kH!iqO91 -{zg4w8g(&}4i}2Uy%01a^Vr3nlJ>Nm&dsQ@dH&OM+PKvdcU6*0Fn<$|aO(lkYqg~8At|!xYEMTMO`7ZxSFxw7<>P17fahveh -`jjgM>`0zFyGVPhnOWV3-@S_a;GDIpQ-TjXrF$})lOwU#eOZBFLRwyxQ8fC{i{0ndN-X8itMJ?qjPGL -Q0z1753>EfQh;zzV}80ve-*(pOUunZs`c92C&FMKJ}i`ryGI3sXitxV=w=VWo*oB#)FoWc!#zD&L}47 -{`)0}_T9(-!by}q9-Z9iReh-D-IX!@6FUzaJ9{g|*ezXUFxCcMpgP-id>zt~+6#E3e+K1okbVXuB{#o -A;2D+?2gR@PmgdpxeZ^r*Vv=p{`h?GDf+g6WM(xv6et>9U3AUp-gb$6i#J+m -YHi2(#ypck!w@+S8r&p4F>5U)e*FM`|l_x0@*5v!Yj?{T_-czwaO56DwJl`zVS%OJU;eF;+?X0CSV1_ -3Yh8)oe2VZcl&Tnz3iG(5WeV4Av(0qGYR!4YGaa^C3OXLx2BT+C;$ixN-M;H$V^ABklJf_HLt;;P$&= -gUO)&qqSlx&cl^w1FUZ!5a3b+8mekHp%YAW_ME2DfL8`URGT -c-(ZH7GL-Vr@{duvkGjTLp|NU0~-QwbCvr+vllc=a-Od9Y(0X|oNmZ}c%)z+=$xdDEvfXsJGt<957tY -J(WM2khKEMl4{cR`1Q4d`VHYAC~C8MeL9e{(O?;DlM?#cD^HmYEv7HZ7+jZjc{a=G_4h~hcZfeIK$gut?Bv^xPqo5W{{qu+Fr=iHN8l9zz@~f -9M%&INF5<~)eu_`SA*BH4`ZZ%$r9+S5(SY|2 -Qdrw6XV${mI6DVeHQat6w9^ZbC2Kth|b%0i09-n@aCSifTa-v=#Vc1vVur{kaLzuNESDrY3FGDfRD<1 -iNowKLNH0y~0n++CtSoUsgW_MNnqIpQ;}QTU#7$iW2>JSYDIW&@(_S`h}{dYz$Kid)2}kTHX6H_A)l1 -EzGu|Ej+y2sEH+M&svzFRUTcS- -XTCwt)ME|_3ezp{4w$N*`TD>as(ER>X{cdUQ^CDcw%7m19|M+bSxo9B`UF8y2uLo4ERX@M4e;SHx8P? -if|M{cD+yi4YMEeJr#rl_=#WzuSJ5Q@R^`GCRcZ{hkYmt}ruLghTuar}#-}uoxg0xbwyO%ZWQwwWo?; -W)FTWG(esScrSylPzZuahMBpnmCdq}pWH=V8*#d0ZXeMSA?otUAg9;Pc0e$ENVB+P!o+N!5Yax1L{=) -y?kS)8+_G`>ze~d5=A@8vX0D=c0V(XK8GgZ-5Ud-z?cwxPm;OWVhLewwbSV3M`kbAP*>6yV#;=4SGn~ -+6m*Ll?%4B)lSRI5&uX8 -VB1R~#vU)C*$qt2nn&i)WajFc_vu}IXx(c0vQUCS4_f*-;MReCts3xi`&x)U||J%~oi%8kB{OL1&$g7 -#8%);(=>hUkeeEed}>@Q`%-+$Tu>KSnMxZ2iKhVWcfk`>yu-mzJ0q6)3$tKtXkk7|$K{#en2WJS}Am- -|t4k=}-xuk!%8_WF5*7Zt^CaZB>7F7kP^*;k}&Ntz-T)t%gYM;kT4vqwR+j0z`Vjd}KH72ch0B~r+ij -+h-*z0^vQX2DZm=|}bNi>hZ&pFOJ-#c$r>bo4@v^gs9BC=u3gyfwq2@6I<@=T&u~atI!+Y8o1`r?t#i -gI4;hq4{fM{yH>&jm=+^$B$Gebf(ZQ1*i={yW6XeV%n|PO_i`+?X=+HBmbd_QMRp_#b|E+(xKh9swxs -`9912}YxmDO*ksx_Ssr!NxAg}E3x$5?q1HfOzuVyM-IL0sK6&1I(uTC2=n~nEB31QIs=e-$=gp65N8J -9pTW{Jw>nMEts|mH@XB||<@1~xL>3OPF7|*$yDr-jkJdV^(Ry#P_lT#wmR>Mz~wKs;a4jals6-fXZJq -lK}`P$onz$=5w3uh9qEYeNv-9LU_@AuWBNcQ(83c{}|(dHOU2qo~jr1tTL+#HNa|XOaoH|_`3qi%emURfXp2B -S%M)xw5OgX@jVLFH!&Yl!~~yN2&#B}D*H8UHV4bwi|F1hQop1zucz?C*fc>NQI#A9OKsc4(g)Qq>NH9 -MnUuu?c}4H)V>oN>h2A>WsSuj1GWyl4u2Go(P`^uDJxi=_qt<5sx5n=OR^`i?w^UZ%O<29^t53CF^v( --ak-kY@r#1Rs^AfR#wqHDQHPo&i$D_&N^PWEE>)##sd+NW#{>jT;@9gu(_q|W&uisTvlks>odfreC;K -h7?cY5~f{N34UUDt4MZ0Z`;bq!8>s+xDNFV1=w?|(R$_WtAZ`^%Hd^Y`z1Z%;n{wJLYmAL?Rtxsfi{A -52yG3q|*?_v!7)#YOMp?DF#LWA9b(hx5xfy*KaQ|FvmyI5bTjHcg)OdZ+I{f48g27oquZEftV%@y%N{y$?Ml=A6t^KAvtPD28I=)JR0=T-{Y#k!Iv#3)Pevw$Ydbdr}K_(r*R(^5nz0?r786r&_ -cxPd>hX*ETdY4Sm-%G{tb9y}CU3@c#Urs_S*{q=GplJsKT@hnF=E<6(z~^VcVrpFf`U-k)_9?sS{Njg -AMX`>d|}=yXf}ezyB3+IY3C4W^p*cc}iPmDP06=%E@~Eu>apPBk1HnPTmXQ6m0${`$@Cazmp7(@|6IW -z$@vaPDFQQ>>jWr)_gP%b|DeY*Flf_gSf1tCa&(-As^y;_U35()>?vS{gH1r_Br*C^pXCg63!hm7O+o -<5cymwgQ*u7pJv?93LK|u6h;>G)tEsPfpJ&&UqS)P-i<022T6pm;_@;x}5~DX_e$2Gz+F+p`8UIi-jF -?V7g-tkignGaOUh>X9kRpCOuR3Ra5qO%l!GNE`54_a`y2|+ePKCstxp6v$$9;c3jM~gT1W(@z$IA-%X -lVf9RCAS><(yDBpOxCuea~oV(6=8n=)cqNnFM%#_0VutY#^bt4tSoaeN7E6`-8sOO` -Z5r<~P(5IO{_1+X@Z`eFI%&HT1LEoDYI=-)zcGmYG`7n_Bu3I_rRSyu7V`GzH^p&(FX&=#!v<4!yD25 -uT*WGyyeONjD3@<>=U;Hkx|sgpjf4cff>>7%rMBN%k?I_HVhw{o%PE%)We|>$jkR1eyZi_O&K=p0BOo;t(yR`G0g -=-f<}cGpGJ%7GaW^w{x<9$jzOk2x%VVP6G(XJdl_rF4@4M5t%4j$=&TDib6i()40xJENr8biQ|@Ju7A -RoC7cX`7aWkrSb5v9%)1J0v?&fHEk-56Hii~D>pjBMPp{YeNG@VM^+bAh>U?&p$A!g)QBCDfbV9}^fU -A~Q~Bm5#QvnCHDhERq?s>sSLzCW$rrTq_`@NCS;An4Lr6zGlq)CO8O5^z(%J30g+S!QKOV+E`AQV!y9 -K?rA$hL{d!#;M`KLZQG}8k9aN#Trl@B!=03u^CJDGD~wKd5_rZm{9Ol(rg+Ll&9ID5%{TVQk^PA%Dj~ -^b5j_!)4z}vK(|jnF&Gv7%wWU*skdB)M!D4<35X0j?-4j(kuf$ydumPb5C}5HAP*tq{t-dh*n-+NwkQ -#)#a6j2z89a68!?Kx0U4c&|>l9AF&?hFHmem-B)6SAI2d -cGJ-Z8<*{Pc-~v^djW9KfpH2TJ#S>s(t%I(Mrz<~BLWvh>z;1Dan`T!f3lqUd)iKI-fei|nvVb{U2)W -vuba+H@K_S{%Enx(yyJye_GWY%0lItPdR|#QV?|OdfTQ|X9WV=`r6r*vC3Hg&fdK?&_ko+Q&)YtP3S~*9` -6%dw19^oY{+onObMOy|bXR4!Ayg!YT!f*x(I@fNzTXPx`vxCuL+aGs@$|T#%@F=_K<&CN_lwwA@(-jA -=jHu8MkD{!~Z@@S2QjDDW+nr<}u3_HU)!)6_zey$llZ -y)tG*;D!|Bom)&f5KuvlcV$wY? -Hh+qCC0oqBWfk=^s5k?$MN56!YM#1{OGS6B1D3_y%Crr8_XFk`#_CW_}u!$0X{8lb_B*vAC`XVDk^Ny -GRe!nSgZne&Dx2URB*zoA(n-jwUgebWEJ_Jxd=V|-{9)N@{n21bF-|6&57swM2T&)@U>G}HE+7tlaLX -B{xDlc504x=>=ytQzM$yI`%)X^7yQo%0&$OX#cvUb|`#%OiA3!S&0*+5I_;HF&IFM|C*N=NuI932N<7 -=8Vdk3t!l4QGNPb!qr{}R&dy#j5-7&*3zG#2C+IL3~_GTpF_wx0yV!;t}hb6)zcjBMW0~J`XLl -y*U=VU1m{rlgQLQAG*-oahN5hkSp(8Fi}T6Y(55(#4${LNnwWz7A~|HBB3vKaleZcg@quzEs-yct3Hq -AZHaklud$MiV)xj=_7$<*VOX{jB@#<0TWnGDSB<&Q-~W(;w>2XknpbfK}zU(j~K^z -1E;HKsUtc*{$JnJaJALQ<~9PN0u)+uJZ!23u@}jSSve`UJnJI#(0NXV_xC?FiM(thZusjS?jmS;O`_v -6!2lzmuyq2JD?&NfDiRCs$G&_`G9rnF;nTTzdK{ol9)k@w8NztH{5408z4!Mn^h!)8$1NxHrDE&#cL8 -14EkgF1*Fw&fK1OXOFZ|zjx_(s5iI<>G^tQ(xhJOYJR+b?CU$V|aA4wvK0zI4mP1C%)4WMQxE*2 -m36XUojZ;QSTi&-(b^uSSD^f)?l@&faBwT027G0!d;AqgLaK%JAY;4t$ZIm)fGVKyL10~yPvlb^F_AS -wXPED?`K!?cb3c(q-D5N*EvohJT&`>$2LJMP!U*AG^{DEVOz?!iRCb#$oPD*PWSQ&TO63^=@w%e#Euc -brzAq$nYrM!vbz5^@y_qxN;5_Wq#47tLO;f)SJbfz_rS$*KtQIz>M64sQU-(gMATCA}}ssq-a%6X2R& -hWU~Ri?A58#>4~y1$g~(q&LmT2+pi(Bf^V&3iEHP{=BdMO&rs>kNHh2M(+p?zw~IhR`+Kt_m&Hb_ms$ -)A7jBIhTZv+5A}+e_*358+B;oyXKm_l7_}=gPV6QXK17o2SHiN8^m$;JI)&iD5zm(3_KIo@XDeY&7Os)^Ce`Zua*lK1}`vd1Anr?j{p -wU&mOQJNyySbTp`Xw5*X$d#V?9m|&_!a)M(ZUZzH)$r04xmXu5;xeRedwq?1d-Dq82#2)6DsIqhRiIGJ={jO{zHs}k?enT*f8??_#MT2r`R}uJbT@ -2lR^T`IxxA*#f1MTLeF!6Ifby#;35VaMpHk^%`&2HALV9wOeN@i01ZHIVam37WpkUrmHA*3ecm|8rr1 -7xSJO(rZSx00bLt9Qa;Z=7$i%{2&r8j2@u2n -K44SY+p%$n|(+)&Xzd_ymP8CUg-bg9L8~0VJx=P5M<7}f+iz@bVN+Xg1`!wfa8u$!U$#hWeagl1GvtV -T>vExi0|hZWe>ac?>X9J`t*GuDwzs6R&8WUeNQ8a+cDpV8MWFU5 -IKA^3*3n^k8G?tGyXp|=2NI2FQX015uyn#jb*j_gm)c!8w>HhZGa$@&eStA{QJ&GLpC>3#-^bkginvd -vLEzY+z_&1{0Eyc)YM_jsNfGB#?09NXxC?Sh#wP-cM$#>c)*y{>+XX=E2W$+lX`UpQVcihnTPV!r% -It)285p{~Ts%^#=F01dpGED+adsK0NxsdwHA*{oE0Tv5Ov!8xGYx9ybD8FM=2qGH!x05qR(4KO<;$7) -J$g3l}+L9$Xfja(d*lFx0S(k@cJxkUbg?B;R|Xc~%9cvjWaKP$N@%IuVR6HoL=*lYc;9o~lVn&b-r=%zHDxKL1!-%%M8g1|n3^Eln$k}#G5g~CO4 -#A7IwJw!-ZK}{V~Y?NdSP>`>4;(s6!o`<@QEz}&SsfF@^@+=f$S6Js86!Gm_s0C1gg<1j?TBsFJ3qo; ->MZ#|oPnd@m28>Ucg&fK^#C}pE+KcBt4gK;9ZD>X~AM)hmIw;Yx+-QPwfG{*0V@o`2M7)hhn1x9KUK_jpctX^T#;3t4=vh>c$hDPvi6Kv67bf3;{|ox65K-^XD%}O41aUAOH*}Wj$?PEQK99+BllNhH}X7j%s9IHIvfQShD7BPF -1sOTdCmy&u+dl|V2Kzt&rCWb?L9TP5GZtLn;3Y(f;fZgGBsVtax<@9RYK3l65)DoX&&lZ(8l&ovFDkG -ys;wLR}VD7wE(Z|p1^TcU|PGVn&LJLvCl@dVs0;|W-ia8rB5~ihg2E?KXNz@MG*=jVmGf4X+T4j01yrMCYD1rS$gim_4_Cr8io&}S(v)afG)GT*n`}Bj`UeWsJ)UzvO3CdZp>CWTK!v#7vTd1Pd9= --*;+B~X0A0oU89Q2hv0<&y`L-+A6}GUB>BjKO1YQavbkgX=enUfgz%C0mG?V9>b|yKEtV7 -%+L1#4;YN@PPm%y!On!G^F7#^uz0>-0X}Cq*u-goh7+3$g3saEOnv4*Ao+|LPV(Uf0yNATdKaMKK6e= -W_nCY}!_LjFXxLdh@ek60bn@~t-(#YQ~z77|9-`=Q7DmLw(~hmyp^{FWpp0f(3d%2S2+%3kH}P(ct)g;J(33F7(yVNuN`PGb~m1Y(>7)y;MWd>Z#^n`1O(be0 -)>L|P9Ay63Ng>vsX62_RGEiOltyp~Zks(W1bG!AC$4@e7h#`}8Ho@n)*|Lc&tmvon}^MiZ#{}f2)uh4 -9!cn|3l{HQLK>>e)T%GwIbPzyGUto;Fd;oK1DAz)VW{Q14}qz_ZKkG@fX!UMFbf>7Z2FcLj(Cc{qD={ -nuKwtNnHorht>F3#gH9ZF7&L;|$d8fjr=VtPOyS4#cu?Mi2Gh(KbLD;U5&7_PX`itM^B(&{goT_F@xE-;qIXoE -WtU6-|^t7)B3eku-zxbeWPAZ1B| -i5!A1NS;H|(K1jyK*}<%_0$+>FMJWd3^#O1KO}gL2^UZsv5?Y4B5<=B{sl)F@Xn5cs`ZnXN#a(npA$y -#`pK2CNRGK}9Ikq#lqIjLp0%!)QWK6O0;vJ;4Sjw4B)r~aq3;xCh0ROfEmDOQ;RspK3P^CdEKC~pK9t -az1AODM?UOEnW!kCt+nA&2DUeaA}bHZjNYixa92Kl5P8_%;Ko3(^*!STBNyojV{<3m+}4Ns -L`(5lAq{YpM_6SS#B3v_J3Q>6U{QN0VMDhNr-|Hz`;1SMI~U@2QZ2+Pg4jPHdw}> -X2LMn~jq-i2duRn57NZU)gomr-Zj8P^BZb1k!aAvUzI*!YfP^26fI}-}f*A5^T>)l>RRCp*0f@dZ$F~ -);N8oU4;s#o$%$1^!WxnjGIdTvD+eTryF@P+$@0$-vfuk}X@g9=Y6>y<2|_+$cRK7f_MTr>q5n65HPkO?^*G&PVs6ndXGD6*x!Z=A(J8CmT3nDxfa;Vg`5&{ETOY5 -7%>GT{3s}IHIS5zcl1V-~(#a;fxL}KGChS6)3sg5j40y4K@SwzE4ki~10jC17K%fcRAfqbTgcYFDJRFx3+5h0i>*tb5-r;M^CM({lr9q-r3Z;4!79EvZ`;5!_fo-O4u5|$OY5kxl1(t -5Q%00I#$Tu5A6Z`m7ryA;PPxQe{wfB{++Pu82cNAV;?O_ubrBV2o0k_ltl>uZ;+n#B$)Q3zD-h9V~BA -xOIv%Zi7WuxdmK%+SKnU>y>QyC$Q38cDq2O6tAo}TZZ5!%nC~-teJQt)Knr@A_8nQd*wwLZ&5rT;zei0UU};%MtlWl%PTMAi0 -dI$!-3&L28%1G$BdzSfM}X7mQkE{CBIBzG+-4fABDn?7D9w938#E82m?2&S7GMUjt23bm2Klt*Hs>hh -?79wN)#GOMZ^Z(OxFbhTSL6azDm=p?o-NG2sGaDiUtxWR~@TCW#w%?bfDt`V3@?$U3s9a&vY1tU)_ro -7hK9--EUS%8gw8r(V`%8(4-v(ELIIq0q2w1BEsZ$pgVVrB?~v*m5FU-pwn;!2xI~5N7}@$ -{G$Z2ZjxV9J4%kEq>Ej!b`X4*(L@kbyA-X78AoxBFr~p26q4u&yVWQu~_fa5j`K^#S!Qn|!_*27U`Un -htG()5G_QPGuoK5xV>q*htY?Rd0Jpy=Rj@|lnTusLf_<%tDSQ^xts7XXaMExr`CBIJLutrJBUL`Tgqc -A6^&o2jggb0@uextj1Ra|asso2X_X20hlIH^#a9N^)2HcHvs0>knXDGR`Dn}X)~6DhEx(YA)Gb-Y=I7 -7{w^fIDUh!;!m(Re`Z&;%#;f7ZLa|W3X)F1)&O#*wSLeh#Oba$UH+vZDf5C1`LT$r(pjXA)>Qj*vtn0 -Eh8yohKPRn)iEW$c+O3-We8fB|jAdhxK>aUnAx#@$naEbp@v=rLh-R)~8I4fS`_ -}9=qd)qpM|xOC^0e`o0oLYeukFVuNB7`l=%*plmte?nHdTZUJ8Sh=o%}q -6PB?K9XUF3~q^D)<6VoFN>cF^mWp0V_=&okqkPAOuev%3cyBXiqW4o+1`+G!8r(0XPcWg48Q&0nf&IU -cg{_vOADFl6WqZ(FdGvvK`^YlXz|(X8BJ6tBLFm&)xp&6trQ9y4J~cunVsLYNxINM -6+xl8YUQDn&pBN^GTMK{pp$zU~#<-USTk($(~m;+BxLjfh -d)q&Sae{D0B{_0b_>3w6w9wc;b1RbrzL`Xqq&9f{8i{ea*zy4FJie<21_j@6a_?& -2ztE*eC=#j#Bwq8sql1sf^&QbgP-!6$@MrtWi__(t}j&`G11!FufI9=9EP$`Axtr(_UCN;R -!fnLW{9t}|NXdCAJ;1cY#qn%$?pK}OSZh`n2!d+d^syV=>tz;-!5KNfGXuky?xU^wkeh@tDbTf=F89Fl8>l8Wz*AZ~>=O{GD-(w%RD{egvMV*1p -07A$IFb&LY8jVVAl*xkP%`rnZFdhhjxAR~9Bx?1H76SmvH8?-#|nCfH*64#6hKZL84?%|pd$@lgF`%e -AuzNvn-G+YX9po%V4Q~4NNF_>C6ZiY9AWAkaWDvDTp?Tg{4Hb;-p87EBF65nW%fRr4!yk0!oYeJ$xQO -K?d4!NRuBS0O4Wx=>H$F%wgS$&P4qn-IS1eu@oA+*eB1sP@ku3xyAeoHu=_z)T+&Xqe32E`a}x3WQoCyYK&Hjh1@mNmQ7v)2mXDi1i -S?!@A>xWirnr11F+ -^1n)#D$ZZw~e7E;E)tT8;Nl4PAt9kB!t7)*ZDp@3gI5!)EpqR;s?KNUfrv#l`X#D&yR`XJ8nA%o%e`n -C-a%^VB(t`FQcc=cvcZ@T=C4oq)22&sH=qV?GVh~P%tg0M@qDc?5S$|=N}b;B;&v`qULF7em*0b-;Kz1{xSgEpOZPbDKwK&f#rLJr{RL;F -(kaGdRq3UcxxxMp4$Y%-Yg)3Ar27{;Y~LoJ-UR1)Kos(i2)#Y0~#pbJA&A7%;w>6Qb7@(AK(PCf+JN~ -ViwJ=#$(vb0bJLxxq!{Oz-Y-?(U4$9SsY^mTi;iJ05kf$0(#E3i>M>?UGT-RgwDEPTt*w>DT0(8fU$( -m9AG{;L$s+zcytnszM7ocg@DaEz#E#RBShAOPdX;3*9^miVKWy{`qNwoEk7Pix4lOrIB(7jm<_U-ZE7 -m`jUo9Lf<;Yc_6vfok=E76y>6aM^bE8D7a<YdeS=t5^eEs2XW -AR89Nb5RzuIyYdc*C4o!Cq8G9_JhsS$JyCS(jQRf{5xz4=#$h)++zZO|w>ehL{bN(>nj7&nq0_NfqbE -;VKZCj618Y27@VhTG^hbmjuH#ttWpDQ=NW21jPI?pC8$I1xtxn;nx+rOG5|Oyo!aXB{Y|et7_5Q#2=1 -3iD$jk%0>&gUPrBL49Y7p>HJ|aSNf}yoBd&M}|5WVP`R6ovX0C4TG;yp+tJpF{4B<5E>}oY#s_Yoy`y -f$(gfpPRL?@x3WafTGF&qE2-E!#Rw?Q7MwDe? -nMk4POp588P4}5s3LPS0^V&#HzOFz8`>d*F`=hNV@A-G2??j05y-KzgRl(es}vz4_$r0R2);_;GlH&C -915<)RfSm%?YbxJwaA4DM3 -kQa~mT!jV~=Szo(9A_U*1paM;(7QFKvEVL-@QlQ!(mRBQCvAksw2zcg1a7H7<8zMzvK;}b%#kJNsW8} -3~QHX{dO@}_dLE{m)d4Uv%h?#kTlwel!I?7wg>tNAdOFBD-4^!(KRg==qK7 -bvkDZ?&O-ar`%0@oE3f8ZjaL_6w-CU9LsIqXn$Z;<$c#(5ECDo7j`OvspM3fB!2Y6cI~(XEI~-%ss4w -FNQB{C;#S7P-Y}C8iW&D2SD)KadESs1IZ!o|1>^%G~&X;G7#z^ddJNW!y1zsFP!Z@eZ1W;Us#a^p1x!q(amP-5>xEYWI>{&E~2n;s2Dtiu!J5Cl@mbD63JIa{e}V@8n6Dh -dc|OBlb+JUL?6tP5;DBs2p=zx@I(AjZ&om3c5>RN{o+Av+y@Vs0(eh|lHQ-gMLX`ex84$o9%t@{=r6L -J$~&>EsHG1ZqcUGhelzp9lyv>%gKVJr9R8g$lj^Vki(dP6VIh5maPYdW4{qg%N|V-EexNH)Jq%M5S-F9kvXd1a)CM|jC|Y*a@ca0JzuM#+NG6iFJHk@U>}yR?MG?B#TMOIN7#0b7*4m)J%Un -5&1W!OHuvQXkvv@#tgR!IC0F#&f!GBSdo^-h|b}JC$QphVj@sm98!ha;&+yS4oFR~ot)5 -uUv37qiqEGEn>m2(i^oHT%^bi{0~;}H<^W>&8Olj|1Qs(XWP})b8Z1ueJ7TbyV~2!U(P{jVKNT2mglm -&`Q%R9Fa>lZRc*gn+Cyu!$F=KsFEGsurB6xls*{Ex`l>asurCkL_fm1R(QyGgl@_2)&p#9i^o8SjV^*S!oM>r#aFHSI -Qc2Un2umaI8QF@-IK8s#VX7~^F@duvRW#{BBgKR2pm6ncP!xfJ+irifk53j5s`mo-L=B;wWc&{&i)Z#tAMi(l%KL2;_=8T7S!m>kpRw|P}=+VC4w#%(I{yYJtatGoR3@tHMM0-P#fTi@m&8-Ep{v=+s&Z~CH;D?y -sx1lW=vO|!QlpqR36<3#E-MgO*CP7CH~R*yrjI&`-vcCjAfqzWZgkP)+y(?&2Lc)<@-;OVX++k{^t_! -uKxJ19EafGMHKZb73&hy{VjLOxM}XT6Hs2;rb14poZVbQOAm;~rXQk+^Q6ndwuCwa4gpDNeqPf|Q(h> -`CaX3+7b{RnNuSkRieOX@9(5Qc_0pTmWZXD9IdL%kTY-0T&s`xUX5tb40LcovsnWT3w8Wl$b#E$SjVq%Z_C=bEn) -`nvMb6IKD-Pao)H-G)LlX@zy6kA!m^_d`xi8+!-_2Jf=EA{z+-=%eh3;H##PSq^vL51roud$tW?JV3` -jL2M7t5;>?CrvSZ;ezvb8LpndYSTO7le;#g?RU^@OWi%lsE2+0G9ygtKb9iaW)J7ZLDgw->g+q!yf;JQ?OA`UbX_9&Q;;0DkwR@LnL7- -1iM}y;QWDuAli19%8n9 -RYG4NoJ7^iPaG=BOn)oGJe(}s)o#JxUtvIwrw%lzP@U{o1yz(R0NT#mfqzY3YBFfeu;BmBDdytKtz3m -JL3OQ?nY9c4=HVg5BxIlEq$e6=BKKg*+xZ@8Hj`7y{9UpmM({d-jo@$il&hD_0_8mWVaRi6;j@>MJDD -jTItZ1gzT`Vd&23%BvM(JrJdS~%YJdR=1_l|GBc~Gsp&R9(e#XBB+tnMxq8yj2~xywWsP?P-jt64biI -ztIzivuYSXBBs)Dx4@ildH!us61k;px7NdgI-Il#0*Fq(BQr{xjgL~X|A-h5qtOrTi@X6v7k -$vuC31Zk1?ov}5VdEb$|(ZhSO=OM6hk`@e`*}yL{H3pX!zQ{-&Haq;yU}Rr@;m2HYf8k3Q7C5XLV3GS -G0%%4yUu>zN0p2)6nAWF?2lybzmySHm5Ps>%6Y{t=j* -TL_WsoS&#FxSu3yo=diCnGsYZcV53Ij=4evE4qLi2z{U;CM3 -EJ2(!%#&_8@dsdJ!w6Df_>N0O#yq^(2a@b)3(1Pm$O!z2==O_du3qLegm7L-RYo}! -7*S)e&A&~YGIK5)W{F*PL2G)^R$(p$&6C4R(|FtmV&kNJEbsefgyKFvP5nYal)CpD^@n;Z;GhA1+9>50m|=@4l%ss<>4Sw)13F8B8gSSG^reR -yG;Ui-*Lp4~$fSXMP(FQlrZ&d6u~DA2O`>#x -rqJ$kgs!bNmf3+v|F-c@grX1%NDy_;T?^b&6!=2|HEi>L_K`O|0rWeWW5@v0~`-+uGW?d|Q0+tG_OTY -fW`PLIENtK6pQKW{G{|Ej8etiQzaST*IxUY_?(y~4Xl^ZW<31yuXLr=RmMYyN)rZ7rn7k3V`*9tOSJR -habD?xyFLS*8TpE3z=`MR~8(?+AvfUp-~AvM{f3iY`hvWzh>%6RMJ`vW;rfwEf%1D&r?SyYs`1zNGNf -{MEEwk^f&%O9KQH000080Q-4XQ?aYPpWj3P0K&-u03!eZ0B~t=FJE?LZe(wAFKBdaY&C3YVlQZPZEQ7 -gVRCb2axQRr?R{&L+cvW3cmE1TQ+uu1mE>1;lg#n>9^0|Az8gQvo_$o!am{ihNW&e9)RNT5N-F>Tq8k -t5NrIH-kv&nlJQ4{sfJURy-Dq^ri+mZKoz2(Px*%s~QLjm-qFHj4lu4eAqIH%`^B -FlylZq5^THc!jh*h$hoL!M@mKT#U`6A$KlIr4YNve37#$`E*t`f4jB-`@dJzk*Nt_b~wFeakbS(2^RR -aq4zUYf6}{E}qJPoyv_(bD96ouo5T@IFn{N0aloB%GZ~7DZgqnfj0?SyeZ%jH`=fdP2Tb-ZBU!p8acGR6D9NiYQ?i0?-8;N@@7|2eBYZrZ<wU%6x!y3WC)moQ>r?Legvf{oC4a-a_o~V$c9aL -Q4Xpgbm3>TB6fYIjPli&c)3c+5&QM**|P{222j}_)8u@DU$9momI(fm#o6TDe+mVkJw4B7+h1pXnp_Zi>g{3J$SI$Y$ls0le}0wc=X+O-#z#O<$r`oW#?wzkB%I?EK-gdt%brN;;2@H3l(Rkm`i~e!+eZm3OQID_Q~Yg)~FopZkc-`INz6xQ$j5g^VRtsu -cdZh!;?g6``D`ijxfJoO0d4HIV+Cx3??lOE^79UyhQ2l0Weq@|a?n?jB$E42& -!1It6$#AHW#m{q-@kwP*SD|Uot(Y?`1}X>qOFy0zdnEX-CbNOz6Bw_*$asZ85jBNY(}Oz-P`h_mO6&> -bvA`s?> -Rw*r&6{=0hPM#w%*(g$s|Z6V~}32ql3Sr4po6;5h3kNU&^?Aq%3iW0cRM;VLi7EmuGY|z|*GH@oOsZl4j;Rql;s~3MvyMR+q-Rj(GnC%VL-SA}{hZ1s;k_j -iMPwf+MXiVvwrSG&*<937JhISf{`!wM86FDf2(440=lCMdVZ$MZR8KXfPymYFqwDefRVMF$^6=MVzp$ -h6r(v@!*)Vb5ay^l~CSAvT44iyuJV`&_L~nIZJRjBd+Qsi_cTS-bYKS)X*}?^dd_0MM)7vCqMzEGj#X -_$wW0>YxQ^`jd32jR&#U^EhM?~TA)=$CNzyTp>Y*Pt6c(U4PUwWOy7x4izE^kiqdo -ETAMV~sQC587)mayHVYN*dblPjPnS_%*?E$Pso$2A5=KYF`Oow`NO2uKrz9h?I?@LHq)|I1ZeaT-xesVmneCdms~ywCGqiz!` -O+#e$2nm1;n-B_o~KD7T4GKr$XPGt4J=sX-vAv&~{Zjm8_$c*0seK926MQi>d?a=&kr5`wGIlGtDmA_ -gf==}Me!ff>Wl*;7~Ppl-O44WAX2gls}VRx7|TzI!@2CzQF8Khv#u@Yz#^;*au;3d-=wa;;rlh;OhYU -G+e#Ds!;VVv#PcQ5gJcONF=UXqJ>9UmVk|j?5`F&&F4Ak-(!d$aI0j1Bo<=VQj`Kk4v^y&v1uYGy4KwG -tDDY0cU^d^%vp-@#}1aH)jEhcE!9CZNTN@z>F)+HhU;w1)MWZf|i`rgV7^ed3N~K0oxyM#BNHD>M>PV -P+7oR%tRtyf91P9{@XtP3u*R*230ELm>2^(Uz{xFEe(#tm@ub?$V8RqkY{ODR~$Rvp%dqc?x7+d5~~A -sNX8g^OJta;i_j8Q7dzy#aWBLQ;NUJW}d?AjZ*YwzJxJ5hrwPcg}=|Iz+_#8`75O`DoL+Eo^qA1->OU -)Bzi0b6Qy$po$gizxnh#7V8QBJu%1+EquN`ldr<|oF2h-$tp`~R$vU>VXI&H&SMQkMR?DH3;b(%ZPRE -ZvOEJrOi;K%1iUIsx&5hK@3YVe{sGSLh>Hz46WdTEbih`ii0jCC+EshmFHYvnyd1L#wEICw~07+-?c~ -rLwTIvuVQ=y=wEbZrC4iBABRSI!p$1Sn(0Sr@;4c*gt2m^bl?zSy>mwQVMk8+-PomTbR2577sjZ9N%@ -X)*KFknM5H9Es-ti;b$PX|CUgU`5@=<1yp`A;MREsSoJi*&oXfGuVjr^zB)#>FMwI1if!#%YQ|8X8=4 -=&qvQStRnEcmAS%{_{kMit^C&m-K^0UIfdg$lS~tLS>w-G7ecI;rJ2k4vC_S -2FwL3MaN;-X}msRi7q$)40-}euk|`E~f)n9WT?oL>0oj-hp#_YCWG`X89&1vjv%1Di3_jZF --PVR(O>svoROI$6zZPQ|%>zLzr=yr^#$h_vogL1wTJCu6Lw$jWNPs4pm$E-{C|A6yKCe?9rU+_~ixFv -y$q58NGV{T2U4_euZ`}UPz;}V*F#mqO4*_p;%!;^(K=%u%)QB -4Zh*B&eXb#%cA{zuREPn?o(sSnpZh;hMaM%t7eopv -FzfRpJv`CJ&;yA4h9<(H#da!{k*JXvLZvs5CV`MW>VTo?&g60H>Nff=AGiS1SLS`dYPd32M0nwl`GUj -m+7qb*5GN02$w2fZ8e+$C{ZM*=>OB%C1mhQdl=><4$BydIt$GC<$kSVYe6N|!}e*`$Dy~=X-NS?wW%z -5 -8QsQx(7zzMX7k~I2WZXV9=G~7nCXXL{3x`%>9DUF;taUn)c}we1!dyDY1Ck%h1CW7I<)fmR#NcjXCUr -Ioy5TV0l{Yg*}A(WWlGOUlw0njSPUh{dHOSvg_u`XEG7DUcz@U(jO20@sEFu*O@rTz!y|W7WNDDl2N$ -9K}5?VsM4lM1QD5WpMQ{6bp -rN-p@D0fGbaT$xnOA}`zXFakZ+4C`sfiEH_N+sHs#-y(koGll1m%ZrP*m+0B-tnSUFZ0NYR2Ba -q`R>7?xNvc!QOq$H`XhVd)V6(K>7>#X@Ms3tgXyE&#RU)(-`+p-cSC+ER&~;B44j~p*&qLvmfw**&wu -{G-$rB`|RpUJ!;omah%FZycWOA7T^$zmf$DX(4H(%b2W4s0!Eo+J}dpv(fHA4oi}B*jXV>^aqGZgr5* -^YFY&x|X4rVOCLexo*7Mxr7<@QUYig{c*9}k(qL&wBdWo2!GT01RdCBCS@*-cSGv@DdftvtdZXN;gAe -x$1JU)2r2Urosx*93OHRAL!;E|lj9_?8pbiFWaA@=%r<8f42u~E#Tm~mu3Scmx3%Cvklz!et9&eU -T{!#kCD25gZ)o?!8mU8#}X%qlG1k3lm|7Mr)CQGu+X^ZIEZ~8TJ)>XeD -cMBz_p%s;Oq=0qo_(SN)89S;{!YjiQsS#p+wK3hxUpSrdlXMIk)vb$>uqXkDY)XDWg7R#>@Ng$4#N0)F3j9DJzq8l7uhxaZFenw3~XosErqS*a%pccH&5x&kJBo3=D9Y3zDwjx&pI4F{2O|JGcu*EO{B#ML1Vby{lDjc6L^#c}2 -;QE;-6Vkknz5;&%xL^gTb5c}clzqZ#*P7(jMk=FqXNNTbS1H@L_GiZj0b(&(J7`YWZB{< -1P!mz5(7L#x9nM~(hnUcC_*CNos+oj}C-np+!X6hn1eR0+jN4c>#u(=^|JNRhxbpMq{o)dbFh3c`+l@ -OZ?1zvm#zuSiiOvl+=IZVYm{LiCOcJ$?tDL+9YcNuGaj8oyMI(bgIHLKRwyy77#=Sh<@}&VE~3QHBNn -B16ra#tqy&UA~4E#2Iqa{8L(DuB}r_kbKH#RNabjwkdPEVB$#@y~x+u>@6~`?-MHGS5tTuLvgGJYoFF -J;1>kk?ePlSG%2R*G%o0~IW3q`4Wi1iZRAEHloZffL6=AwtykDX@|1t@#L -FP^knkwVf4dGO0txm)eE&?!`p)DRaDG{SGQI(Csy4d#AxVE7Jb-09#Pf~;QpCp{{%$Qp-nHrR-5q)N- -0BCRcs&dTN-~MTed(Q@Scn;NE?3ON^u{uiaPPRSFvLso>d$ntQ&Kn@U`Dt3J8qWlWk&Bn -B9-rq&K*X-j~-1Pxh2E{Zdi*iIoBjt2}&E$Do>N?mad&_mM`IM*@tcPloIDRNZ_yEynY`&JBHs*xUNo -4R{3g}!Lo;7Chl<@nkVE^1hOoVZr^L1fH`K1^b6-~v^*v2oq&C$o!I -&HGPeVoFY2dYDDL(5B&YUeZ@xnALLF}b>lXPF^a*8L+SLnBm<$w3aqKL9>TY^%w=cZyo5U<#aTij5To -@v{O*0I~%Oq33~w-R*(fg1@^0N=1%Ure3E{}o1{s&mzvDlEfYMPa;d*H6_6RzHNBR$sNFb6tBz8#h^N~ -zyMOCCf$%G6xcXe$86Sn(WzQv(8p-PQe-|Z)DfRe+N=Ps=hft%mB)%#)%B+iHjv2`-IfLS|j+CaI!5hf3-yELzX*rgYJs`jY!h -;**>q(?(fCXc}#IZpf3>|UpH4K`oh$JLiq`qBrh*L{2%S#}@hjtgVQMWI#iJNB;C+RHBq1-#}=kpBP= -{gMhJG4sm|0Fq3L9N01g1JM(TjEc78%PP$ast5o_=roLAhouNNH{N_>Tp=&^>zVr3t02xGr*gOd)s9dWNCh2a-c}1od -LJr7(qRcJsz+e6KX4Eo`C`IBkgT~QN{0Y%{^=*CgRC12b7I_+4(uYzN4rq;Ba@^L{Wri ->3rdisMYXPoIAi>fMc&p*&X*A9>;arQK+hgrJ$m$jKE#__kH%{6CiHciF-8xWW=2(m54ukdgYmB89D4 -WSw(b$17b|H%$iNa-6>oh|~G(3m7OLZHQPL0YlGd`JS`i!$JMj3sTmeyT^=8m~YXZQYecF -%Cw7)=+D@%g$MFY;;(p|Z+>BkY=n1*eUi#j$Z(xZ7PZI(HOFdR)be5C*E;`8Kb6?{%Q92ZNo5N#)Z!H -%Pa0R`;Rh^L~U@2f$;eFfZg%>2VNqAUyb(Q(~!&*bYeO8@OUn|Cs015PSd=A_Gzk2O=bz46b$P7<)Kh -fZGk9)luYdy4iKQ+5#?G2sdEg*tyd_&d7rKWgu2|;&;+9yQANT>7-!Y)7j4mS{a$uf9}aKloxOC3YNH -l1HX%IhhUO#a27abls{(hLf9dEf>@&a{5=ShZft6NiQ;3o1LCF?EFGC>D-Nc^=fcNy+Sd1tF1x)9#X- -`6zFz9}YlG*Cn@?7FqDI#;7)Cf_H0~L>ivkib+!p$2`+bIc?JP-V?kehV-oyTKTT{p!G{Eb78-mvB?M -4tLT!IkiWAU6$-?#9CA2q#TTlh3BD!AEoxYTsl`wJf>lC|{YVt&M -?@fD=d~%u2~Ns!8U6L-_4r%n;EVxwF5>AWq~1inmu|t$l;8ii@&?o}fltQg6r -Gf3MSo>kOnJy9=E)2pNib)e8l*#q!Gm-{#1a-9h^4rRt5HM;&Yg`}Z)3RY1<^lJ42JLR6~>Lis7++Xk -6P&npMQG!=FPZVuLw+Sw8@Lha^eJmX&8>RoT(Fmz871FxuVgEh%!21O;UZRnvcIVB^B|JH}^E~78Zuk -2{#02WGb)z@u+IL4ql!SQ;w&L|*4 -a+LTeV#{8P;%nnfz+I1c;z}t7$R&xbdrc2=-YD1~s}FiXKDI(AeE^ -il%12Nlq{4OlIRxQBz0#e1!?R>@D!a>D^6)vhUsI}93=9B1y~FAcI3X#$ -0a^dfEAFC0HIX^cS7-`7D2P&}uUF>&bb2x2*Jr79S%mWWILS~U9ZMqI@kq?*QTqu^qh`+7qZzT>rI_# -IR2n4kJt3swa3ij^@7q!mZn@|(-cTD&fDjw7$0>*XfxBEMX{fpJB-7}ztsDCxR>rIXu(w -ZnOW6o(nM+0m-k03vK4)M9ZWp%h>|EVJQSKmm8KyF~mbD=d|pQQBLVTaKiR0-rPTBcAvG -O;Y$-73<|&Usi+XV*n=-Thl?n)@V!`g;47FMFOLx#W^@zOKepnqO|mjE8uP@7gDPTe5d_HA9T)8qk8X -0C7&O-!H&1e0XeI!=BS6j?1JV%lwKA0gT%kZN#44!x7=liJiL`;r=%pryRV(5>bRpz^?9=&Y;I$hO6& -O*J~H!BUJYYtBeeEKC&2cXNy&%q{YP%V~KJJ3_@6<5zlAj63qD8Emxv^u;4Wux}8fx*t_wuY*)LOtN? -^t06U1(G)5P3wOL>)BS>OU!tJOr-8#w#JYC}$jw~M>@OVyR$WBakC>XK=P`Iw7+p+%ViWJ*uTEstXqb -23gP9adnLN;>|kD8;EHV0tFBiEsV2j)tAIFV2q13xkH^6F9$MbKqF+&QVCN;e7ip&ps4}p -o)oAF(lY)NS1*al2qsN5$>6f*q_IqRNz=d?yFdm{qiT`6BX(-rLq=#)Ld0{&JcBDGxZ3kKlG>9+4^ke -8|{t~p~yJZ=nbjWSaS5sG|5_aKKQsL9y+DT6|siVCjEdb#M`*IH1kc1j~$zz4NDyDHqlFlX>si2bvB#>=j$YO2g^XTcSAkRk8(=;i80G}}@rKfO0A -JeWqi_X)0dP$eX#th-Oi8D-aH>0i7r4*wlRmQrZ`yp}XA-_-Dt~-=5v*;+KF2r=5iJoU -0K!%Uq6mqYqsIdkJ~HsC8#>+exr5Wtl@gkx4E~%xI1V`w#0z2QMOJ~@P~uIm-!~MKrk7@!*9Fe#fcMbM5btl$vZ -{5#q)TET#8B*kd`verZ%;D)G-%fQT=jw3df8x@~pld+zb^n89^m8!0l%j>9v^S0{9VIl|cg1^-PGt`1`4%o#JxKT3pzFd6A?u -4+c+7u^&Dx`f!`n@sROOl -X{w*^zeXy;lE2r5sAUm4AZ;&6E4r+;3?@XW$h6d~R0z!2|&%nI&R2YcD@D%Kzf-30 -l+ZUY%+OIerUTL9XC-jE~E^lS9{zTCT6f=8(iqn2myr80cYjw2y?3$_qc*a8 -ng`!^ZnyPIlB%uff^}WI+uz>R58hg6?8Ohr5=&tEwXG32ax)%rm^uu!Z(+YNU-K0(XQgL{8o6ec_YQEh|3!zL(bpY5@-01mKX>^1!r1J(N^-J_loikMUKI(?>a66_+^emWj9E!TUKz>0ohbv$x9~c}1o3>TO*7drioD%-wrK=JufQ8g0kg% -%Px@jlzkBe=JvbOG?}s(_M@-yI#;gf$RU_xAT?QuwT`|VXxVC(z51n)F>8W6h)l+ETVtQliAIP9k&z@ -rvrMO2zmqIKN|3&!ErZW_rZgG2R<;04Tg-d&Xl}n53STGtNpntgBbH9#|+yp%0FPq8!<2ZF)J3$a#Cj -ZtB6G>i9CaCM7mPPZH+deCXNB-;{5%8Tr!V{C -GxIbd$juG0#g9+9n0+I5-YGsd4z?K>0I37}(=~{>215h%s@h&46v-HKbEf>$B-)R=?>%8F*Z@KhmiC> -%7a+)|-o>{XR$=j5IzcjZwUrM*L$wjnm{O%vu(iYaJ$uHndz-A(O;P+S9@zgya#HAO`4Jg`wF>5*Ktg -JjWvnN}`kK6`p&s=g}qEZt`MQQX2MRN%3B@n*5pV6=FSr*(mgg9kgCTGvBX -kU5cw$Hq$uK(MA+351wvYEgcy2iC11&CJA@}Wm;%qHi4(Xb#Nga>-K2cri&e#*f7Mrm!n2iSf4;rmBf -xCqp#><$^*^W^1biK@eDDw4cM@Tp(cG-6)I3M%Pu45u;O83oxW#KiWF%ob|2yp*ghNG$JNK) -9vQ{@-022e*I1B@6m;u*!x#W#2d*=O01gC3SZuM=J*C~f0p7;d#dR_V-sgA*k&>aYw?96Y0$v-RV6|v -NBw1w5)fATsyhVXxt^7qvh4%FV)0cI?m|CM=Byp!$j)77<_z%_#S^tT^whla|l#cu?3O$78dDOV{SNY -iB=Dm_NhbrFyE`IWMBiZXECFGvR0ATj-zxQfh6Uc^@b@a)<5FcG~^c+^kU!$#v^3K5q^`^wR^h0+qXI(b`w_y<1+Eq-@?mBe?3uWu1D_F -`XlW9jbea)zjK%41>?em+w;_C5(u0eM<5#tx7n4=*A=&KahorNNA!U+l)lo>s+;K33m`L^y#~?6(lvn$6?y9gFdv(8#eAV=(pYGe|+TFapZsv=kw<11~v2SZZp)lFs_M${U4 -YL%Yx?S-u*!BCEP*{kH=l`-+_d4K}u?C-2a$?9tD!Tpj^5_c|%Gmiu~e*=OMwy&ezcw@{EYZEu$~Q8E -)?_2>5wG%Su-?J|6-ue=cdv!70SYzFZNin_@(NS2V&p_EO{d?B5|U$cn^FRs@$uiFgi|4gSl_38O$~exno!#lXh$ErptEPi~xPO{s^KDDhs -%`>Evy2AJggs*I1%s7VKsaFiUk6H$67cpVp~bUE+Utq|BW&CpS?Z`7%w{GjiO(n>hk@XQ~YY_Y$Cs<9 -Z7R-!Z%M9Jl@AAc#W{9VF+O8vvvvnP?( -MSnlua@iS#duC915z~3n1UI_l!&I=C{7o7kyIB;(=aFFrIuHj8ZUK2&RL+;9Mb<)WCD8JfdKoQK`qEt -QbH@=eEX+w46xt`_5zd3Mm60;ODfjVE|n1lUckk0%5*39jH;h1vWgyQ5A!tz^kbf{Ugm2qzdUqSd#pW -V^J!In8hv|o&zcAh;feLas{e;$+C5+WubrTO+7QC@8_?${CTKVphrlT8wmnvgpk0~2D&kH2r5~i1|e2HR#gp!Pwh-W~eFk3SM2L#}B -^Nl;LtYRBghxy8{zYVT`WOcXRA4q}c4NSuqOI$T9^fZ>}3UjfrgJQt!%Sr -11ByOJ(7?myR`AHuBKcYLj7OP_u4|kJu{4YzG=Ynmq0jkU)YCx)*MZT*jZP#?o%HM$+?x#(HU|<#AGWd^WqdvPx+`S_O{AQ~*tP45*>u2=n{cVmB{H{C&d -Y=|&1MLIPqb&Vi1U)EYLn>#0m!0FWU#g3@%)H|%cP7Hg3_`~;XcL1sgBQUw>?tzt#n;fr`Z?7P42BuaSsUth!e;;|X*wF@d~BG0s&Pi^(ua -awG^W&f4Wb9#$!Wn>UmOwiAeaOUZqSHfta{IUw!OXd>p}~81|9K{^TSwLx|F8O|o$k{q;Ep_;owW(`uPX -YL3)em>vv(Z{K;=WxdIi2aZYd(kz(@u3=_6MhPc5XlhGX+xO@gU4nV`xgTOxC~%$IIMwlv^d+Qf6?ww -q2wAA4I4NX;3o<>M#K9hwankw`zm2&w%W>jmU*&a>y)lp;27h!-fC%{Vd>Q7G`gb*ZZ-NbUJ0D4a|dT -dnev`LjWe4LGe!X0(-Lz7?KpaKV)M=Q!naSVM2aL`ToDrVzyfSktQ5_UyoN57lc=$Cjo4xs=qKljbYch(Cq3A|%gRc>6>mX*EkAWUzgy@r( -v*oWN7&$y+|Z04ye_lC1WXv1-p1b+l}3RW9vaB_ukpmA1DV@UG878=ddr&#&38!kVgv(&V1@wVU-a$M -xAJI(2$%hoo}X6R!iH2G8G5RN->NO!A6Fk1`tWe;KAEAq#qi!cO}r^_V^<67m!n8b369>Ba>_-{h;ImX(Lz&?^4)3+ -X$~QP&j;yf?8~F@y<;ONG~iXQb=(*MT8+NcuZdv+|{8%CAj#| -Kj6ZKB4#{ehUEhG<%~PD&#^JL0S%fLX6$#SqYivR=O%1kT;TN)%kZA6My5 -T6&{<|^c*))ShX}O+pnYJZ15^jx2J36<&0dY5K@1&cc__BPQPkGuKQ^@Dk}W!Q)8PHswGP{h5PjA8z) -jj7R(J#*RDCsVyNhQ?&-UuV)085o;m(MIp4yWX2l*uJA@C84imF6CI+vz7X -Yivv-<7ewdE28^ba3BkyZ*DkOx~d`S9|aQ8~{;do-bdWX~D`(Vl;kcP}CBG{bndMg|>YW9* -KneZgo@lBsC0T+?|gP<>dZ-Z@}515IQZUTu1war(kWu93*{aHPz#NOELP9}9z~#!a^&vpZGtmX!ioI4 -E0iWhw{-xJ-&+m5!BC&mGGHi6dFCwZLeFVDvVzSg?aE+xs$3Q?>$%wED=ym -aC!M?FAzOU+ZDL9SZ@aSVmqb$s$RiG^=*UA?#Xhep#-R2LG{KStW!rtAz%3^a)a)j)WY7aeNT9EQI1E -+0wldzHJ;y=G|df9W)A&NO5*B8YxqwVlu}fel|?fK-95J~~P|?yo -C#-n%*vkvP;s5EaBO1+f;CLZ7Z2)D?IK-*H{udrPzFjy=0&ul8KTEU2XmMQAi6i3ydB<8_MKj(SDD7C -pG%duPFe52U`SXD<*Yn$$NBcigJ$fd69CwsvZKV -mf$p{>P`fV97Xsc{;z1QXJ=)aSLN9mgaHL5##DT}WYRXZ^47%1BIext*9bMxa3K{WP=oy0>;o+rT(fF -Xf4#PgkYrX37psN+1m{Jo$?qHcgUhrTBY0|C3`IvN@uW!D!3IY<_n`-`_urwUbXd(npND^)G;SF@vvE7UEeRq2)CaNTL*q`C?xO{G&Cod#zHXHRq64-7^xRx|#_ -mY?!+Zd+w!9kLZQ3VO(oCoMXL<&w-rA82F;Qa)CbtgG?^JQMAS+t@C46wN3BaHC_z^9I#XQHnJ=%`w~ -uq@6p2c|qKWU(|{&QbveD{g(2=46302P-~MSZ_z(D;Gh?YB`AhE?gotw?w+kpTbz5MM-rZ{b6O=Z}fy -#r;;;FjLP7c&gM-lFyM?Qm$r8c%;z@A=5yU7~9_-FhkKgB>~pPL++7Fey{fya -Dig7ABA}#$~)nre8HmsIQBBiFj*EQBtFsMZfXmA+))MAm2Q2Z=1B>-ADGG^sIX{Ly=LV0xB -VeCHP{Sg7htW0yYsA(8rfU509b@`K6G=N)zWL#^cwlA#0@p81mnS)jsWj)4@IComWW`%C0|$>-1#xheo*FbE%_eCeb7S9?05)eXJru46ZR_p% -rD;#Y{i@l~GB@;ua0;TFU+lNE$E<`B}D#UV5^sWUdAVAL}FE+@j9jLXlO+%?h|T41}z8B>B-`pg4OA2 -Z@*ck4lzaU=*!-QD{_3v>Jz;Dqu0q$r!H!&8b+RkJ4qCpz8b!QfmT2M95XD#LQ(rQfMT*lFuOS1nmrmJLpYpBP$m$Yd6>pRD*^*E9OzxAprw-+RyUf8kM1A4_^^N;F`;O?)`(C!}1G2!a=`z1CRE;8-+ -aGP(o&aKMn7{sE2G32NR7H)f8bFPnir#DsN-u^;9NtssN;q0!_`q*|dm!TG(Mu8!gJjS_ICM7YA9gRx6=S49-nuSTSmOE=>3P9WSS($=voH -}cUpXmiurmGp2*+REdxMYRfwd2%7)pk9v5iHE!6KLe=h -P?e7c@)==J4U=l!*z6%eWw}d -o!VR&X#Plnp#v3_$v|%OJg5TE`5Las)S>(Kct-~M+Hg;%>wOpFIJlE>K>yV8=55 -@a>28<^=84xR*(o$o@q2hX+j+%FTC1Tq{A8q2?=;%m_brhwZHJngTb*MXSKSS`fPc(4KId=djZV@&+p -Dt7rWAr)TjTJg(?Om;*uH{o+mE_28d;#rdE4I4P63{uM9;G=gfEZFD9@scd;@0Ul9CF~f{nhsiemnM& -DB5jAJ_=~X&%?O@=-NF;hZxBN(&@z5HKP6Tq-*dx~!w1O5Q7QcGH~p7B1SBYg&DAcWkG7IbAV%X#4Ss -j{bI)r9)QN7BAdBBy)YLM>|o3TWkC9L-=$t@do%y|Hf{#I=`_SQQ}&TDE2A@^w7||@WtCZ)L;@zEz&g -j#+v68AglF1BQd>n_35$Yp<7kv8{LZ?>v+dwk}$W}nfH9;nvO;2==N(&DDq7|OF}dR33#1B+MZg}MNb -0ml&_aR)Yh-Xtm(k^Xn81_Oa(g_3IJU__U=6!5Hw7xra-QI_VErwl=&UQezPImqzxfd4R2aSH*r0mcF -`3Fqf;D&{yKP@{r$u|zfeU!x8cB$+nAOQT7AsDcENj>1+z&v@m4zYiFA>=%w{S3x9(}zLlGD+^U^$TeII`pDn!yTGdk-XW$yeGr3Yd-z*rXbp1rAhX7?$?AUp!=7Kn(oVJ5c^ -)9(SUTjKx3Y6cM2gUK0VSffv=>yr|{_-^w~gJ*JN1Y(=~}xcP;LkZ_F?ki1yBh%f1~iW8B^~5Dcbq!) -LmC?urDxFZr}BGg1!DI -ywkUwNBHm%dWquN`ybvA<1jajD#k2!j6;bqylYQUmw9WJt}N>Hj0ziaPAfvOI=k3t6(~hYGbzRh7>%Z -Z_OY6kJIXlW+)GaOO^5T{Z;(?tdHc5*2Skau8FsM`@4+ru(l90Z??qA@S3O|c2^M?CT;ZlkMRyq#?Mu -18G?;=~k?W1~?1S;x#bz5#3$0m9-c(VXmbn~OJ?Puw{Rh#DHSE*X1tdlRD!N-_#RljmtkykfZ|u2`8H -AZvY6}QRro08W5u;Bd>P-CVV$AL{((Ar?{7rae$^@ml9PTP-22m9}wSOK}5qea~rYm#D9=1g!werF8_ -@=k1wAbiV_vR*K>f4hal`6N{r>fnU-YhhwW2X}!*6z)m0hw)eIR)B{DKHlS&hK4 -*X(R%qB!*6WuNG^IB;j=UYGYvrP%YtlK=>_L5YXcbX-X4h6@PbK`#nBCc4UBl;T>QhT9MYG -=v3WtwUwb@lq3{aJ;>s}}~(c0I!E;PijWG%d?-Jp0$WtUmqi2TIWp9I<(q$4v5qGJhV#X|+hVs|!%k= -=UG+dvix+9oo2B?R(Oe;aS#%5Z`K5$Ufd$H%pdrMW%V07g(45eu=-s>MnC;nvoCQ<^ho?MPbYqh4K4q -04MTU3yf)+tSC=JJj3@7vEwMss52*&46+$inUfdzwY3JKhxV*UzOZ_TpSSEmK<(u!&nR)Qv64c-;Ezw -l<1k1ioo59C{tT#@UFGQ&(Fehvg^pvE%;#&m3X;VI@(Tp=J$v}0co+bsl&fVKuLP9(;oG=vQ8OqNm2< -E!5(GTKj{I -nDxU5Ke#BXJxt&zM{%iu6E%Q{TbK;DqV6EpCqmMsi6G2IA2qiITf|)-)(i71el-y3(b4-!F%?g -r8aJF1=|?$`^WZOF-({Tp*CTSHfACOM{N@EnRit|V7tgr^q&0!e3r$%~@V%L@((8z7w=9 -xFsYO3{@;|Hi>YLhuAN#*+GGtq65CL-*nP6Q@Epj;W8dYH`=Tr~#oc4e|r6F^2Nw($i@G$y6RIX)WB9 -)lu@s!Eo$xo1VwP!BRYMDwaNs^5@OBhd?nz)Am9cDpx;?Q;`#!_#8b?j@tuFh58ce0Z21TBi}9I_=G5 -4@p6r91}nT)@`R(6_^G(5X~+@%BgXaOu*9V2OPP$twiwggt48g>q40VN~`imc{-r7>%}q+aT{jJ -8^geJDt0I5>xwutm$P1l_Mp64G&XN5jh-gm9i|oAduvzY)|K|E%JRSwA-i@C+}&z;;@5fyfr>l|Z6oT -s_)goP0|MNJX1M{e^G!A*UnrE+8)oJ(8b^<;dYyQ>Jos$PnB49&AASfJSo`*TKvpcC6y&@ -^I`<1wQ3c_)84%u6Pv5{pNWd!CgF1AiM!bYw;pEuq<2Hx1@XKz()Nq*E=*X0P`kQGUviKZ0-6ao|CAB -TE8Z2@uC%E;yn=gyS}Pkh{05bEVKKBg(bjvW$~%Zo*eXUpSTc*w$uwS5%Exqv;P;x-M-huX -!5n2_p+5yN@L9y=Km#J?a-w1n$~UwE_)cTGHb@b~Hm-CV0SW0&wB~jtdXN1c`Ka%qX$0<~GzG9>&{oD -VOf3t>)i>yeBWyzZIVw0<+`#R%YjIFiaJJ_tru)Y?KOC#Ejlt;LZdazW&`yqIHIl{%kX@$;kiSC_fel -E`%KAT?PHKUN2nLmwN5Fu=HjgZ -OHvXhE%NET!IP(La}*~3}tUxI(C4oTN9#oG>o(D67E<6AWH0;gzgHH=J{w74+QyKyL503i{K_an(IDW -EY@AtENGR=I5$%Y3I=z(-p0yMb=I;EpR(sFvF%aXKq7u}tBUsimCd$Ie%B50w(qYQqul$R*rGaj*Pm{ -TOI{CVZh}$XS+-^?O#5PI*K73K!Lk=@O`GM@HA~v+YG_Zvgbnd~&#{EP+MQflRLv7+n&?qu&~qkI|Z!z&6?$8kIf?AZ!GezvdFu@%--d%e=TC}wertj3#Yp#o)jc$zTT6hc@V9(X_8e(Bm -HK$FpHzo!2v!O-))$9i-uj3MUp;>)a$`^ffEMeP5sL*_>$k7gJS%QqsxjO+s3SBQ*lC_WBc#lAy;Va};@_;7WU9SJ3b1VGYN%J#(7*NOjd -khbxrNBlCR7gr$_U_jV}{;1*Gyu^M -QAJ&vvtvOx!1#{2GrtHk}!;A|`3Xn+t7c)}QYek|mnmTZ7+5z4g8{w(Wa0)pi-fF5 -UttlHPyatqO9KQH000080Q-4XQ;2if;kpn20FONY03rYY0B~t=FJE?LZe(wAFKBdaY&C3YVlQ)La%o{ -~X?kUHE^v9xJ8N^>Hn!jWD^Pk8QCnG#-KLw$mUnU!_iks>O{aElvlDx0h=e566v+~#Y;|Y<`#lE$Nq~ -eX*}F5l@=%t@gY(9D0MI;7S0o7LWl`oV2nb(gXc$$nzneZ@;SP=7qI!5)jC*ktVBK;OyXqa1-qLP6@=3oZa*!16rJN91u8hfA90Y@V|$rXf=ggdRK&( -?k?BDQX1klt-5_TQRsdBC|A&8BN|6Mb2j+2iy`FV0i~PX$W-0HavM4(SkOZ;GGv75dce<`imZ1OPMqF -K;wHrJ0d|@@JcH%WGv_-Uh-}=fQgWzPJ-Xdv|u%tR~ZYDLqVp*^TxlWJn>s>56RcWyYU9o^NTyrH6Di -M48&SxJZ8T4#e)G`$pEffZ`eljfh}mbc^aYvvis%TJidAb*9lywaD4;Uw{X3N>pQsq4A+~R;wJI-#Ws -U0yvgC6!8N~mJOTVCfd2&Wp8)<7z<=_3UwkLP#R+h6@&>N&;i7N>jgt_r2ri&8 -qRagL#^h`w>|ao{$*AfnQC& -F~pjc@Q1^R)) -=ldoW=zVsRhxT%MNM5hyr${HkB(P1n~h-M2~u%9W -~Lh|>p_`$HSsA;}=Tg5F-n#TiN0`02y-nG|2}6_05iZwT7tC0%7_Bl2=lz&k6#v4y8g_RIPfIK2m7h} -fJgi&dq=4)qBXP*^{)qUWRGzt%$S+k^(D$x$nur**CvTeu${EJaFSmxpv}RlPr2|dL5 -0JoEeG_HnBNC!ASyHYbzZH_5aXR~rg~b>A2u$Y11V2HxyKU(d1VPM`&ZKeDbGlWHDv8)54B-%WYr1V> -+MT$#G9AifS)!S&Ad`I2JdZT$ugU4{uq}Y4vpxK^L&?CsO$pa|OVqBXx7k3i4{d~vWydtjKrX+&Gqfx -kW@yg_+@{1($c_h(7p(BZY}nSfgH3R`iUEiOU7?NVtosyj;_PK;81cZ1rSF}24x6+oowX>rRcP^c`1n -GcdE^E0m+TRR`*J$D9S)7T12tem{HpSR!NU>JPNTqVDtf@SQcccphyiwK=X;%a;ElG%h@p-lg5{jX{1 -4{yBrBmF#gc34G5qIYkp}kT$zuAqG+`t1r$5#3d5~s>&x@Vk1R*&k`a48C{VQ0?|S~6 -j0vqIcJek;2lUr6Php7*olO+5EN;{t^tlXG<1y(VM}2`Rgx&k>RT7CCEx_2_J+VGDA5H%XH>}lfTgzA -B->2F7#>XSP~Wd(J{vc9#(%y3=|ioa{0@fFg1|fwBm}t%7HRUz?4psSVJ0F}5Q>TbrWN?*BP5R)yheV -zxVRv)GOihrA2O&RK^wX{mfyC3H~d!6jjpQ^(i(xb##Ev62j__6;(V4yn~U@8;;;Ox^W*H|{8-&MmQM -%68cE}ECb|(xSG)+6qG=oybRniPsH>3`l_jGQB9;I8Ngh3 -Y;fb;v!^mOcRy@BPK-}XG=O`1rI?lme>t)o}k5p6)UJzxnPB<7K{N|@UVo+T`Is<2~B -{ZKbo7$96EeWAqRCl1fsmx*cZP!%}oq_zn463;#merRLCN;Jbl3Y -HDgP<<{*fFcUOh8yE_P<4@;J~kVP2ZWT^q2Pek)bj>Nh{jRRCPexMs6@2ijaq*oOUWw$y|ofGm&FtXr -lC|i^onl>71=1%zTr+9Y}uEaxB#knCYq^y)&!v##o`Np5+erd;b^J7B`F?o7d_>^fgULuCN10bA}W^=(mt{;04piDDD8=ZMk{PnI8xa_XYNZ~A+*q=bc2Cn5qNXtA_W#yP$!;Q~Be -ZX}Jh($aBj5Qv)Xn`vd7Baa~L0+PJz(1O$;tZVx9FWfE5LBue$3Z8o*y-5rsM3y|Ed{L7HI^?cn#k}R -Zy<_6WJSLfwh0;(W8z;UQ@Gp;DirA3Fq4e*38-o1{qm0O7LOszipu3OiycJ|eGRQF98cYsa>FHdxWf2z?0zowOs!!g94LW0>FkoyKhKr{g*_HDr!wPvJQoZxL46kPM@u^Vq?tVB_Nh~5S-gC1A3eBC}PQQOfJWZF- -ELqetcmGu(z>$3z!M{89Q;=&`B5ME11(9Ize!0HO8Wd(xXx^IVzQ49F;<-o*F)D5=4_wGFVuoJVXjae -oO -<>MdZooAV;TfR7vqP8=BSD%fYJW=mx`0b`^&bcU@dKV#C$#vmfWo+xJ3{^F}l*kdMRb%3Q -Nv;(01KOpL`!q?R&IWq2HWjfZqgS9Y@3-K*vxT62cUMB;mg6Vd>$<%7H@6{#mkSw4$fGF1>A@^+a00M -%R7bQ+tGI${dxTppog^ydJ9g;(fsXCk-MwyE?A>I5Rs{dHo5|VbP10iN1Ci$=2SNp9U -zl&iCitowbs$`0Vc$-u$wt-9@Mg`MyVEsjK%>b49h`h&BDI|88y4cw(1lU)dE?oo>t2j62aJ@Tf;+t( -_MJ%+ibB=wq?4K693morVQ{Lahhc=B}=oa)LKgcowJ$f(-{C{2cnt`gi=^@-0{ohB|QKs~8Pyx!y<@rolxlrxHU@KWe* -eqb^#kF&U`{Lm@@74kUSrp8ac<<9tLUi}lBJ~7UtcTfMCM47hXxy7ygsM};2w6}(ZqSt<$S|g92xn$s(bXX7DDB7h>qhna&u@67niT>b8N0XZgcLn!7 -ua_pP^Oe$4xCE;(T8pG^D>tQL=NL&Xd%vUXvT(TM -aa{n(f=ZdYy5%rC<{m##ZBrmspg*g%ZA?g(H|pbTZvGC*^~d)gC*&=ZcZf@s2cpbmip09nIqV)(Qqy- -rtSbn~)qW1A%{w3iNA&0$aqpfS`n-#T@7rJnh2CtPqyBw$cV7Bhc1L?7FpGIpTyyav -%0d-Z9-oAxA*s%~=ZhEGYg+k={DP89A)ehsE=^P_sndD25&?m<0)nB!zH%rTmWOXM7CX4EqFk80Ksm!;Lf!rA!D2)tyJPc?2daPGF@ -OT=e0J%l7hgUlG&3d_R(K?s3;$jD>Mpye7R#!-dm*Ab=ft5Ip<+Of^dN34p7#-F!Q$cA~g^Ts>(H*|G -Rd(h}P6MP<6GaaG1)|$3QbR@)UJlqzre*DcwD#dQ-}jkYiMMM$b+Lms5WC#e+kt*t95##KCyh7HTK~H -V?$*U#>=?zB`6HNSSZo^L=;>LI9B;8(zY4bEbD`_%3Obstl`;DHWfEa2{LD&tYfZbeaCI -L7|Ibve+w1w`7viF+=zG-oE?r6Fj%5-huT}fH^oUP%hjw(-+o3r%pc!a-dPMRBNxwQedLfREE{x@ys# -WXt9^6TIuJ_zC{DGAO{hD}Z&h-|A(Sg4{Yq7sp6o2G}Dsplbri%oR==OGL5>w~cAVv~{8RLg=TNad*- -K+6mV61C}YTMM;!@ZaKyt=cY*}60VgFAL}QF1BGWqsTw(T8I9-y-RrG}p(GxS2lCehEm(uK&Qa?Aqu^Z(t+UmpAjWp!_9Nt7x!fD*#@?874{%of!bASX&YlPEEv?}mJB# -}v9S(<%%||u+)bzs1H}yV${!A`^`Q_s;US|JUKpw~cC=jw1+FZf>*MhDZU0i9#{$P_et9AvsEvASf>oBoUu+!wKTt~p1QY-O0 -0;p4c~(=>c!Jc4cm4Z*nhabZu-kY-wUIUvzS5WiMY}X>MtBUtcb8c{Pkd -3V<*S!0vrT@htwpUw9KKQ#&Zsb#$BH?^VQO0!ef`kSMm=oQY75Y+f;}#k5tXk9*wZlp>aTY)LlTnN%u -!&;k(O$B?f-o?IA!D5yTi5$EnT-2yjIO9KQH000080Q-4XQyCiPfu;cf0QCa^03!eZ0B~t=FJE?LZe( -wAFKBdaY&C3YVlQ8Ga%p8RUt(c%WiD`eg;Psw+%OQn>sJh$i%p${^io*ZLk|sv(vU-LAqd$X?TTt8Gb -1N!O8}?-V=6eOLc{6OhtDtwL@hwVg0+O;UM4(|MA -Re8_8gDH&A!2!{>gK@sOLd)b8-e=0_BqEEbFJ -H3w2nfLbv^MruzGBAR1ejKCH({KkO`Myo662({U-AHBngDV-bW25VecwT4w~ajj_6@6ZLlZH?AQAIL- -nYv8^^Dw3}X5^=d4HA?XTGw`y^*1HXI_>aHY__D*R&5Tn -QpJ8_yAOHXWaA|NaUv_0~WN&gWXmo9CHEd~OFJE+WX=N{P -c`k5yy;aL@+b|Hk>nj#MQK%ra*8n=CMSJYYw;&*BX%#O@lLAR4&fj;bhegRw`an^hEY9w5W;ujgXHOt -y+lStvlt8D>x&Z3nt?mQL@w@)N*PI^8n`#e& -|iUP)7Xgg@J(T@>Lc<2T)f-e!a7SPL@x2M0F7oK%O@%0kZNm!;sIO+#YZJ39dZ;*+>C=*l&2I-Jau0; -#zU8`)yXk2Z}vs-t;Qz|44>Xty!1XW{G&PPv)tAu6tjX -LmXhTDF9(zyI~V*5g`-&C=q6^`7I37PiS6C4$_6^Fij~Q0N>t3Ai-~ec6l#xmf6113o3qQ_^VIM}Rwa -QLrgC#~5m_i-j-)Xe_5(epgPSC*iDF%;nntYJ@>LDNhYCJzpJnoc!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bUtei%X>?y-E^v7R08mQ<1QY-O00;p4c~(<^ -umljo0RRA(0{{Rv0001RX>c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bVQg?{VPa);X=7n*VRUqIX<~J -BWpgfYd3933irX*{z56Q$U*do)#?t7$nWrc9F$` -c#Kk*dF7_h?_A_E>%a1)bR)-`nYVv`NNhRNGDbYz@mis({N^jf1?sYgt7&9%WfYFTJ$-TfXriqw6!Ef -+=6Q4a>0qj{pa(#iHy0TN|d{veU$0cDJ13}dv&1cHM;#GDqmJ&MSjNK)PUQ<3S9>sEp@UI-984wOc03 -HXc*t?-WoWcf@H4u@~S)8PPG7?N=Pse#{=YYB8CgfNU4l`Y?M5ORxC%IkR}`Ofye9LzKDJW{Gf4?L65 -{DmUtLbom)R6Y$O&(~(q+nBg1*0bk-Tnxyz>^|&esJ_i+Tf>_AxEB*!hbh5V>*PKvb-!8WFIGOf@r*} -e`$r7H7jM)zax{qSw06x`V)Sl}sS>&VT1G?h9G=L7_3)}w^BzIN_6uiMsx#$YxmEd-G4d}wl{J)K)Cr -yc!_-i<2_9a*n`(Q9Qek?-HC)h5q%HJ4P*+`BcgR@o8&Nr=YFpnGct7O9J=~t?e+1q{?o_~Dq-F_sxH -m!sl{s%vvnAIpxZ*ERO9KQH000080Q-4XQzC%-#FYR503HDV03-ka0B~t=FJE?LZe(wAFKBdaY&C3YV -lQTCY;5+cFc -7`-6&gAj!;Ith>?XaWNhg=|(A+W}tiWQ_fJD%e)RW1-S1V);HYQG@lYrf~@9nNuq?IaI6s0xR6+{seM -X4%e{9dcPMu?i{DNDqY&_c;6tEGrl0#PmyDxPc4O9^APalPjnJkN`Bt~I+7xv>i9-K^P%bB~$j`~J23 -3o&Z8T%HwxoW&WhG~oH=&BgDZ*Ehd@ESC)ViV2B|Wxx}rQkJN=ESx;hQ5#jqbSgjr&Fd?UBxh -EAPr9;S1zK9dQkyK(2P57Ui)#*tCe$}Gt9v48L9`6OrvoucC0rc8vD(Y@nA-X*A3h5JE@or5)WHbdOK -@D&s%)2tK8r?fR0X$Mx*iYOH))z-8Md@I!aY76Z1XjhY*_GDA{39f^@O(7F#)~s$5cp+|@HkhZe3&Vr -2d9e0uU1Jp++C8$d|TQf*Np2ZLo6eBti@9lD~aX=L@uLO<_yB1AJr)!mb&D0AZQy-*%Qxepwc-V -2{)t{GgG=k2jMV#r@RHDR*?#AyBzkQMQnwvhZ)kcnJ3_GCMPaQlX38#g1bGf3zME+hmV5wPR4CDLYda -R83zwjFkLcQ+c8`s9Z;>O%y%FBaIb~lRjXYzs?`MHAAZ -fS$iKI@!HJ%*lTTG~&%8P7<3q$srViiY9Xi)SYN#5+=B@&o9`?o>^jHhX6ZX=~zbH`yiCb -UW9yr0(BqXM?mtrO5ruA>6Jw54k*(Jt)d9Abdf_<#vfTkvG&Tdw!GWZq_8d8WO`gM!>*@mF?*`|2TL=NLQeOD5W>yz#8kuh|4>T<1QY-O00;p4c~(=B(PW@(0{{R!4gdfo0001RX>c!Jc4cm -4Z*nhabZu-kY-wUIW@&76WpZ;bY-w(EE^vA6R^4jbFciM`Q=AFP90-1ZKrTj?LSgKt-EK;7Rr|QnYD= -CZx6MZ1eMhn#|HR1>D7#v2B1@mX^M7&2X3(0iV`J_<^ -yP+Yo{(B57=6PM7>j-=A;ZzhEC_-m=f=R5$Y~>VibeH$wg`ZJjIl4m$+8;+tDaDTu^M>+nbLq-D-!6D -*BJ;4nUJXgF1O9uoHjq^IcP2n(mveZe=-KOC4a6Q;{HAgT1oyt#f*X?q_A-u8qBjf7***6=b-UtB`8z -{x5-Ax$Jp{mv%J+6Hrh5km!zOYQlqge*c~poer)I#-lTdxuxx~$6uN+y4bpuLx>j6MyHHJ7tc4N7=xBQXKfvt+tZryss=zhCW=%{yFBmNdXL{5OX&%PeDFz{7Lbb+KQ_obC!x -I3SBWjy;k*#}o^hhQW6Q<+aDZ46Jj}1CO%2iO2?TWnYtJ -v>BBU~$83q^dn^_lGFY;}oLy!^Y7P4_|fWka1=u(y8(G+g3E%%H0SZZFX!tqb!rYra^+3NB?A@B}(N? -rK3w43JKYkD&r@TMNs_fqP>6;FRK8@on5QuJtUInIcNGg0nMu%3(^3(sej_<3Zyb)3@+j|RuYAwiX!kf?0lN7 -%{UO$*lS=D`u>gyaZ|*CFGi!A{W5Y$YNdd!eguDeXe%qIeT}we*yP@s!AS+)=JlGbKndN7DnxKCR;Xm -K{Sc3U$M-16BsrgV>}InW3sQ*M?Gh?WMKH#{m{f*` -4<66!La_a|=6*LR#OeCa<_Vo>&|M6wd0JS4?bAqoL7Qw8h3&xy3 -vG2aU2mG$;OsN(v|QT`=es`9;BD0tZZT!34{Y#$@gp}nJRN@r66_?_Ul|5_`b=jyMR|-8Gs%U`8=C&; -LL1!CRw)OnrDD+6qvJ2l}NSdFw+v!UFFopu^|8L?7gF$rGQZ6^uA$aX7j^nhlN^gfJ3s8T=dmu`1$sj!|nu^A^Le#T{3}v~s`)JsbfevvC7W#mscprgrZQaj$C(ntadbq=$ix?V -!($0n*u~K3maRk1;wj+F=s86 -m4^-1x@1|;L=k2k{02g2QE^ -~=XR}NKpgVtWa5)K^8hB2!L1;^3?}Jf3dlfkEhDUaHM?oCW5w*xEk(MD)V`w3%E{W>{&V?lq%P|H~2VWrZzNK}?4s@$3bv0K@A5 -klT<6dkQhGhn!)Jf%`NWmq$J^*mEEFLR$%sU@B6AAf}W@{q|+VYjip2(#4t957%!5zKqL`}WFHUm7homuU}QVy6=Wi8f}8`OlTbWS6~=Zi<1^Uy9FH< -CgqFrY8K`ZZ6|nkpEF42mt_qcX|7WKx5MXEXPUix<>by{Q_^YYK4woW@qEr^b$Q~N7E|EaER=P~t<~f -`%;Cu<^Z*abX^L0wwcBIST6@9Brir^csrPkx-j5j{>t*mJtRpzBMz{zlhVbbURAr^5-2q -bp(se*qs<=@e6#Q9%S~qRxcF6O~AaD7@JfQ(6`0tvHwgTvZ%vL%T0i${8TPsF?Ali|6&;#yrd|Z}E?| --BF(p^ -L8&_^0*mDD}9(YsKey5B1~)ou&BiIE*~&@(+Dj!%8LIM**PNhN=Z(<&Y5wvJF~cm)o4P1mG}m-Y`T*J -ch{v0lgwCi+_1Q2}#VUjFL!RH>EvEs`!y^zuFSPquq>!bZaNe=z+=U~19vB!#1OX?qjR*%K1>Ijtcag -_3R_wQThGk63qpPBktq8e1@6NWpXZXd96MDcN@2nzw58q(s4;zBITrM@11Pf&vBGC@0{A -ta+CHXg$8#)648Dcz>=a*_rKo^_6xh1CFETBQh9eU00x8k1hD96H2R+Bl7~$5C|JoX2{t;3{~U{D@b& -!b`O(oO=fTGfmppjB(K=UWr>AS7H^p)uX31%d+&s+kz20Q$sZgrm>h#-h|McSHB3NbeNTl#LVOnHqz? -NB|1EEzARlNiBS@7|@n6q$0_LO9wMI5u!r9pv4kAw=KYL+BCjevNj0tSFQW+4ZVo(Jy$Rq-I^Iw(|dn -`XN}q=DY>fB`863A%-8h!c`9JRVM+=+DhDU7{y -}2;utt^9$bxn5HSpHbaMn?Yi1XTT0{BdD1hIn7L~CI6ibB`pSeVqM~xsp3lvD*C^)Vw{2sXrSQ-U+D* -{b(jVwH9zDGyXqoaCa=J+t_5A7uQz6JbJqCzELOSa?Dg2zVG6mS4I`q*IrNz2l!F%D&Xa}tc%c -zSZEc)64wj8JCjKJg$$k>pBwkRP_EV8*4=FzOE(@)*^e|4y -(k=5a7nBzz#)AZ+!#xCr!Z#$eJU)!ri?Ehik^8Wc1QhQuQp7Y{OYs1#SSanm^0mS}`G#lzYY}JjIDe(nXpVo{12tN9At$e$72{bG!6tf6SIX{f{|x<7>vl5R -XVD%wshnJ}7(`Ug1xQ7+kT>jHk?FV^V&neWuyh2}Ji^0myRw0DqD_zqRKNlS6->q$dI>oGd2@C1m;Cu}DE_n<7ZXzxMsa8`qY -`QvIW5+5n(5j`jyUpY8C4~l1gwTA?cT`N42x@DgJw~$%Tc|*eVrZK?7y467&V3>`=rOarWY^;@Jp%;n -vDmI3(=_-^W*R+ALk^STIGb-B-D71}~L+;3Fx#^&7SgJGHH+JyA_&#uma!BO6NINf%f%&mMNq=`r?+2 -_#2W!AO^>Bu4R2R*UGi=H_SpG>wYvo7s%M3W22S}E|dWa0BSxOI2ZPy1DQJT_JKx3X3N}ShOJfQDA#G -kT38`H!5AX%ygGm#oOj79!#$RagJLJqCI)F=^O(Tdd~DB4!l2s{Q-)Ztz*^_s1Ql*+-HVd(==l{`!y9 -TzmE@jgGy!_6(1hvzL8DQEJpl0||KWSBPUL4<_K+X4A*P`GBe`f(UZC?(SM%s)P`*B&YIBV_S@?GbLo -@s1u@$l?caQVvDBHNw#HLsPCZ+E6wK+ry%xc0z}T88c`e8w8;}`l$y)bEZpE$0a>aPr0H%!#}L-gSgZ-4+ian;FiY~7TlU#1TV-LPoPs -;yz?Lk$#SGKRbefWwrJ>XMavgSTeFKIBG3QbX?)H&M+pTq=bYpE%1cu;1F|cdnFlU)*h^R3B5>`4?6^ -3A^4-}zPQAHaU0#e4)wZkA31PYd|RxX$Xl7bX&czDa5S?a86*RYr=XA-~x5_D1A2cTO!i#rO~fPu}SO -DlLKT#=7NH^Q=tCD{o~tOM=Xcnh|Q9cCanXKSv+XQxvvvwdU>=AA6EhVvCGT)m5U%p6s=m#iC*!jZca -O1558Jc;%xK^;#+z0VzF?rLi%sHkD?+;-xr8YY=L{xwRs+}n$obxEPMtIg$turqlZPATXH(oxbvK|#b -FDce9B4oq@Ka}WkL%b|GF&0{L42wn3l9KM=G+Qi-%stc68U|9jd&QtT%U0|(&e5D7h4K;|)CQl8^(sd -^;ZQz+h8p1-b^F$+Hw_Slku+mHt_&Wkb2yepf2oTy#?M!C^w!+bJi10Rl?&5M?0 -=k@~v18|?E5wrLoaq@c)pdzGTh;Ls7yPLZ(lwcqZygXCl>yz6Ad3`?@LsZr-C#w9nXAC%in2=^2n~Tu -Od-`hp)#bx!Q?wr)Ll-gfkgs&lQV6q$n(z1eFX>KEYgTOGY2%R-S|Uuh&GuUY;;5S8kpq+F&1SWi?rk -~x0A;3r5j4`d}KX`p -yWjy}WzrT_LK%wMSp5sHSw=ep2oF@oI5;Tc3vIT>deFe&?BJXlO2?kM{OW}E_NdgD4K;4~>bzXMK6T4~LPY(Hh^Nw$`&= -`r8BUad4oM0&1x&Lq<=vz3-yhZ{#^X_L?ac5DI+__jKHUuANp*8AfoFnVzj!BHRGh6j64pEGTt&plrY)%m(bJDI4+@nVW+_i!q4(u{ -JJ2ncI4d(qUj^k%8-&4l2z+^__{@=e%5Ea;4Y{_ElTw2z5tm#3O6B0|-{Y}#CvI>6eDzNP_AeKPx$kR -gwWTcj-ZhDJ`g?H)rW`+paO0i|;|FMNkNpECOo%v6LvRojHy%@52z0V^+*z8D?#z@7h*) -{hNpB$7U2g_ur;9ZxKzJhRmKXv;XAy(+T}szsS>?2LR0t@%p_`mqrN3paUR1>ie&G@e_-6oNm_Jxb29 -|1%6Y&PDTfy9oiJ`pyPS02xZro|ojz(x_Prfdo(t1a43&&rXqYMYFM2j&v%hg9i1K=6Z>@uT3<2%dI; -yZ&eiv~_FV67BE-&XM5cWyL^A2cmROibW#O5k+qw0buXV%|BdzM~IUx2XXObuo0f!@`oG%m%h*zbILG -Kr0W)X9o9)>70jLaKXIbAVMcRpb*G>(wXSJGbwvV{y%3~@+ytql}W4FyM~lz0TQ?nYEei*eGgR!Tf{m -xv8G7v5Yn5^L)!`T`t$IT#L!k7xy42A9ds7Rzf|aY^Nd_o+x4R1tT`T_7Eu2HcMm6Mvz$&W!$K-z*Nx -6?2&VSVy*xP1JP@6d5v_8<>(*5^{6aP@;tayg#pW=xSgilrIbl=7dnd@c;MY`aK%W!q7P?2n4zr*Cm= -OMuo`Hc=Ol_1&?dJ>}*s)wJB}tl6VAn&NgDstNEum72_P7NxFJtyuBn9+Xwq)sQ6_C|%bCP0K9+)7D0 -2VVm*5-g6RNT-c3(R&6S<*&`6}&v9P``9a%Bij1A|b04x&~0LJyfqe=38c$ui}j9<^{iivYzYZ0vlwO -2|56?1Ykqz)Zd0xDSrjpblzf)!ZWKEFKZ~qHeE!_j8?eMTD|A2XlKB@%-9NLw*y*o{9wC*nNwNzOBD! -JASuF)38t!dNQ2mI=VexXq@cY%44nrOn}HxzKwrEYq}^_8~2GVV0%zBVM{VJ15*88B{CwVb -XayQPRk4cnqJfT`-Xxy?k#d(&HO^HguU4fw}d>CtPDiO^0QmAmEp9iC7j6|R72xvI}T6|52T7*wx)$? -TWH(yBOWpDn#LirKu$Y}bse`sB4$m5!QX?!ucUBWR|5TNU9(iH5?pY}IBhId6LSL-|F!CE)_UH4R&FU -bUhvT8@I>$=d0|o&BO2LJB6Wd1l*v3ztbNRn@k8tqn1*FxQfd$-E``h0I&6lak-$5B+}iVwkP#a1*eg>MAf@J!g3iHp5u-7 -pKn=_QKoNXj=y|6oxb!F76kYGN(Xy*NSlv>S4x3{Gk3V)nxMHh1Z2Ql^JMl}NK0hlpmcH(kIM)@3d9oerDu{@>bNS4)}>(Nz8J5lW7qpbqueC|5g^Wl7Kmba;K!Ki>bShInLanbVB -z;LD~yenbcJ@uO*T-fy(4f~0(Hm<~2<%dvr=Tz{X<%6~g-Xc1YU!VO4YkY+l-!_jy}SqhXmYyh$#kRM ->$fR=aWsZVIgQbR+BGN{BQo+s<`lrjXJ=39NX8xIV2rwgnJYyrQH_mfdWs@)s@Q~|;@DNV~sfIeeH7GTrwOHx@iv9ylhTi(pMwFmx{Fo_sDAb4&D&otzF%Ctx%lbg?;kEtFdnU1D;)*a#CsjFwk7$ -!`ma8Gc>m*f|M+lmIS(#-F>KWw8US-WVPrZn_c~%1cupP73+Q~#L$MO@y4*J{gQr`(d;j*`#rqFGUR+ -w_*$Dr2@vmRrzW*M{hhA#CUv0O~ZMu*g)M>+5fzJt$;yuFX93xW-@+W9_V}#MA;%`5cn^YE>^3_G359@W)s`8?1zdb-lqZn -X$>|qu{c+Q3p8;$A7wMke?Q3Y*iBpEmr;?;5) -e$*C}+Z!s_Z3Lvg1-J}Tp57h;-=IGwjP}Xql+}EpORe -f@wun1xKG!%j&D}$HG)};?P3{F4;6@+v>SgXrs@=lqTNv@9hGluV~&j5EsZ`5;HfKfGw0~W*WGmLS{T -W7A$$0Ph6)3|!%g5fhKE|=ad*1T&x6rxa}%r{bz9CM|F)|2JFYslOe4xVT3sWJyQttL||1i{~<;JpcmP=}W0OTxa@tbe7aS(NdUkz=c$*-&^wOlid9F7a1Qv -o{WM+sC{_nKZ~lBe5*)PUub0&N-OUHvthL8;u@{C1EY1wVqxOvgz;A`p)h)Gp40&| -CV|xYp5zIRhGAEk8+++oK`F&Pk9tDw}tlXm^y1XaZIAYdbyZbWu7tC1jw!9VDeDVOJPSLIPvh1=C)09 -coqAW11FjM&?$s~V*L4Jx%FzxIFW_3uCODs-OJ=&;TvQ@=$GP?SBefRX~?E3DTr`LD?v4RKo^!i%e9E -06`z|vd&tD7zr>I(nfOs8{{EpT4Mu&QBLmq%L@RZekBZd>9VSLc0RH9r|$gBxB&=u1W}HCpan!8M~Y5 -pR8Q^SpkTbUJa{<9B$6L+~@I(*<5#b*Odp6J#+O1B;su4p>8M0fQm2WQ+z -kJ}Nrh;lDuxX}bS;Or>OjY?0jB|PXbyhsh*;b4;C_8&?({)~r9=H?^T$=D`nJSCadUDx;`8U~H*p)|_ -vLae%$l_tXKxYc|)264VtPp&8M=jzu-pz~M1PH&E1UQZvc_CYWXz66^#6HXk~3Fx|9>1T&jgmv(USHm -l#ImisoBuJYa|JpEO{^Uyrb?8@)^B_StRB+eI%i+2X>zead<_nVpf>E{J#lLY<-%zOujoO{IPS{c{6S -UTvj`^IBlMeg7IFrE8v`QX`?A{`B^!)U(dNB$f52O%n%v)FL1)ODYb1;c%QWr8*y}EHM!6a2xI&czfn -@G1NU@kOIAXpuk&^gQT`i03jc&yGxkJShsN_{ZKQZ<>0Spf!wGr&QV`u|+uDpfa4_m#44r_;leJ$n9F -RgCQFF2)dZJRwlZ(9}*R`gsA>=z+PZXJ)>JFn)xR^E%G(cCLPs*l)KRV4uytz6p+l(dmoP(NV8TDP%~ ->>m~rPu&ijkM;k5f9i1vg-RP1<@1sU1FzT?@;>@B=Z==0qzps_C-S9qzKjW#f@o1c@KJ2`H%?e1>xwd -(U{!P}NSB^%bUnf^*C(j-|o?okG&-f2F_t(4G_3nOrb*-*a2`6ZIz~R88 -9Gew(}6xK;!RVoQeuc8(t`J^VE=V=?8X({Z7jD@98<07hzJ5aJ_A>LT#X1`pN;zq0Cle_kAeRobvgS-?+ZF;l>>WPD8amj=dYoE8+<2R`IQ_K!#)XIy^e}>x1<6MQ-xw3Pha -ERNt^#K3?z61aODgXcgaA|NaUv_0~WN&gWXmo9CHEd~OFJ@_MbY*gLFL!8ZbY*jJVPj=3a -Cw!KO>f&U42JLe6@-eSHsTrEbwCe8;O$y0Jq*YKL$T>qoh%uW+>ibGQMTMhPCH;a7?DUmd`XGvtm&by -yU~vp>l$P~80$eCol&F5dfpe%$_MGB(FKfJHm1c|Nsm@2$5@Q9$XFL}F*tPact`b448W%b2Mdqh34t=)gMB@eUg~t!D0VSQ!(pKdpJs?}`=;p#L)OF@iby0R$L(D`OKE51w#)3D$Q3vTK<(BVJ! -t&2UpiFUl9(MeYILI#g&;{*CxW5&@IQ&d}mK7S5y=YtH?70>_VS}uopnS7gD_u#In``*Q5;Lk(U6pPp -RU!Wp^bg#Jw{hC|SD%1-tvh{NbD~&VxH^6~&vpT~=sXymI@0JF!h18NliFL;j`ZlJo`h$JmaNnFF?`7 -D44nc=AMpPw=cJNz-D`LgN2Hz=W{Evd^FB*du`x7$;Q^mT)8mgfZIS+yG@QBeJr1y}RXbYEXCaCvP}&1wQM5WeRrrs=^IY&`WM>Y*T5XrTw8hf+dzchq3aq)Arv?VD`WTJW@)`Tk~SIOi*m2 -&z>sq7Ps&!ihR)$R5x~SbN$7S%jErA^NU~olhl!vJ|)I8Cx9H-Wi-QCPWgp_*5Hec9RLXQ{0ke44@b} -?Swp_ZOMb)J4ylDxHr#6*Y`N$0*ah|o$;*PpbcByo43@!3RMM}Bu@~x!2Y)sXGOczm>dIYUL=%C4tptCw(P8yw7tW03VLtLam9T}5S8I$a0@W -oSue=`>SX_DuJ|MW5*z#~}tQ4Eg_x(Kg5xbU8Q5jhnCLldx+XMdrbOZTsMap6Pu3s6e~1QY-O00;p4c -~(c!Jc4cm4Z*nhabZu-kY-wUIbaG{7VPs)&bY*gLFK1TScv(qhfIewi)P^O^IfRTKx#m)YPz#>Qj3os(OwmGnUXC{QOy~ojrc$xa$hS2uSqQhrDE%sEv%U -Sk4Ix@DL)b0Im%aqVF^kDNhQi04ncft~*y)G{IH|)28IqYJ0$|ZVxW&*hAq -HzKTJypy?oqR{|MB=lM=Z)Oz`DAgRil&x-O+p}>il(f06}*`2BAC*uVj}Zf+Zi>K#K9TH3N&wVL(LOs -Mb=h@km*m!g^#Ep1b;1st?kDf{DP6cCgbbDZwystOZOOhR95DM`-_-Sp$S#k9@3Z~iH8teD73D~pNal -E(F7*okJZAyseaZ?=9;}~cc3=_QS51paJ}vCeAnuJFAfA?7Wm(Nih*m}90rgz=d%&q#Fok2RdsOEkHz?$LEiYy2#=|ws?bHkhB=z{I>ZgLIcbUCa`&)`X7J-c4UW@Her~iz^XC1t<{wZ?0|XQR000O8`*~JVfN80hL;?T+@CEsJgq+2HyI1op6`yU;^JAuSY^V6^r -)Q6o!6lCxP#|9eNaqqXxv_psGrwVsdn-poi!ZA}A3QFp$xSQH?e)>seX*%{S&EQaq4DtVAj8l6F>Woa -rbl=NtYa*WjhY@n11yByj)n#J -9ZuuCfzDyrf+2c?foKj$ZI^xmGjcA1nkZlZVJEfyKKgPRq4l3l*)=3NNqgUP0JE;o8AEaB@Q^Sjvc4ER -IS`q%J(I>!)nwj60VzXr)DsuEuCO*YH9xo%#Bbe2DByhN@6aWAK2mt$eR#Ro$YfMuG -000OM001oj003}la4%nWWo~3|axZ9fZEQ7cX<{#Qa%E*=b!lv5WpZ;bUtei%X>?y-E^v9BR@-jdMi71 -HR}2&ckqntYZ!ZEIz;%qaK;r_5o0p=HR^&+BX1S~GE+xA~|GhJ_q -N}wX@)MP`k1*7+~i-Y#VJ#ZTC>>-3~q42p_dA1II;=9OYdNKiUH#vh1rIuIbmvxrI?7v`ml6Yu5z96?&Ku!V2EB# -FqStpD*FY)$&UQ0JkYHhx-ZxLfwQ2J%LyuXdBV*R3U^no2?3%52_0Johxy(VX(O}v2xA6qNd6bVCh+a -N*%cueH(+`TNN9R!k$jio>FXxzA2*f!Ym-taif!vzvV6c&1M1j_z)Xv5amY3t<@ta6C7-@uL4VkQvsB -9bd(oTkXqsR8}){s(iFT5q|a2M3Xgv_+9OZ)YwrJWq;C5MX$R(oA`z9u_nsbppB`>Duzh4|M}i`t1r@ -5OyOucU7?_mkXHujzcm%`4(cHxV-*#+DP%(PFZ6dNT2)zMFpxT@DKxg2QyT*q#_e{1++6{4vfP~ptNPfy6fUdXHZlGMr%hK)gD|jr1Ro!Q~|T6w$>z0>M$i -)hU;pQlCo?zn`N2kb-ms-yu0f)Z;JC&y6LI^qEXgYZ5>7xHbks*&|TFyVukPOXDnHc)7g!n5x77_ -e(7-wRRrsWVy-vnYRxrP5Jl}_gZmq+?XbjytcCF@m(#&Nsy2C`D3%-of`$~^2T^k7x^L_qwPtI^u>C< -f5BKX-VLy8@LuRRCi+=l{n0>4B*RNlvYq5&8`OfI==w+kxi? -Iou6T8`6OO#R!2IOx|7SwSj$bOyf+YHGqy#>jR&}PB5>Y41581w;yib -K_PxkuQ;?a{8>gFs91~AzKXBZnoM+?q&osh*BS@pa$DDLC1yTmFmUyJssjE)GaZF&`sZdL@>p}&C}=w -D-#XOasSAD1|74MN`dJ!ZI?_+ZG`>{7a9EQ3`Ee!Rpn=_TAHHy8VN#J-n%?$4@HKerz4LQg+Fz~5!tJ -eQpB}y^&0dBXZ#BF;cRX!I%WyhJCOhvV{{>J>0|XQR000O8`*~JVK0avt@(ut1wGg+JG@{{^K3gJ)t=ty3??t^(O`+un566fP=GN@D;><29ic;0u@Y-m(#SLYy>=>GQ;mGaan88m=}Pke^K*|&|6*#eRlSMU9AP}6>P(sx1!OEH-bspCl+kC7Bz!?q5|!-R(g`(f9+RtS%fm7=QP3$GXc0pA+n*gZqwtp>nGv27!ORl}C5*>Gb30l*{vMwQ@i;rj>(frSJJ0H -o*%q9>p@dqE7aIeV#UkW?kL@m#;)TX_EK8G{eLov|?>`b7YnW}Jv_GG))`Itl>~ga}s8?HLFIo -+v7qwxL4wFApWbS6_c}%XJ=<+#kB^#SctJzvB}I*H5$DZprxA5gVzjfbptBN3>b}Rgn$LKa3Pjp{j!$ -k_I3=cEUlF}Fb>qyMsOnl4q-a$Ud?#3qUAF(M#Jql1~jQsa6ENjuB7p|1#mja5T7gmz1B7piPG{`Dlr;NUo$jB&i@RV(r+_;y=KkQEv~_&$27$=pk|u~r)6v9 -Jbcz5|a79%HSl1^O@I_xxLjdCMiZJLCeqPb0xp0f!@JD63!%c#@A15;SJqS*~TZaKvW#Du8XN*`|fnv -M#F@Smjj}fby=50V3D}{{$$o=8dfOOaZ*c3J7BXc)W-}LjIC)&)M}&mcWhF=%9HmR12(Ed<8OIa|2$w -W_J(?1T3pN2_zV9?FND#=)E?LQ~N;SdvU!n=#ximRQ#-DJ-!}2)bpR<$4{dXduZ)+YENWxf>Xoe3bue -8PuP?kV1ah5$KVCJy>b~-Jd2<<0^)kjMt^>&*+cy;(cKx)G~U_66yVy!hH)8+|zkJNCf1sJ5c+-$8{i^a31QB6;+-F^iY?ucD*!> -8`?_X5sZ%+aE=^y6BjpB#j9$|3PnwpCi9zXvCHN5F7RuhND$WE8|qh?AjXQv?u%8M=h8EV?2Q43c66L -quf+L(4)OGm1Z`cYD>c9i(#oHXC1%t)A@IBjE?VA;r)GKqTa!OGw;45jajhCQEmqRZ~#O<;yQnuc_zH -!EvY90n516g1tt1HTwrzKhcTocK|eb%^qh5>x)&JmN`6jWlvUebo9uw_SrIT#74ar#|`;4t+D7RZ|SZr(f(|(es#z7svuc!A}y8lyxxr$V4F(&s~TG7`j$ApkaFEV%tL1UqOLn$db -^ixOOp`#20E@`G6|u%_dQPPF?xN$!s6&a{k3as%&Y51T7S4e7ZiHDYbXri|0PGD?z7 -jPSiht=gVkETZ1_UP=Kpb{@sfFQzRwrpxnnQGFT2|mjDML=85~(2tCJrnB_i6`{+wQX`C=1Aexc$9LO -Ldri+T;}r#XjA_T!>cJs>@58&?$VbqOQ4qs^EET4s&Ikm+lT4HU(!W<$FNk+7ub!%wvhK%Wf1n4eUpbnxUGltc=LH@Og)}JF_e3+OaVLn`wRQu*8xv-5Y` -?pD0{nVK^u@@TG>C!rj8Ww&+@>+3dKX~pPd5EJ;ltV)5$Avq~Euo@1hx0$KP{p3*kGv0Gm-(q#W}u3; -4HPf%udFo?E{_zk}Cobze!|DPy3%Q`-3p@FBe0?SQ#6I|}P>S1poCA -i8zLO5ynuSH++?_zjTj=%MWx|sKJA$-8JtrEfnXIk3LWz>qvch2w>C|aSjkWY?Cl)qaI`&25G4RpVu -`dqt1dYs}IC1Sb8v=O?s*M^ar9ThJFybreta@_~Mxl3)mrcUt;9A>Xnk%pU$$08q?THU?T2v^_F^|FEF!t1qy1+Av+9Ig_-M(1e;H}h>E+R_N0qQO*GJ)K#0qoiK -tbH3tW8jyH*$f_}HPEE6-;X_n_qcD)l52o#cM3QCMrtWJq2rZAqlJy}k?Kzdy!X`-rkb)NFO8SGeOu(P_2|7G!_TF3nFp22U)68;I{4t@H>a^U#KYbn@;U;QGz$V&U^zDfWB6-1OUF} -ik7;QKRzOuBh -=>leYK(4nl+TFgsyGQh51cX$n&&hpWG`q>&FFMC>q0ftjtu4lIsuxO757?uMmH -+I6578WvGeV&<>hk847vRrP>bE)j_H@k`lgyi+N8Yi~`Y`3kdX_u3JP&Ehr3R&D~%_9ne`6sg)0rAIn -qLw`;g8g6qJ@%ASEU>=4mQ=tX9S$|XWS}A{_X4G3zhZ-I`Gb-BsY|z*)K3x6z{@0*w78ONNlet@(kob -e`3ml-*%_Y#|YwII>@4s#DDp_7G`UU<;ut@JA%h`mB{F6}-&s=vRDC@drg}aybpr;+gQ@D1323$Y2J( -ZB_scUJc_ne>^uGcNK9a)OhqMNH291?oPvv^sN-U=6Cb7$-XL_y$Y5)d70NB*dSE=%uAuw!p;U4pyFd -cVUoH*Av}J?A=TV9<&Jg84XzdDDEqV{%B@!@ih1-umFCc-|utQEG!xbfZQU5PBIm77kqI+p&tI2@wF& -c|d+%B07dQkj9rbk6*TRNu`b-wR6W19Fe~-?8S{2CS(u`*G$Jr@0duSb2ozb3ELz=V7(A@2pJO$aD -D6Rmi4YvMPY#<2I6jzfTnCZ=4?H -tnk6!FlE!j{gRAIe~twY88ZCr=hC0Yr@#@G2X=l4U0kkVDh(dKI=X@nfS^Jpbc`2U<(`U -wEg4)!r*N;4_YUhB6*m$w$y0;c;Ed%P80#O~-UMnc>sP&`bRnly@*Tl}{x4i5wbkz@znx-t+^Bzt~XOW}&Qv(>X9H{9blY%4}r%(D3JiM3=KsBO-eK?p4I@z -K0*4nSL7hJ~8DZv3&6*e+q;fePS20uTrLRT(9|H`GD8eO-oA#wK@;={PZO#SwoBVOr~cpndnns7^O{h{&WNSYBi&m_g-S-rU8E8lezA85cm4M#5VJ;9e-^)OI+ -o3(8&HT360G~ns=z)t|HqTDv-2IHokpV -e$sIrDYm8QR8BH9h{guwj-$SJ`6Xh`hMU8Ur53t$U}EqVv&Crq5aMdKyoBjp6xLot|Q^}7f8nSasq2I -Y=>(dS=<_{q)J -yQp#6#oNIO9KQH000080Q-4XQ&0q_vHu4E0No-004M+e0B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZWo2S@ -X>4R=a&s?aZ*4AcdCeMKZ`(HT-M@lW5JX-Tq07sFU@4F$=~iq>iq;)Aq=vyrbi!4ZJdsM`VLyIHij+u --l%2e60vgxkeev#iU-Gi9R%o$UHoU3HVu9#tUDX`nTUHefCyVxbf^zym8Li3$H6_ieoGc2;NsS9OnM` -=SIi4VRmV#PV6$QyS0J9X|W}4(>!|`oFF7bLz%ex7A%E+3d4|4OOuB-YOp*{3*!|$sS9i=~b2?mHsu% -}g#=J2UNRH9=P(?L;j;68x%Mh+68_GGNDX$(i2h7BobOSA?x`Mmms1!)ej&ud2K$$ -a|des@|v%&{j^C=#KF7YW1$6MNaA%{tJ32$cmI4i*rs}b;3G2L4)0i+{_Bh7&_&={*^Kw+Zs^>#3R@( -7N88iSj3DJ%LI{@m;odnv6reXV5|N&aS4Id6(I+|Cg4LQ5&*GDE5#rqy#;S#={K@r@pS4EWrPFTsuNI -XHbD=#Xo$mvE)n>lG(<+?7LGTd9S2SdgefCne%`5%>IY9t*t;6srES5fi^Z_HuB$bv`6e##>Ndv`_ -ZZz{CJt!%U4o(x@yn+V(G=k_A1KOtERC7SsH#CrC2{-~;xQ@nvwKjTHo2`iDZS9g8rSXN1X%F!sYy`c -X+1mlx@4e(8F}K0ZH_rk)bcyBtZH&1#kD2jfUry46NH5oV?lO0@Lz+=(D5wdCTJ|U+cNg2Gb#8TI;bW -=)~g=Hwzo?HS9^IiG*DP%(*zy1Uuut)EtzVvsvgJ|R{`ysEEPS$6rI?1+oB+oTe+jm!eLyQ+f7NQR#^ -uGCyyWHx+DD~fDu~`xR(-H909`qQ0h$Vx<0w2HEWsfeo9Gbn4&vby}si?k_@s=W)SUWZfgZnRaY2{CB -m+HGi7!>l?`Ym$jIgc@96i2ssxWUYb&?~Z7%kt^dYjMzjJOfa0KAZn>pZuLviUwVR-TU)!>3IDal190 -0qaI!fO;ANAOpE|9(ESrhfb(VvL6(8N{*t%CySW=T^LiD680>-0qn8kJn*n5PT -P%XbsY}a($HY>o$GDC#j-z+LD;d|uw&|wm{78D_)f>NjCBgDso%;2^PYt4io`v}ryORTRkC=2+o;@x+ -KH_<&`712EDNj$T@>}Z)Jthz@E?wF}N(A2ErW;L;^IAdWd`|tIpppZIp%0xGL(v%G&`QZbusrYWGEms -x6nIP@cn-n+KJ!~p^-s?-dm5ryWp3vn*EcBY7{_oUhTh+#=aN?&;%i5RTfhgasb$;j7H&VKVS9|s4){ -U$MBRc@Bo7;kBAMzeZuQ-5g!ys~+fO;h|l7p15T2WBKs8$>Vc?>{MsE*EN;*9Mr*1{#7Jf7604QJ&!^ -DZie(?MM)2NEZGwBl(gT12~I)TtXVY$K`5+53-f{5zNTRj4YRSQk;0?TQIm;---9zvZfyvSjK+Q#!q9 -W%6|M{+l{VMI{Dxds~v*;-D<~oKRY=Ts_IEd$*X{OLf-LX-js8$FjzEqnCL*d2R3aiqxQm->DVqi0;_ -uK<43}352JD{;gyRwVvt-4u+qs9MGwdv?a$3{2FjO9fWb+_?r&U(m|Iy(%7|SxbMPin5EM*amSzG7>Ayk1`?@54g -!_wAk{%G;Uwdb?QjN(n}zCM+274BexEI_uFo%IJxhB}NCyIJsn@(XV6J{f=7k6szF{17rDFR8LrUH&= -v;QY|2HlVwKAD^e62xM^6i^S&wU==E$atQGsZME9oxHZotC+{GjE0&#vDe?WbQp_4S#-p -_T}>8YPOjD@$o|HZ)jf=AC4I8q`DSf*BH>9b>+KC-(l}DDia@}C<=Q>U#ht5wqVu|+J~Yj`e&d0_zLt -PG=cURiuPF~|Gb4ww7)~|6Ca=h^qn$8VkymA%Dkj0GDLO+ehWg5iQr)uW)))NVKQ~&P2JV-)$E`b~*XU?!zY=% -)as_sRj;wVdUOPJn!Me#>HwE*(9&idJ39QSco@uzCdShrxwK>t(GD`ofXc^ZJYFEDn7G{>ixXB)xLA{ -SN-rxeQ2nHAAmFS}oR==WnHEDk`aVfunVSPWYh^?PhOeg;WP)h>@6aWAK2mt$eR#Q!_(1%hA001O100 -1fg003}la4%nWWo~3|axZ9fZEQ7cX<{#Qa%E*=b!lv5WpZ;bWpr|7WiD`e?Hk)}+&1=ozJiruM3c(eG -><{YK;zg816#*P9bgN`6=;bwqpd~~M9##s-TeC=-XxNuG#V$}m#vW2vUKh@9v;ferrMw+S#H_3AxVPr -OCreY*zBs;@tN+_MpmYhq -S*Kx8VWmYvY&7U+YMxI#0_BEN!)RTrNjI^rv8jRno$FY9)h$&Hz+Gk0=#z1mYxG&PGqzspg>KE+;4N> -9^p*04PHAu2$dC|V#RN1y5S2$%=vsXW6Glo}5LssOeMhg%f$ZqPqAYt%Fc>mj%hnMp&Vf=C)%xAM%Mw -Vzr*yUYA$lGmRWTXi%3$h_4?zDERZm6_}aK-^>tP20ZW^#CBmXs*Sh>JKJcndJ_hiX -2CoLb!GxNEik1TGUTR?Z@}+L41RHSk|Zy4mLwr1#WE5mj5ss6Al7`@E-$j1px&j?mcis -O6O>#WtK47;CFZx3h^vD&%0Wg=5vc$zU*G1oFcCNn;t(Q7l*ssg3C-5>3y(fn<99K3`1D>1R`MqMaPV -Ta2@3+VK;zdDE5p{S0Nlr(fT3V(TH5H&ilr0<#8lYnix^gzEtLggq8$)$ARysv>$5*i!P2|%O>8jKgw0dRh)j4{g -y;N8E5s?C4?@G$qH`U{go6bK9;D^J~Xzz2X7ns$-W>FY*p7dw#?OP-j$Xlwn!nC+H@e!MzllwTZb87b -@#y?^6dT*j6k?swe;fYSw+f#nf=3?>nRg$l*c-(q5!uTv#*RCCO?#`K~pneIFsWrHC2Agu>UKzP{kv? -zfNN0j_w8AhNG?}!21XZgpfDnh4N0yM`eNNK;BJM!VWhUQxuKviwKpYxeKmRO+x9VyucSSNr5FJ85+2 -FJF=lm2;2z@@~HF9Own*JEWUMd3T=+)q2-8Z$wIYXls?_1JA^Bey#n$Hizz8F$sBtd#2j3mhbtd`L#1!)0U!OM?twd -yw3d@>^;!giM#bsJ9A`#Sph8c)xjOucsidHhuMKJ*ds@6S|XI)zx4!3!0Nx*C(ux8k{{!s%CWSDE`l` -=CTt8oL)67JWxdNLCdb9hV6aG63ukk)C%1C=^M~)?X;ccpTPqO6<1NrTC!R%;A(CU8#Z3_-Kk}faCNN -^pUDExP^vK5*1^?X3M90h*cmy<9iU@6Xwt$R{g!o3Hsdd7O3&A5@W+Et%DHne?6Od2TUjLO@(KBoqkK -g-{X6|?`&PT>TcKlPV^<3f#vmgRl3!)%xSKdHLKn}7I}J|LG{{G*b-i6j;xou9@*HzkKes}e{uKg?d5 -qe)}1Nk24-xEIoDI&_JOoqyt7odeV^Sb^vQk$oG{Q#1JEb%40J52epf=}P`m4~Vqx>94sS`fV{e+pbp -LJZD^obne)Sh+r{k=ltvh55i|RMFhQZslhT(t98d{vl63&|xQxalg*hA2N{Q2_cF8T2O{axVYcGck3p -T+g+!%u(bH<~ji$OvV>kS51P{%_K>+xU)hv-4CssxkDzfWnJJyIHcuEB|%}_T{|m=)ePpSAlQO)0K83 -%$f>l^jafp#<{SHi?M6Ux9AG+v-0bvpAP72j(J4g%pPupmox>Y+I@tc -oD>MtOx19I46vJJ(A(bi9xfthJoE)-Zhv|CJ&Xqn#gWE}_k|*%)fR1a+47&e0rJn-+pU~h1dVyY|PzE -^q9=)DN7HN;IEVZD8JS6H2kKdi0@d!UVJ2%`Ry)o(tf51Y`IFi@L_YqowIZkPW) -C61X^9-tHyufe$eGuJXmah2@u{{8f#UqQ#%;$5BJ2;vV4CFxEAa+D{mo(A@6tj5MRNI<{{@HxpkYz3= -7y6@sSU!TyT}dCt1caQ^Hk2s7lSF}(r9Jdvgz3^Amh3gZN)nUWU!iY@{n0FNp`lwzz=?JNkg;NHsP>Hx7{yauXCo(vc$4W` -2+vT%@u2b_&E2iRq3{nne6UN#>*b>;s#TNaC2sbIjvWs -jvbYlK43HH!<{!HLb5n@##;orDs9fXBd2fW&T5}IMzc5y}!Nm7SA-bt{Ha3VjwQ(Kz7{@kAzb^1?6({ -udy^EdRe|BKIy8X?kncE;9(Y9DUi7~kxT95k;iQA6IiK6*Qv^Whi9vs!2=;_LZAiHQsl_mh+>$>lGuD -38sdJS{wq+lw`gMO{5Mc2%zsi@0BPfJ_C-1=mD^mA^j`*Fsga&qeLfRxn-2B$Vze(a*h$exKUM2Y?)- -4h|ktX;dH9ehuyg!)(#b-pdN;;7UP4DbTz!>BGkuZdSxS -w9ei$1CM_`S@8YwY^J}Z{aO4W1H_x}FB(GUFBImp{{m1;0|XQR000O8`*~JVu^G3384dsdt~mezF8}} -laA|NaUv_0~WN&gWXmo9CHEd~OFLZKcWny({Y-D9}b1!9da%E*-YVR*L_!v0ic|^GJ~hpM?=BuB0D_e4xt24@$;4t`*j?-{z#s_Tm1&bRvM -MXWo9(u&>a4gU^_r3V^Cpj2T`~BXEh}2>B61Dyvy@S?W)=M6q%KLr8KInPnq)mGS7K^G>+GSOhNIh(^ -K6;3IeE?+YM+;wBUY-c}{ -d-S8hKlWX?5CZN8g+#tgo#YN08lM|(oss#F4qVqD@Z9yi~1Sv{_5>6(+u0bYR?9>#X0b&RgNzznvl!I --unox31Yx)+r|CLq+Kum%lm`qj`=q!#`P2E&1j!Cw`wIp=O%e<*sEdQR9G<(Q+Ru*&86j@THEY7oERaM!6{t<7FYa;mra(Ztm5wU -I0LI=;5)6h3`f$l<)m%4jp7GZq-7QH?6byj_38ULS!M;ePF#bfOhCFO&#w`|f?QV(o7D6!uGk&>yoEO -v0~t44aHBBzJ-qqnAGcr6{s^P5XTfYTQC2As0j!`NoynJBKzSO>BESYTlALl*z&INmy9W5MT>b`XpUI -U-*$NCSE3!I{L(cNmTv+a$Y#T84ep;@nW`E1AYg=WKwKEQHK3=$p#EtCXp8`$fA(12!=}qjto -%^v^L<%3OvT9>mi9gP28Z#R+ewQTj15-n42P8l6fT1XuvR0tE~)4abPr)5oVB1BbOm@;jKN3fpgN+21 -}S{D6#dd99?>){ASQ(hS&Mbn2@(VHvsOx1O%;P6QuLG|r(4Q_2(e{Vy$fxGKxu+xYR2o2tjcJf{hL*x -t*q)nY8QWJ#j512ev{=OD!gu;D7rL9LI~jCFZFo|#=_1iUn@)I3v>%ZK(47?NSd1V97v_`@8Z;uH(L? -hAJ&x9x~@9V030K5^bN3mIJHzREIvo%4fw9uWMJ#ytOrm%MAEFn;3!oV9n5-QKv%CPK%u@O2-U1AXnu -Zu-luEG6}l6ptHoIg+m?5te%y)D1>>g5J(B7tt<#v-xQfA#U!DJacKzwknwMu>3G()A{d4_rX=a{2lAkUOc$=4*5rcAyeFJ^PCj#(cK1_K=fa -0^fz^y86!9@m(XnbQy$eJ?8;h(MTIpuNc26`0XBb{|M`?wlPQF63KYnw;wCtiLe;>`zC)6drpX&I ->_aE%y~FFl%rvG%mtjyCi`s}|IC)YOh}GEEy}yR&wPZ>i-O{T<_$AkK9W;058m_=VEuGP*|S2#>JR;g -nqfyo7Co-D`F65)j)Wi9@i2S%Jk@CRgF3^602>$!%!Y_c9+b6`VY1_SmyP&2Vs)0FO*k}=s`YoR4Iht -*aHyDB8g?iL%%gLcsmP-im>*7uk|~3T$Lh`a{^THS6B^qp6r8(-qY^|BHo7VHHbgN+?}Y!yO-ajpV$0 -iYO~a+ysZzbFM}vP9LG)Xh6=C(zR+yzU8mG2|yuYnGK?F@!NR!oE>8}t~Y+XX)C9z;9O^W0c{zE)Izl -eNNG3SAYfBQ*_e!9|VU|Mh1u*-j$a;`HHU8h(eL;CHC6vGRjow#AvcFc9oX1d*%2v(c^6!Re56Xy{`7W#Ins+qNCX4*M)`Auk81o`IaW^r|Wlbm1ZC_(_Qk#8wxIB#9URN^a~ -aUFBdpNo`Qv9)WD;EbcX6SDn*ar_4So{XxA-h1EKpucJ3shl6tEG0rlOjr!h9X>3MT@4_;63l -O8G0^!lm>gTuhtYTK_F8FQUOgzkroz&ENt-|8B1_3Ht(2VH&oUS>a?#swAYq-j9&njP_PlrlUv#IvI$ -%RIdsr2zr#QvvU*K<`U?Z0;}(XhwasQ$G>V2*ShSNWXa#V#t;V8kG4|mRry{G}^$(WOz(P|I -gQJj&)Fmt1l-osS*jV`>QX7HS`jeH)qMjW>72zu*LhnHxa@}lEmz*Ma+hY^N_Iz6ih+=^8@1aHTakz>d2Fdn~$JKL1)d=!OHAWd -{PcfqEFYb)6BAW}8BK5vo*v;)imT)iFMI1o0>iwg{Mr#}Z0PAV}xx^=k$>Hd|m~=Zl>HIbft`Yg*9+& -$2^=+V6`8u;C3~z!@Gx4=s3?!Qh@Jp~5!jnmg&2_bAr(sG+*fAl3_!Y7u>i2Dj8+!X?A1zRjW4r~cj_ -R>dkjZ3z{r@%2+sZY-6Pyl#b8a{fn5Il&K)+7<$vKVGz)9J==*MhBH-)pEacVYymVLkj|#Q%hYqtpow -%)KyC87UoDoxmA{45?yV*aSs^X($Kd7_R0<8q6OZlt6hclkQz|7RMMA~FkL-myu1W%pagcOiI;JE=gv -A&5Fo04)g@@luyZG6sOV8^J#d4iLx*dt?;$x_fH`2$(*4>|xR@C0P#<{-9q+WQJNo_E;2D -=a`2!SCNdhKscZ(Dk%sKdP*O~T*3=|RC;gvvACbvtHWO1)OfASKaIaa7ozwYO@Lc6TlfM_EQ)-m}8cm -r6v$>ze@o3OcnJ*Vz<+eq#U`SK1VieiPhU{Uj{gkbo_tzIz$rk1)W?g}n=|gl)F+OfG6)0A37%h2G;d#Q0uX}YiO*peN?tSn|rD;RO -pD0Lz)2eILd<9NHHdtHe=X{@Nd+6LfXyAcelQl|Lh -q)Let-1Tf-YGdJ|c)U0I~P3)_6mfp`O(d}!=-GJl-en^CUBFQ*qw9*Z)$cS3e@M^(u)!uCrvVZtlZ6wcT^|g+PJKcXNU9kLn(v -X?&)Kdau2h08#JMN@4xRIrqZ83hdkdo76^tteLpLw2iS15dO!TG3EvMm>T>bL;?d6%g0y!neULxu -V2ZQ%jv&_-~As`k7fqO;8qw<#ffeTwZo~2G0Zl`!>Aby+_tuj24nFcfRazRc-SJN1n22(jOe1pLW4|; -JWbsZ>@vVgP!GA)42nV^!2#y$C0Q$kv}Z&&pSE7*h(ltbh}5$0t2%jxOqi$6^V@2B`=9=R0T+QL*{a1 -aOvYSYpCwVeY@kNGqhL^X1DY@nD1L+!Vm`sr{h9-QL|)&MLdi3Ny6u-2}LQ1@6aWAK2 -mt$eR#V${YK5K#0037O001li003}la4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQgzbYEXC -aCyyG-;dii41V`t!MZrq>5^dBW3Ut`uD4+iYl}7OI$VQ5XDf*sS@JCT(yZwJK2q{;JJ;OyvH*8UB=RF -gex&GxVfYeaEhUew&O2Tm8seNY%YgwQ!;#4Y&z(CA6hDGJWt<%Kc2By%RfM!U5N -RfPO3rn4_1z~tA4KSjM9CFy{o!}ek65dFa#X7c`owI5Vi4w;;kjTEeS8Kf$5SM5+>3Y*mL0gM>@WjHO -MK6Fg}1MgAm_HoF%C?Xal{jN-7;3F??W{Y!Y~YiZLU+;Y_i?$@t -bj9Fa5m#;w(AQ*JCA`)C-wx&?Wlu@9UueHL(=g$vEjF_1$0>Br>;Ac*$Yvm`F%y=r^vs~!Hxa^xM{cs -d^4|j!-b|w^)$|zymd{)PA*Pm&=mhEv5kYwo>;KopyG6z_%6H8l9u+XpT~=aoIOoapnQLUgToaUi ->oap5qVLcDdb<*v0l6mCJl6+rzh<=znsCzd`J}Kk^uRI -en(OL?a0rajdF|Z;Cr#!yPP7X)iLzi{pl3@vD=Ix-PY@ihjUKkY2P2uD*RH<=upLVT_lq!oTZ1%UO)A -apmsc$X~=y~ -7=naKCtX7+fCRI(>=3=yQ*?PjNurxnZkM#nJ$68Df68c99!dezI)l*Ll5RZ -o|FBp1dAuWTWUl4kZjGXe&PR5&qLl5l4r@w?owRKz~?w9n>MjK-P_>*zT_kT$vEAAW}3EG>YvqKX0;x ->F9NYJcFt>g64j_GB2FH5JH^NJfo%#XwtPLo;74%RO89yZx6)Bj)SW?0P2}fJC>mrg -~D?YxPb#FR^iR!pbL27)CbG}XeAWj+fCL@P7$ETsxM3c!VLu#b)WVIz8Lme0SGxVtehdHO$dFIrhHNr -T-Mv2qJ=`mdOP_^zrXh5DTv;2r}AHy>YDwEVB(o-;8ZfCJ{PcefYQB6K|dl^Zkr{avL*qfbdZIujlFV -MTzDa{@|#bMbznawcZFq@54PxpGBoU`>x$NoNhpie&B>$2ibiujjgOlrjM(0Tsq&GqWfw>RW|NSvX6F -G9-USt2&C?ny6D=pN=V8qMJzqh(Ima#GT2#i_MO{KpG*GFBO53jZ>&|KG4SM;nd7x$ZDh0?m0xir^BD -zIm?6C70d@6aWAK2mt -$eR#PD@+Cmox001-{001Ze003}la4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQgzb2T54h~M&@r^$#E2@lhV&0{WDM&sP`R!HOPHXcXts|id2L~V@6VVdNE -CL%YR3Ei@MM?;p6$Zg7_NzAqk_D4jno^EJHL!b_{=W`mzAlEUu^3N_#KfaqxXa0!bc=q}I1c$T9Uvg> -kc4BraFi~;uuu#GX*=KdaXBZH2mm4XoW&7O)u^8 -5h=L?Fwdq^j*-n+f*ng}&z1ztrzNpu$SBVlI7*yvcPh|XG|93wt56wEwFcXK?JDq^C6H)Rg+xIrV~{& -8Hd5*}L~BAIr*gVxHQj)mQcYntfiecEpX33#WErP1|A$Q~9_=ARZz6As$k$^HDpWefZS~u;A8|Gk)#4cQYIN-8e3BV%&u#|kweOXV -e(B{#8EO3D(^zgqAdctjA{E`iBY-U;GomdK(HpA=?H@~O6zL~<@pAf4jgI8hjN#%9P>C>N+Ow;Dv$g> -;Y=yb4S1!}{;�IFd54FBm%`k6dc>|9Rk5+25g0f8J{(zv0LJU^p}y>fumTk|gSMU0=13X#c&-s*Gz -DsMqES3f?P(bPOVGwU{$Pn+cc -}n*#d=)veg%ZsSBj@@;5i04IXNM+Io*hD{9DYI@oirE>U8t+;1CR)5pnWR_=!8K)a -2Hzp0R|aNi$TregNK7y&}E2R2-qe{0WlW=c|N`P@b2Z?^S>waPjAj=lgsxnPbb}S>=DBa>p39<+LratvG&M{I*vCWCM{RxtX;skD{3A)p49d+21vLc>^7@mC&$PLClgPndIaJ!LcG -Wtb7dTEh4lXv{vHnT1W1P?22W~!}I7E4@0=T-{*pD^|V{RGexQM6b@=@hD -&|junXF3fF|l-)N2Qg^Z4V{NC^;T*aBr_1}HOhx*Ea7y=CIwRt90aji@>g1%wN2M(QN9rvX8fTEA~;Q -6x7>dY2eA(umCf(^cgS)V~-M(XbLqUitGD)^=?ec^6jGiCFiIbYJpJVH52%ba>fMZac&9z -xLZ7sdBvfS$!p5@P?^HXa>*fxlJ)nHc~9ZWBQ<+6m994pFsz{sMY((vDfjQd1k8bgE)OrVSQ^}-8Y!z -AGBsaVB;4i&eLu30_2}xC>vHw#*TC&stScm7$fpIjFNCr$E5`9L(Jy*IBZsZ)rm2ZPYlcIY8jCG@_~z -FnwkjWUxQ=t#NtFbf7?lXMoNaK_kBLnZtyf)jWgOLPq~&Ok05(VHPV1VKSrg!1z42c8*k$|IL)rAmix -DVmzx%dp*|bghe$=NyE3KZ-QnpB(-E1yD-^1QY-O00;p4c~(c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bWN&RQaCwzf$!^;)5WVXwc$5G_A5 -g#tLskl-4HY8%q9M)`_O=6++UDh)N{Lbd@I?752;bO4($qW1J0aN- -MqlvBLwXfZqGwQsbOVYqE`15S#HiL5hcSzl))JaWdTIf!R#r*4NuvFIOwug<@epSu*~DXzSpszawN+@ -a28A6Jt-yV@-GQ@wTex3&k_xJA;SEf{7Xny~e5)Xhzo&L%d{z`~*6BKXjH7g$5bFr8OESTDWd25Z2RF -Cv^Nzp9Htl7`R4F0R3jO{wGnb99E+d>XseH?8EbO4?L58nzl74WZwI|ek7O#7??n8_Y-#CLsh?Z>}w# -KrhIqY}aCGnv4r@uhsGXZaM%89lIzqABvI@%gt@&K1`a|bEue;%CgPqN>iE*pVod0lp8Dtb@8kmQNrxh`*d -G2AgBl`Y!)_kyFd1m%F8Q4n}jk4zu%`uSsydIZl%9;krYUQ-`)v;)@r3s6e~1QY-O00;p4c~(=_&&Na -S1ONcX5dZ)w0001RX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bWpr|7WiD`e-B@jJ+cpsXu3y -2bU!+bE>UN*9dO(*rMSvwKG7sBeWCA0dZ6eeQNXm^d^uOPd@={>HHX(pz^6t6k9^R3Rm#WNJk} -PXeD@YP1@~TvZ@mpQW+Cb8Lk60!)LW{D9SY3#;%pj4%fZ|en-jXoVQc6e-n&=kfN@buRI~A$%+>ioiU -WqDBHc(`xinX|JU9?f6SS4m#K?@wWbGKZB8;XBih<*_%qt-w -@c#-He}27r6MhZiH=!STo|nOrt-#FY3gD+&NaP*NB;-&S8amIb<_o9%!efu&)0;>9QR{u%lQOrjnBlH -l>}CR=>&y>hr0-ehRC28unf%7(9Wo;!;>)tQhO|~%Y~W!oJZ9`QtF}@U<~S3Y-%6O)MXez_+HfW4y9K -4k_x(@kT`;o-R&ixST1&2WGE-0~&}}jinhGm>FT>wxQgg&`H`CftS_t#zrfPbKI5918b -LJsBFx$nSYiQ{U(m0EOUuGwkYleKnZ#_4rU -?776Pw&W+7b$40qt;zN7=BNNOUYNvH*jzK2tT=yK0|9y12Tx3wP|YU9NSB**4TbvDX6k -EZH4UM&;t^;ip2g|fu=U#ff#M6dW!(*&U4Q&UFGCWz^qaAp -6m#Q|5nog|_oQKF@yipT;D+$ob=nM1#V$X;Z#|U)8r#r02Lq-YN>WCHzL1+&fG%{UB+r#NVWb7G@2EAi{fWs2EG>R6n4VN|T)l3R -)8Vv|S+{Spb$dUiZW-u-z!X5^s=W!W3@{6D+AnvVEU(n!P7~Wu5OM4J!T;5&;Qa7>+Jc72NPmc2>|wPppwN8b$+2BQ`CYa~o*@2NUzE!V7d~{0tt8wwJ=9$_LwaLhQt91tZC7CXo|oPXoA)T=62GlPc$Fk+DQ@%_J3SvO$QFmvM4nc~;0+@neId*-2(FX~~#<9mn{*uDAdN6`PfIAG%E>+p=R>W5mtMnyO~=xP6) -M9R<_&VltUzY(rSC@c2s0nTl>E1U`I2z(hqJi=5dv_>l~F%FRpjjtP(hVsfQvp0b&mcSnu}YO2;zLqr -v43~8NBsY*GY&dC(40Zyh7*q92c6!}mTdRDCe0s?Dnv^`09&UKQ^6cd{{5a#TQuVN0c=mnZ)JbW!Pbw**C~5)OF=eGDzkxIO8q#YaWg&e6OYrm=H{=i_#*6V55@o6uYJZi_GA -C}tKAvXtz=i%QKR4F0^(y##PThAIKfag<<`ph|RZvmCMuHu7T_Qx0U>tn3yj7tMl`Fo46+J5A1fW~V^ -1y7oPT$f|>A3_+QpW`8@u@_MVG>~)*nO@drXa2xkP$DCmF&q*7(b5chg1aYqhv`WB6`|OJ#7!nQt{Dl -@DR@-M^q}>-p+UI}er1K2>XX?Cky=v)vkaUY>&lFAkGtOwE-Y3m1jSbvKiIFN@&4_FYX&(vCTeMa1`2 -Y08RdFl6ZqRgtE~3qe;7W&btSKRPT?JK48zsNH*F&jStbM7GEF9S(h#o*a!(bfZ%`bx+E@1oxV2Xi2jv)=j)qp -b^su9$HLAAW6)ng+Ld>b?6sGm1pFonv$0Y5lV1F;BxV_oV%5@DQ~T9ku$3G?@Ct4Y0bSYY?$A_yZiI) -;&C~@h*S?yVXy{k`RV<_@*msj$viqEl=&W*VZwX%I&nlO5Qp<_(~+zg@V=d%F>FAjv7)OPtpVmefRWo -JF~ZyW9<{6!9x`!o&@&~KKXM$n(HZ?3La7{hp(Ey)i*PtGJZMEZ`KOE-jXwPz -X81z9uT6hbrOIh7|;UuHeMJ=N(BvY`7BAU3aN1DohJ@+$-!*@zA=u^1hyq$#E%@?quTtt_fi_3F& -}z8U}-)UH)N!r%MeG{_?;h0di&%2X`}f-Ma3}Xu`*jcYOaGZwqLM23ftMU!^|XoIg9NHnpZ=~QHO+q$ -DPm~|9R%nKf8ts=6?SvidL|lv1Wh~c8-ILzNzQ1Fp5`?7QZiSIynL2G=&&{X?og=M_8SqE5pAgn62lg -)0FMc)d|@P|Il8z~P!q}248GP!w(A(Ca951zDM< -@pKvpRDAM~;~>`qO= -r8%&qw{JH16!87_WMWAJng-@x8fZU|Reig3;l#*SoDBHdKf;O+RDwO!GqSshE;d7AaFe!ge@q;1Plo{ -e^pFz`6BxFPi)fP)h>@6aWAK2mt$eR#OvSZ4k%;000FE001fg003}la4%nWWo~3|axZ9fZEQ7cX<{#Q -a%E+AVQgz92cuor^wb`t-=En7Z;(siG(_DAQ)0#yj{@rYRO+9})0CLBMEuLWsG7J>d&gZKhgE{Up>5w8E4BzQBXfrGYtY{qRtOzN0Rn)eJUg*H~FdB@7h{D6hUBx+;*`P-{o#^69@YmE9Oa%9|{P#u{V17}_x_z@xWg5CvLn; -(O$r8K>F2_1SUD9wOAN3f}@4zz)KDSfX_bMIK+T|U}eUfu(DEvky`==*oTImZL@y?qY(F7U|FFYBkt2 -HA`Z5;$oMTtxcbeY}T@IbCyZ@6aWAK2mt$eR#Sy|x~Loh002<~000 -~S003}la4%nWWo~3|axZCQZecH9UukY>bYEXCaCvo+!A^uQ5QgtOMYAVOHlB<(54w8Ti!Wfzq?7?h%9 -NC{(YLpu6xv~6LagDIPtxQi&aw@Xb#0|XQR000O8`*~JVWffy_D+2%keGLEr82|tPaA|NaUv_0~WN&gWX=H9;FJo_HWn(UIdF@wAZ__{!zW -Y}U>A|+@S}8~zDj5md9tsjjK*gb{qD?$*7H!tn?ph@v{yVde#Ez2`^@4c$U?;oZJocN}&+AInOUUQ7L -g34$Rt8Yc>k>04(Lb4BGZY!L;dyoO_T{BgwTgm)h0XQ)pTelJKFzA(@^0<)W7`Pw^{z3zmP|y^w3XZ% -PRrWpDMc^HlJZzKTwoI4Oxp4IDNfpF^q90&HAZ`XetH|HQ8X7!YdE)Y6CXWyf6}uk0=i19!ZH$#qN24 -h!!kgdwJu_96rY=z&=9U8n=YO~LQ@&gErpX8KIxm;%An4GOLM!y^C~!lCk3qib?)q?7}wa5mBiOlw~Z -wOOK%JdCQD&SnvA}EpN!(Xs@0O2#Jf(@s2@+(#w}wI1x>3Y%toUO#vMKk2M(-Rnt?#+e|9AK8b6k#z{ -oaDj=A5Oq&VKkQJ`R#Bj03Ka;|WR(lBx9*i`F|dqw?-3d>zY;LH*{ojKI>U^iw^aoP~+S;sHGle8TV_ -htsOx)y&F^@H|wN}_4Y4^<%7jo>C!V2w7Es!hX!$R>{aVZE#EpdlMSb#rohyFIzvYn;R4J8LE;w@Q9?Z?g$I`Zz#4nauUlb9Z#uV{f3-3^-V9K -=L$X}%jpV)LtZ7h|iGNWf?w+Q@idgTvdAgX#1)N6vM(u9&?xNmycwJLE_uMSt`k3AhmxD!3soni@^Us -7#{Adct+k_n2ZvEJOgLEhyU9`*@?NN*i{F!0|#2&>sD9!(dVkAzj)Bl?BcWXxbOElv#!ti(@NQO#4k% -Ja|Fbdi7lj4R{z#UPua9qy&Q(^lzWHK;)kE}dk>Zi6%t$^GiP2W)ysZ|nYH-g|Kpnqb%l2sib=XCaFh -R@6aWAK2mt$eR#Ppb#ofy -Q003wK000^Q003}la4%nWWo~3|axZCQZecHDZ*6d4bS`jtUC%MA!Y~jA@V%en;0@M?io8i2grW{zT+~ -V45ZkK+lDlv@hkkq0XhE7S|Kxsut`qjKYFH4g4=f75Mfb^CY$l=h!O~+4E9w_;CCgM4Ep~9>>b$S((w -RHD`L=*`euf#`LK#)&u-w7DSB&{dP@h78G&FsNMkuLY>4eIaw+t<^XGBc@pZQetji(i+I2n&YFqCoXr -hT$;V!}6KY{Ycc+6RXoNwGbOu#~g;(2F(I -(+HSj-ri(T$7UvoUEz^z~S<;bG9#`ytzZsH}_>lbQZgD*Wu_KC{_Y7x-T<~;71|(W#DbGnXPw1THw4< -MAkziq7J{3t6rgLUa;HC-6CCd`b$0SOVeey2L;g`m0${RVN8V}XXHs|N@F@>@igOX5#7VkEVxhl{h%||giYsZE~cEpE6xT0B8y_y4aT2~;qZ`sE{@>mb -Fo+q8GerrCPA;$QSjfqxP11K^OSc#Dp>NNciJKF+1uMAR9-G@x_L>4J0G>+xEK1*N)vd>veb2cX# -PiHY(30x^5WPqb%;(8v=I6jzmV(%~!Ai+I62N162T+0CBEg4HG8mSCNF+!hwncjmYG(IaVb8CbjRm=s(7joc_n+&da4zF@98J(qRYy$H9u$%HZ6 -1-mpgnVt>0eItz -;!3S_X(v+^En=1|^XsmBa3RY$HI@`g=}c_#fI4BdUP^u;qoZ1Znuu347R;pTiWkhH0>Gm2nPlt}kara -0Fhs14MD0t8)|1vfJUPZe6EI;w8#z2V>g_~!c+J9_DaM?VS~U{q{7kNEv@M^+M+Ac$;YAD)UtFg}3}G -aRAIOXmnq@HtE1+1(hiJeSxaf-XmiMOJq7 ->5(TC`V|VFyGh2&8^Cuupy*h$oLIY1-XmzYZApVTY^>8^0Nbaprc`10tFRA9?@9W -AB;C=pk7K2gj=--gzRO?AL?BY{^Sm3%A;n;VrIscm{Cb~z}{<)bUjn&S$1LJ@pAo;K}|MIZ?wk)J`%5ww(mY^l4DcK;SWD% -~H4({1aDo)Vb;^;q65M91H2d~jdJ(#`xZ#&u{;Q&EhYFJb{&MSB#=Yd@n>9YAtnFxS98x#7n>Z+6)X4 -6}&Rl(7)JX9iv)stT3a`hA_i|-GI!|>Ef8DKWlD#KGZba+L&g@E+Q!KBwipe^m8h5+eipZG~{ynPTOW -Q3BF1#lOz^$+%m`g6+lFR8+)_pholHpj(a$#R4GXyQarVAh7Cgc)1~J)a3wRg^fv(S?A)R)V+)AnTX^ -y$E^@QCD*B#ajH#IHNa~qRd}imX5p0^fAb@E?580o!hr8J)!X4pUQ1lu6T65uG5JCHr&u|4D4s14DH`5kVt -_{ii&;Zr*%l$3UTg3g{A)=Q8uU;g?%RJt7`thG`n<|4 -zp}$7e+P4>+TumN=>Nk=Z)+E~PaC{+@Z~DQMw6~qRp@kbQ)AFg>Wx<|wM(b&EymEa{{;1T=kC -~%K65*LsfpMt&53q@cXA{7jIq!6UTWxpOuFAtw_$_U0%KXZZf|3k@0klczGjQvB@B!*2Bd9jbrlsl -G|e>3S`1&~P=flahCph1;!aCd#44p&(s2B<@I!5nbw|rLr=mYvQkej%azHI^>ftt3X{gXpIy-|*CJ)Wm2iPL21r!WSnj^SeEm4l?Zcu?a}N7)92n|cD5DQGq;@^S|#_ZX0)ALvcF -*Rv~e*2lZ;Q|wC_k1ZbkR@@en-7HmeFSpQ1V=C4d)5gubJY&{G@3^0Pd&4f)xrH&TQCyRy8AaO~f;tU -aHJ%3O?YwJu+w?2hEECuz>)LkU<~U7&$C%i-%OD7%;zQMaR+790|(YSH3#+ED_(oU6&t5^DqxiPrOWN@?Y(DxK%j2-R967X`#$=j01ZRPCGkJVC_Y|&I3D43t -B?tB)#aeP0i<{Bx;`C;Tda5 -#n=FXRcZb7lpLOnq=oohC3z=rpnj%!fYYr2LVT)$K#lpYpx2XiwFL#-HZMd;2+%ZkCcxGmL)L~@%(gg -eHXR6Dx&$iAm7}Io%>-VNa5$-PM9U=QUh&;6=>~;;!Zv?nqG--~5mYx6Oxx^jv-YT7TN}C@G8u@36p! -1}#E-VF$7j--?9>|5QwIzpl9EOgCLwRPVzrUntr*opvIMl29fNS6D-9UW?A|mqR?5_K&hAyX^!C5{-B -wAfhv6G@Y7dT1Yb$S3i7htHbZV<3LeI`e)}HJr8J1bEP6X5~uPDq_$lO;hQ9c+!R*!6svg@)OGyHm?x -Xw)NUWV9^_h2mXcOwl)vSZIRcN1aqXmi)o>ArEL?aaEmCW;t`#*-445~P!SDC$HOM^NJo8aLl+o_m#8 -FB26vRZb$Rb4KY#UJcRV-OlO~OS;!iW-c`?Dz10nIY -VTDoG3D$?cAye#2dNHi~pnP_H#o-gF3?m-~We!6OsCHbZ+m7?)FWg~?zr2O3it-P12-BO$JZf^P4T+l -5&{x`0t7}?q9)RxoYckBJB#R)xU{k#3H28Q1C{k*RDk-aXk&EIpo{{m1;0|XQR000O8`*~JVb{HBmZz -BKzZlwSK8~^|SaA|NaUv_0~WN&gWX=H9;FKJ|MVPs)+VJ>iam3>RkB{!1YUcaIMFXRF2aq>+L;Duoue -!}BLV=y}*_{es7WT_#^_B4$C?`B?}`&FGIX)y&92nO%RJQ0ix2AL7$Kl}8D|MB_fPxt5V*QZ_D|NiC2 -|7E{?9`^nBfBy8x^~3tfep&NV|MRC`e*E$Kpa0e8`Gft-X}{Pv-+cGo|5-o%{Q1WpzWeUezy9>C3x9q -6_4VyHKaZb2|LK>XH|hU<^J)M4_CNfz)<1mvH=q9btNQ%A{xOv6^)Hn8FNmB#u1q$N7nAQpt{0JO1UW -G|gIt(gLGDZ*AWtT*LpD!j3x~XzeDGZx&(~x({R_%2JJZuV^z50Q@%t>l@Ash(?jMhJx64d-74*e)kK -af6eZ=ph{669LsS3S;ZbI*%51~)cwg1D{U4&lo+@6@;k9_VS^a1*eOrLMM;F-X4i#(5jb}9Ye=;yw_r -h9kBcbBAU*7HknyJR_E?xnH2thY268qA3XUC^NSkp0yuzWtB(gS=~g9kKbdB) -g-%Fs8--LJ?D!)pb@aoNv#TH@{And*zOBjLdIjelxR>nT5h*RCtUE>tx|~p|DPt%vILO!ed -kzb79Pd$Effa6&|C)3>Idv@;g|0j4F>&zk`j(xA -FKk9^b|`r}6kU9^b~}+nC?R{5Brn#^c*~d>fB%d9XeYet!qQzk~T5{QeH+ckuf=d3-1HJNf;c%>k9jIB~GsLk0T2wv2YR#C$Vr63n#H~5(_7>a1skARj#r!BoFu&|TmJ+$j{1)c7Fu#SBzwr3716kOCE -bKrQb|4EokcAz{GPvi#J+rAVgL`IEUk3NgroOPLFOwxXS#u_9&SXhWmgHnfPS%{snlqW-$^1^{cQU_| -`JK$~VtyC%yO`g_{4VBqF~5uXUCi%dei!q*nBUht__3y|k>;|>OT5ZUyvj?w%1gY;OS~F0_guJI`Nxf -0jaxfc2UjOo7guSFDqHW$*1NLxu57()qPM%U^{$EeP0Vkix4W`Yt(kUCDjU_xMzyk0t!z~5V^qKHiea -!(t(nE1X%eZt^r*b_sJ!&3y!5EN^r*b_sJ!&3y!5EN^r*b_s3ssgR|i+gMXa{Cm34!c9<`I@uibN%`Q -@cY<+VoTwMON&M(sQbyw<3^)~LMJsJzywyw<3^)~JIS -tv&Leev(vKizdFbuaZG^(gfu^(=M4$NP`F-lT3)?@}L9pHg2^zquZS{~-Ja;XerfLHG~Ce-QqI@E?T# -Ap8g6KM4Op_z%K=5dMSkABF!Y{72zG3ja~~kHUWx{-f|8h5so0N8vvT|55ml!haP0qwt@E|0Mh;;Xeu -gN%&8~e-i$a@SlYLB>X4gKMDUy_)o%r68@9$pN0P{{Ab}m3;$X8&%%Ee{5!|5^CY!ha -V2v+!Sp|04Vs;lBv~Mffkme-ZwR@Lz=gda-@3PU>R+Ymj=Bdir|5W!JOR1y5BxG9FdNqsn+x8ILOCQD -r=;j7O94Xfhs6#-qu2G#QU3({Q&ul);uy|#nCYG)sM*NfDv)Z5o}lljSHelnS#Oy(z(`N?E{GMS%D<|mW+$z*;qnV(GN -CzJWfWPUQ4pG@W_lljSHelnS#Oy(z(`N?E{GMS%D<|mW+$z*;qnV(GNCzJWfWPUQ4pG@W_lljSHelnS -#Oy(z(`N?E{GMS%D<|mW+$z*;qnV(GNCzJWfWPUQ4pG@W_lljSHezMp6RPtAKo)0eLg~~&4O7KbuN{C -9Jt@nqJ^|3{NQS}#1e_=}K`X@sP^K0%O!qPr&AjLRZ32!c -^!>e{l<8seUY#hb%caL3M0J{S!sH6zQ5uP^3#nu2(_YtJ7b2{YB7UL?t98WbLCUp(>$iAEv+PN*MYlQ --87a7b=gY@@Og#Q($IF=t@vvroe0|;bwK1PYl)9x;`&4IrH*L2ug@bNJ_{`C`zbGXi6|8bR`TWOeHKO -$SXG8r396SQ+YU*hf{etm4{P#IF*M}c{r7aQ+YU*hf{etm4{P#IF*N2d3cqF7ZW^X+v&Px-)0k>61)< -`7~HaVvk6HFSqVi6RS8WAri8A9p@gY~r38`b9aSDt5kf>%ONLR3OhLRLaiLRCUjf+?XZVJKlLVJYDjf>U`om4{P#IF*M}c -{r7aQ+YU*hf{etm4{P#IF*M}c{r7aQ+YU*hgW%cm4{b(c$J4&d3cqFS9y4qhgW%cm4{b(c$J4&d3cqF -S9y4qM^JeLl}Au{1eHfnc?6Y5PMnM{wWYt{`WY6 -RPGUtF@$YHxA6{~<;ncP6;jH0W9%z0$j1ewz=uEWhX^h9>R?HIQMZpXMCa7${R18&E-9dJ9wjSLXy7` -Fp%Ne@Jtit~)y1Gi_~9=JW@_Q35Kw+C*|xIJ)l=9u%q?HM-|&9S06B(iNm5;*{OVB7%!0|P{+stb+D1 -~O-4h5_Es3MChLR6B`86m2~qDl}|Vo@awapECPkYHj7 -CP+|{MH7ZtazztxOR6Z+*qmgFCg9GD8`i{09&HBh%(ydfOIBtE?##F|aA!#_1n$hZGjMZ?qRYU|30tl -JcVXNGxH(zcAz9iL#$AA$bEqA1s2y8aSD>+lHMT%w3v2ArwaZn`8>;|V8DIs#$^emZ?J8?5GOk@^Q8m -0IjbD<6z8m-5FglHgp@E&o>=@+Dxtb2y+|HOC!y}C!X<)~g9fQ1$kr))hekUbQ67paQB;mQk6m&KP9n -5!rWXB^1voj!*!7{<5?dI=f9P=h0nz;_LB+Q>jN`9$_r{vjF^6->A -drBUjl4noJ!&CC?DS3EGo;@WGPsy{V?wJ8N}fF ->j~COkr{wXLc**{Tr{vjF^6->AdrBUjl4noJ!&CC?DS3EGo;@WGPsy{V!CaoRBQ -8IgyujsRF4qE?Gq$huFGNo82J?ZL5Ag6p`>pTok;!`cV7_5s#Du(c1c_JOT^fVB^7?E|cR -U~3;>?E@br5^ezGBR2w;wBc?{#v}RQiwL!f?Da-?y^+1%2(LG?*BjyWM)rCmyxz!OZ-mzy+3St)dLw( -i5ngX(uQ$T$jjYHKiX2&yBNREZB1d?=k-gps{f?~Pu>f~r+)(>i7aiF@W{rZf{z^8M~)4mY77vrbL3NEVgqjWsAB_eK3XC+;O5geA{_0=j& -_8j9of;2aI_;k+7YL*L}T0r+{U;KxY_@X@V_Jb-_d}Z=Z4sUyEE<%+?{cE;O>mO19xZKn0I1l+#R^ti -H~sNV`tnQxaG*4;Xw{ejvyx{XOIh%E69z>26AWe0GVwSRvCLrCh~+}d3KExg5?97;)G!NK%$6wC-S@# -G4Dj4cOvGU$n#Fbyc2oeiI{idlCy|_o9CT~c_;F`6EW{Zo_8YVoyhY}#Jm%E-ierZBF{S!^G@V>Ct}` --JnzICxcTIxi0LTubQG~BiM%F>H*nvK8%veQOO=SpEAmn$VyO~&sS>eNNxW1^SgIsmsw6B`5-(K}mMV -#tDhW%K#7mWgrAp$ZO2Sel@lqvWsgii9lCV@syi`e8sw7^jBrH`DFI5tjDv6gW2}_m4OO=GBO5&wT!c -ryiQYB%jl6a|-uvAIBR7qH>Bwnf{EL9RORT7peiI*w~OO?b+m4u~A;-yN$QYG&omV=6o!2E;I>bcuJW~Z-(j3b29k=%!?$L*O?nKTc3O%a^aV|yubNGVy9sFR(R6IM&(u5C>7X~!uA9 -kl~-l;!O@Uc_p||$|fIPdl1tdJDwH -ruy(bVn*GCk5T!5GgZhMB~!&g3Ilb0jK*SvOBynm>ZIz`qV2hVO@y>!_T -gCVzkzxPDc%#Jd*~j>^k0m@Z@gt$J#tSmT>d07Q4H|2_-Z5yb@xsQSvBnx}&{$)QHE67{TW)a6jWyPw -vBrklps_ai*U(rSj2rgbc(>f3vBnx}&{$*hXmFr9o5v2-?5vs{s@bo}%1=RSO9lp3F4DdH|`s1)mf2y2bmp_j#XV})7!Cp>#WFFUS6FQxx*rNw# -Xfd+*y$^Ltlecb6f}DVAUM%2zIb)4yfi}WgFoj9IR{ud!~bxZA5g!$~K^EBNZlBkV)PU;5AqW2Xt_-4 -q~-@4OX@RWg9$24=CGUWgAeo!3J-@;0?BH1D0*DLJU~8!E^BdKZ6xwKp_Sz#DL>B_!Hikz&&Xi#VY?A -Z14v3WQd+%H9FY6g$h1V<4iuCPgb7^^_i>?6ACd|Atn@J^4|JP$03|-eI~5WH346U0TTPRkoT(GH+=rZ$H6c7{EH`ng(!r@TapV~l8d(_7q%o9`^ -XEQ?H7*@J{?_)-^+#fa`B)o7?j2DCANL8#k*Sz@8#mrLHykI4IGm@$fQ*`AO5;|X8}Wb-MnzW;T7EMu --=H6xG-cZe(LtgHNzIosNh9hz}IUOA`;Vt(Y9>;Ag6L=gq@0Q>2Z|}}B>0qbxBcaG -QD>7F3PXwk!PrgGi<#_6k@JT*K+u;~(r-7ePPv<2B&L5la?U9X@^KMDOv@R>)Diqr60(%bfM$=iz3%tl;gbbQX&D5o3KmP&ySct33I}{x=W!$4z)-i-#fmKmoE-q -KwXt_N3elwax(u%AM)`Wh%k6rTk;>+hQm(bC_*o}|aLx~l}>SCAt+Ba%_{XL?+lBZ1*FbkODZ$Gvc}c6hGiIdqA%oo6fE&GtL-j1to~&z -CZ{WL|@_yh{mbJYT50H(x&wa6Y*-HXC -ADGUCXOZZ&T_yn$qmZ^zu2YafM4Fn<$z!E8g^YAMeqU_h<#+4}w1ce-!)?_@m&Dz#j#F1b(^ -3YDM6Wf+5~GJmZC{E -|?y3h;}aU$FD52>t^6MerBkmw9br-e2P1Vnbt>g1-X4q+6^C{E~06D)3jqUx8ozja7la3jPZGvaVSb_ -{Gm(4fvbjZ@}LKe*=E;bJp2Y-=#da;kk0v(=t3)(mIymxsum`%^fG3I}W=$yA-(@)VnA6x_Yhu|N;FNr8?0KcqT*8u( -@_y_Qdf4&Cr55Yfxe+d2o{8R8x;Gcqj0>7-Y)&%}3__6+4vi@2V_+=fjCh*I(gbUXaE?KXw3H+J~vw& -YyVHWUfF3bXcO@>*(uh}pQ_%$770l((MEa2CKm<9ZD-QvPJecgip27XyztQ+`cUAb=HzXksd{NjhNw; -wLaH4FPrOR{Sg?i0Mlug2GnUthoZaiN_29_v}zF?BcAN58t3^Y^ab#Jcj9c@|-NyOhi$>@&z7ro$$tl -TFNB$90#b2p!ji%02*gJ$KoKK0xR7??-Nv*u9VIJtf`k<9bioZ~RDxl6B&jYd=Lk*!Q@X{C+_WqW@p6 -{iNr5ch-r!lW*C6WVqG=ujC{5XEsE;S=1fnJ1OL2p7g(7VtF=$ykc3Z7f!xyN7HrQkpMuuD$ -A85R9h>8GL}_FFtYk?t}Jy?|bY-at2@chD8j1bqqpK6Lh5b{+Ve;BUa+1b+kmCiolhH^JY4U+!ZaxQ} -%-!QX(t3H}EBCio5b^$Jo0eiQr#{3iGf_)YK|@SET_;5Wft>5L+}saAA)}X{}lWa_$BpiOyHk_e -**s${1f=6;Ge)h1^)#8DflPw%k8{l0{;^H3;37dU%|#>sm~9&E_mGNNA692A9pe(b90{k -pIug=Ptch!#F6@$<@aZu=p}N%BYk?8O0S^v{DMS?UCQq}e&6NyNO_c~f?db&hx{Jtml8nW5kas^`TaY -0Qqq*MujOT5>#KXHIdAVekwf8E2hN;0wO#UE?!SM9a06Yg@qJ${+$72Kcdz}$bLAReeE1A%cE@uQ>sK68s7Hli*Llp9Fsb{v`Ml@F&5afIkWT1pHa>XW-9*KLdXj{J4%jXThI=KMVd0{ -8{j4;Ln0T1Ah^mXMM3t;j4hJD!;Gbs|sHgd`XCLuHdT*Uln{+;j4nLDtuM&RfVq#z9fb@SKx1gzX87l -H|GZYP4GA1Z-T!8e-r!-_?zHwz~2P_2@SElq<_RPOP*Qv8x4kwTN9Uu&YJvXMz1JVm}M)XA%2ZU_Xo4&(bh1O~$1` -ZYDSlI8AUGaGJ=?AU6~I2K*-Y4fsv)8}M|&)6svI{yX~b(tk()l0Z{Br;d$-M -SXf@)$v5x-=O<9T-d$XVi)3=yHGDR5XAn1#Z0$U)P?urSyq_m8+Hvm*V#{un2yn4N{_E~S6q)HEqO8| -b^_{*frPOQko^ncwd4dndntr;mB1SJ0&&j7#TuNJEC?E`K}WtS|Zf*?-t2%K>@EyOdP4GhS2d#@6ze; -BMnMoLzUIODOP(!`WpL`hDnL`tj(;OFtg{c)^MT|$(t`NGdr@FZ3u#eLqAL%}V$>r -1mcOly5N)oPe;d9wlf@lf8e_i63ZbC+bd||uZClXgZPPkqb4NV*DGz9&N*XrXb^)H~JRSuWkgrlNW;a -0${^1<$^7{My%VO|w_RXH>3Aw)FDGu{UyH>V9A4H6x8H9WjYSi&Z((;)E#7vi}!N&6urG!$((vNETC; -qVs{OSlQ`H0XGo1nj5m+Hk~fQQ<=D8sB>vI5&OIk4b;s`=kv(wS>X888&lswL{98dlwslF8cTDXV49~CSs}>a(&(7?>Dt%rPnpOy4FXG)!$c57(?77a*(P1y%5e!4UK>UdI7Er -9M31;X&cPw!(HPXP0k6L4!U#zW+jm2=`-`iYlQi34cKz(xdbW7w=%mDyxELNTiEH(6hf5O;5<2GK`?xIb;3h;gh)59J>#}m5fhu< -y+7o8-l=2tvy9$aE=@xfJPiQF7S2eM%nMFZVG>Qfh~XyvraBIi3dD9Oy`2>t41juphWf4$#~L{Kb$JWJ -f0VE(I<^;=9BYK7@$|NnU*4xgfi~522q1@s_5&@?)MdOT%l&{3+!Y2y5s{#(WdpY0%UMh<%qu8|rCrO -&VTf^(0AZmgH+&@+p%xWHhkYKW>n4{6XNWghkCJLsB25{cyF<0GlrY -?ghOe^7$}C7lOz0I5A5(Yg{k(IN~zU*;#%<6%(htsqy|38g>}-3~2VmC;AAgLWAm_C`D051wF0SbNj; -gGUOs*jsd{z3|d#4)-U7gRxzf=gtf3>k?46bx}e*--q*k0aaBM6+E4ouXZn#oI`&P=mOO2ML2kc1fNM -A%*M*2)Ix;QwbWi!$ulU;ADhn=Jne0H#=1OL`bDlb`n5J(A6L$5H)S`2bop^x -Yq_HH;y&RA0uo}^u~3}Q9Qi+ -W9zvtd11z!KcBKan)TPxJ{W*IX6>U&Y?5H}WzLN!V*#1eUN`)AcIAOKu@JL(0l33t+zjy$GfErp1;^a -HOWXjz(e$s}+2{hrf(P6t(f}>scjNoz;o3j{1OYP#V_hlq3)Ks0x`jQC20$^kOAV-x?NmxMQx8M(XcNg~l-;5qtd1?oW4a=)z%*NEXBfHL4G7atUtt00cMLn8f+C -JV0&j5yZr{PY-6!6$3ABaBM}{r=?}$c<`xDWdbQu4KGMzH>ehN# -H{PEmBJ$}x;UX|nUaVBFmC2KR{9g9W^ow}KLXsXv;|D%5ti3MXC9=YJf3)6Z`v8?Z77Cjuo&+gM=^5D -5W}a-le+t4)9&2SOZMN&c6gGyz19`gyOfo9$0eUM{8~Zt0sD8juOs)5Ij8eCp&Vxa08mQ<1QY-O00;p -4c~(>DqeJEw0000L0000W0001RX>c!Jc4cm4Z*nhbWNu+EaA9L>VP|DuWMOn+E^v8^k1tCtD$dN$i;q -{ZRZut9Gg9Z`0sv4;0|XQR000O8`*~JVyvGuR9hd+Bc@zTx9RL6TaA|NaUv_0~WN&gWX=H9;FLiWtG& -W>mbYU)Vd9=N0b6m-hCHh^zg55ncDov}qahJ@SsTp<8y?O67YqK}+%jT(|1wm8)0(X2t8Tu6^~LmFr%8WyMc_vtr#_Z>@g!A6C7xcIDr_vU1(Z?|kR -AcivjFV%=}vUbXi16|2^~{nk6{R{S5U-d*?o760;!w^zUV{T2UX?W(ude&;)X{nvl}&-wqEuK4dO-(L -LazpVVJ`>n-?|Hr@lUvc&C-3Pw&o$vhQC%=64op;?6Kl#auAFlXr5dD4I-?x4DJFmR@+KQjN`|9gIX? -y8=KlsiH_rJe+`kSx*V8u({Tk&`QxcG}dTKuISE&l9}R;_g{cz4}9KmJQQyx^Ji$3I^Dl6PKR_ltMdu -K3a7A1}52y6yWbz8ih_d*6TlccA_b)!&i&J63;J)ZbP0ccT8TslV&$?^OMrslOZQ?_B*|sJ~17zJ>tJ -K%glIGzWnuAn1e%UO^AT!3LSqq{k5KawYCb~EN2vJ-H6NkoBh-9 -^nvYQP5o$idr+ob1Qc!4t<|ArTpNIl22(=*6f>;YGT2R%3LXsvZJ`xyp;c|6Ro!E&y2nnHDs(Ks(B+c9hjp2@>rn6HR-fX-~AHOf(;f<|EO3B$|&z^O0yi63s`V%`nksm}oOhv>7J -a3~NvM_*W?iv>?=iNDE>usAxe|3lc4;X+d2JQZ2}|prHl178F`gDnVWIQP+IbH6L}&M_uz#*L>79A9c -+~UGq`beAG1`bGDbNljEzyKdAlxbfZ@DfaQkd!AD{Du>HDDmQ#w^WHhxD6i4?&P%aZ>3bKX*oThqU6f+4+4n9Hi`K97^!@EL;7lO#8MhT!u -w$qk<&_&iH;!)FLS&y(En8G_G?Bqu&)5D_hF5D|n>+W>@Msj -HBI?0WeB53I*$&HpGKVH8-N^=p -?xwlLBNVKjBtPgSxgMheWF$WxUrT_u=?}=~#>Ph=CHVpQ+}Ic!5|ST~&y9_-AszVv`P_=JH -6$ZHAfHB*o9KFQ}+4WCu;Nj}%)5rZoDB%f>Yi9r>7lFv1H#h?m4$>*BFdUrR;y)XTcd}+=2(wZmr?p~66Q@u;>q~@vpa0|%|wK -Z>;ACXI_c_QzU`>A{Lyi3le?)m(1C&|sIRQIFOU7x0r0ZhES0ZiSi?Jjwtx=~vlY9o(P_iDR)j^w^ae -8`j;4^#Jk<1TrFy0`PYLnJqLUWa<9N2gvrACfmV3D_Wop%V`#5gVj1bn?a~AseKgv5(i29EMJerN+=x -1P#>vQsX^R&*6vUdsE|kQ*YuQQ#VUJhabwsQeaZELFSG7<4q*@9DYoZ43e}TUL&~)ypW*%@CM0E;FWm -}Kc)_r!RkgxZmbUJ*AFL1ZWu$h^A&cu$Y8Ff@Eh5}wJi4r{n0j(8-qsj@1yNfo*QK#@%IrCksD8gq~A -y6cXOi#B>X-i|C$>$X=Xws1@CMixwftMqxyKN>$2)x_H)=! -f@y-sC8?_<#cxM;MjoOfVyt9YoMs3JF-q}ZTqc-Fo?;IexQ5$lPcMg%Dbr%7)3MDFptcx#1Hz!8^kwH+)9^jiv{QP$cl~jFViCz#$TNA08xmfv-M8fO#_{@b0#gTn8{A5_li3C%LzakI9uGj -rZ{ul4B-APBJ#uf#Slu{LRKXkj1-8PSRxYLS*soZY8;~4rKA}lJ_!MyfE|D@dk-OEhzzk`p-!0`F)4P -m?Dml+|zq^jO3X1&r49? -PjnyRf%6ON=UKzh8F>Ia26ccntv2Vuj*OS~ggKA?U)88WWx8Ls~xo^Yod)0TKz-ENxrt*2whGnTxR53 -N!$o>6ckmRN~8@azf43pdxX(RXdF}2X-{vzc5{&17z`QneRkO86dfzJAQJR=V)fQgfeNBRM+Rb&`Ya8zlE^e?n{{LHEfml7nqBsyf&n -CAp6lJ{cpq=bWnTBBb>`nIO4m{*y_Pd*(lpbFT==y?f-~QqTWAvc^{$2;kJ@fZEN$#1y*F|# -A{Jn0Hd*<);klZtWub1SW`Fk5l?wP;0iR7O7dz(qFC$=I~o-t -R*LUI2>?HRuda#74Js?S*aru-~I{n@<(BsY6&PfTwB8$OYvx_6%BhEL?E?p-9g;S)Kkd*t~|jw(Wq>K^%glcS1CSSmGlD*#K77TeJ$?lGim^uBi0>_7D6?&*UG-KbPJE{6YHX7@4j6 -+ez;EyuX9wp3nO}GcCiVWLd+dxXnA7`@5)FUU~O-liVxs{vMM1j&Xl4$-N5i?<2YI9{2Z?+$-|_0g`( -~-aklk@7(VnBDwD>_YafYtNDIE$-SEIA0fF{^L^=w!5&P0#(4B-cYojGCVNr%A2{<`^|S_s^1C56m%YdhVYmxgMBf)b!lHNOC=R -#Hi`HKR|LlFvqCrxqpS^dSH$*ufI>8#LVks%9xl81wr3*C-s+o48Smpr*3U$ -*?OqCzd*j+bz$0YdyjoPMuCC2pl0gcynqjyYu>=Q{Bz!=u{rhDVrfKc -L~iZq$Z(_5*T%ridm+5zPZ~f2N2g#ti!bxj$1x6Ju`ufZU(i-56t5{eaw`+1?mqKK+2)pV{FUqpaoux -j$1@6Qiu=0lB}_@QGyo19E?6%VUf@{R47;nc)*T`Um9xGQ%gb^AE`VnLUp&67vtp{h7>sj17+u$o-iu -k1>jF9+3MpMK>{aIzAxxXZAV94V|luvB&Wtg+vXV;*PPu@gW664P6rxV{hX_3WFMYZWp5}=ph9_4L!G -uvDxq;g+2{k6BA>j;X?{~8amSxqsHhVg*&FkC`Lx-A-Ut+*dcN`56KCVv -wnfAf&svB}@W$lp99cWm-EG4eML$sL>gO^p1_L-La*e-k5r^N{?c$=}4t-#jEgY4SHQ@;493Pn!HqjQ -q_*@{=Zi6C;1~ko=^{-^9q@JS0D9@;5Q^HxJ2An*2?S{LMr1lO}%?BY*Rd{G{1-7$bA@ko=^{+{DP-J -S0D9>XTyBCp{!TX|^NAs7!iDe$rGX#mLz_BtL0#HZdxb9+ICll}RzOI1kBBn#!aK@;8q*kX-XwLFVR> -pE}CWq`s`7NyQXERgklJ)J1ZwwhFQ}k9tV1)mA~C=FvuyYqeF7p?S2KvM-PJk=*c!yvw5lBsY8_#wLI!4x#1I8mPbcPZum -rg<F9-St+;S<@EM`uZH_(UG%(Rq>^K9NCrbdltSPvlM>4UpXMiLA+^D>m&#NH%JctZ;~AR --y%8qA0avTA0;{XA0s*VA168ZpCCE-pCmc>mqTm?dnZ3%Pjb)y=QPk_2j%DOB=`J(-a&HD|L2_~_xyi -Oqak)ve%?)TeL7Ob?3%2gB;&Kw+&Z)Us$$A3DkdvbF$Y0aOqQq8&;>0Ov%R5GnBOviVx`pQLsi?%fog -$1%&8jCgouuVTi(N1tBKd&muDZ#o>fI|@^P<1ynep!QHbwFk2fjAoDrAna+j(pUrXBbp;k#?4tpz82|ow`#HZOvM~;R?+WXtC;Vpn(wKabJJ4unarHvW_B>ujP=*db -h=iU!{f{pv5uY5PfqQ5a%!BgI!z@) -Zt~G}dp4cPFV|ChZk$fe)HCxvnNBp-GjqzFv50zRkEGM-oqEHZh1bxLW4&S8+|ZF=yO7a_*LXUPlJ0Ie|Ehv{j~N8$cTA-jZwTNdl_HZxu;GhQn*UMn+RD>J9zWEDN$W+K+Qbo|LBI8T9-~}%>J6uM%6H6yh3KS9Aq -`awDAg=@d}xlF=jPAK4c~u%gkXJnK}43Gi&mhImb7v8?Bm?d^2;VM3$PKVV1G8)L4f(!y!ved(5f0ne -jkbWpR=C_m#<> -{-b2A3!=8XEhZrW^eX1OW#$uphD%1!Z1o@pD)P3|pknD!W3%^TV$|!rWcCj#4Qka_`ikkT?bN540GY3SQXsR#=nH8zF$ -ikG?6{(J<3R6^9U{Cmyv8_)=i77t6iN@gNJ$AJ)_FWkJE{wk}jK3~SA1F+bR$=^gVdAAC*W*VaGG(I| -pNt+LEGO(ZVdfZa(=STndrQ1?$FB8G-;FF0V!VI@q23q{|HNu -dzn;d#Fxt_2@V(w7|V?AJUSYvgG*+6c$y=EU9?$4c8{0x9@RP}l`qRd?GC}lqWm}m&o!;1%nf{dx^BGgoS>}3R2-=LV2-?iVHfTf1> -h!g|y67cNr{klx}F^!5}mNVGx>`QV^O$C!9!+2~K2AvkNQs -7c2G`O&%r)&7>*_&0S4SWWUQ~R)f%-JrIN@CmMt%DH=RGqD6hmzGG&%56v*^MD`a=owXC$Uo=B)5Sln -K2<=INk(os}QR{TTG9REC1?J77z_!`UuARtOdz9!-9z^!6-pE{C6+~tUI(W9}m_LO|)v3O#jMRY)GLi -xrVk84H%m{jT-!(>ZAbc`?Y0NvezN=~`V@_mG);8(AplS)KmZsY!_7@W~tql@0uMLtw2b)0>+V3*UZ9 -!t{6oRCpL-!!DP9rh#h!fd%C8nz+=7K&avcG7}E-{G|Co%_uCT6KONK9VBiHr#+=6V??GM}nh4b|F=q -1WskD7D39-G640X==@sx;T+-gjH=VF~(F|;*@QZ3l3`bc-@-qxpn*6^}u-Nx@}iI)brAyZpvqz$bQNi -W<55>RJV60)T@Toy16ydiA;y7TfXY4ZI|V%Zr0s`x(z$(HaM!A$ySh>Ix#1*pR!R&YP)D^y?JUjZw9H -km@!E0xSm>{pIVoeriS~}@|D^VJG1tc#fFK@MoXD}$IR+6D~;>O%neLIBQmjOBQj^UKFfgfr?~ixM*r -nKax`dQ?=78v-~b$#aE$?+m%xWf*RWxK0|VIG+|B^DGH+r4d-G2+;3I%<4{T<{IIsrx>rQoSXCwf!gO -L!(PDUaiyBLXq>}I3_WDg@%AbS}}fb3%g;ZMhYMiBmV9AE_DPsc$<5dL%=Vx-jsr-AA0R7XD}1&||*l -tAc|>qaW{bBqz8pW}=O{hVM#=!cCa75X{Fh|tezMudLOFe3DGmJy+!bBqZ6oM%Mn=K>=_KNlGh`nkl2 -(9ZxPLO+)o5&EH1vm2Sv&s9c*eg+v4`eBEX3H=N+BJ{()CKLL(&WO;@4Mv21ZZabDbBht7pAklcenuH -F<8dPs^iMD%=x48)3Hs@1?uMPKHf%PdVIzfxS+x%Aa#xUJFU?ApV3RXT0Z!yKz}AC}HEbo=Si@JPtO)bAF_P -OgRFxbhb}Qw)ymSZjiZ;;%T<#WFNY(T=xjj;)LXq=+Un)3>%-Cx9Wmwra|uR{@boU<3xnwNWtQW%r9B -Mb*5s=U;MOH}0SHWAV>!+v=lfa^mEJwh06G@i9b!_xbt*?l%GXroh)C~VU;qKn^)W^~-yd@aLOgnRfB -}R%H@X<{`uJGAI~?gC15gO_j#!lM#mW(i-et2yBzl+ooH!x=Is?$iEe5cO -B-9|WZVd_>NBlfzeK=8bS5zBKRc;3Z`M?_tF990;EGF=BZR1kVQ;u{;NY=R=HGo&&*iKO>gsK=6 -E&5zBKZc=n#3Lbv5TCDyfIi5tL#KKTwXdmnZY3Vrf5VD?JvA{6@Mo51X?*hMJx$(MoIi?NGP=#%dQGp -9f}k#dJV%1(5k@S}k>GiZ5zBKVc -%ERy@*D}CZ!=!n3#_6IAC_kI1zXr;c>v!lsOT29^rAo --iuveE;=>B}A7pX8&w&(=HOhGy7+Ip>~l7p4mU!JGF~M@XY?%UaMUsf@ -k*6_GaxO5j?Yhw$t7s5j?YhHfQlVk>HvAvpJ5}i3HE=pY5VXK5=64)F_Vw=FH-tz@f!cqdX3n18STI`W)qP!0cEK -3b7PA%Hx1J*2am1K6xB4`<$If=#$3*dj)ra)yS0}PcnklF6BVY?nfu`um0}44A?gpi;ScE#r6{ILaZ* -1vS+sUXcuBxag;r?y-K?f>xrZ6na!RBClVIMp4prQ>_o!C*fX2sfSpKK7<*=W(RLx$3`f~Bn?r$vLM# -@JvS&8u0y~l5nLV@FFBBAFd2p0Hv)KseM1p7b%;tb#ClWlfXSUaE7h)xFls&UKCfJDt&+M7)_1uM692 -{lOYz_+!o^2F)_Te%1%ni$P13ZthXEqlnIT3gsH?ad(I2NGyCTyt~xIS&+MOz)-__wdz`KNX`xRZ2kgb_MJe>jqJ7IJPw$nkDW;9lg9yjJ9| -+IeeyV94nTGyp-&zM?CtD@C@3G}abSsjx2Pl^<8i?56)8%=Gmiu2Fyx>VS?Tfhj9}e#d;=qB`gl7d2* -1WV7{MEicQS%%m+>w}@FL^gjG)?OoQ93kRxX!zXF@4*#N*o;5pTVN5%JbL84+*2ixKhGyBQI0y@wG~y -jt3V=2B!u$B!{0-rzVR;tftPBHrL6BjOECF(TgJG$TT9XBZKBBllR^YV1-ZB*(8ZB3^Nj5%G#cjEGkp -W<c0;Dd5TlH@YYa85;~g49x -H8@eOh#_u@*dRd+JGXh@h;syL`dV^8be9Rc#paz6mw1e7Op1yxog=W-@P1hsL^>1UMCH=2Zt_n~6ruG)?jM6Og9g{RW -GAZPk?Zasg9(EMw;U|ytrlP9ul|YwofCESm=p(?12<=%`}v0NWh=|tWvVwU>|zQS^E6IGUbk`k7C4^(5h_dj) -(r_X8oTb6t0Gu^wB5pPvLWVv@eFR|QP*7sQMWAn8nueBfpM80+J@YkApo3PDX*BshI;=;Dl__6?l_sV~GnUMg9| -9y(SmiJ6>*8*=Zs%73@es@(NI*N9p%JbKF`5)e5qzdF0jCgyY{kaY>LrH*z&EH*pjS+7xue01+%Ns2B -KJNXq1YqCHDwcbGRa?jCwry20BlZs1@LlbToeX$#zu3iqH$(0XkqHh1tkS-=^4E-DRi`C`tRT=YrvSP7JK38L`A={#`Tz;p*d;&DFSSf#xplS>6j}82*6o3u<8u40N^-Bi4 -%9I>?_|-R60VpI9h=O3u+#9h3uH;@rzgFLr0Z@on4i6XA7Jd_J-`pXw^39$vX`zjzl{S)=+DKYyBWa< -Hq?I<3mfA>KYa?l;jl^n$!oKA_hTi>tE3GFjwVt%ldeTDcNeitfEwpY<-*X~;?|>88xC`^RHscG;al} -rf6VqW}jx7vB#0Jy77u`Pwa|R7=0)-HP!gTLt_m9A+jX{H(LGIaU%n^&0_*7v&Wr|9|2=!+(>w6hVfo -x<%mpq1Xs1Jz^V^cuoMEZ=XFoyRHSnm6K$a3G^BbNKl9<$u{^$N>WwiZs#o^r7^rvlrWmM~_T4W~Z;fG)UmI>W;P-~#4fw_3cLRQNxZRN7e9`Sb%ZLQuW5mxPUkVlC -=a4T&3h`;rmtuwZyyr_5h4{qhOI3yVyw6LCLNbrk6yh^KFVz)-+)vP^paPI)p#-F8sI>u7Jk$a}iipK -z++QHY#G-fo3#6#12{kD$YS2@l$f!Y4fnuWuO@)e$no0S`b+EdUKU@c^KUD}weOV#k^c6-@Q2MGu!02 -lV0imxe1bn`s5YV|wAz<@o3IUmKDg<1vRtT(qjY44cYZU^ke@h{-`nMGV{rp@Z(9b&xfqvdq2=ud#kq -r9zg+id8Un&Ip`ISPTpI<8k`uUAQpr79=#19fHS-#}Kc24dGiI-XK2MHf0Q6#v$w*a``_hZqk+%C-PS -?))o*IDidqBmIX$Dvg$_rnm)u48O-dz0mU5L(T0KL)K~xgUbovV7^ZO5E_lz}qbMdj2`fy`JA;x!3c% -Ecbd|$8yjAFIc|hiFQuz`TrHmJ^#OEx##~kEcg6VVlB>>UT}zW&p+FBzH|aPMwQAdEcg7gz2%;Nwm0m -9V|&Xz|7>sh(%GCiUpku;=Syb|V-&WqyXl>NYU((3)40=C$lrJ)Eu7`rU*HI@Pi*R;+^{s6;K7B5R -;)!VGz?7`+t9nG1HvPTv_2nx~3(x_XRtV`RN7@Rhzzgl@Zi&TpeU21Tw@3swb`vGlJ5ItJfH*0J+WxY -A3GVU19FRzI*<`YQXr#@V7>P07$Z>G)p15Jv%fmQNC9M$5lro`-X;W#O4Ebu8G(KVH!uSI -47M`@{S0<60{skjG6MY!b}<6|40baD{S5Xn0{sm3G6MY!Ze#@d8QjDO^fS1b5$I=d3nS3a;8sSUpTTX -6KtF?hgoHvr+Zhr1*};g=&rU{!es(b;^s}20p&!2)isHZJy$8azK-j@vZkw=!eT)b@*w2Wtg9D5RJ2= -RQu!BR42s=2;h_HixMuZ(4VZ>BdG;9r7!`zS(Hmbr*jx%CXZVe=-?p|a7TM4eSYe9bMt^&;3X`?EXa* -h$RZr-R0ZCqf)1ntgd<+Ldm!y}m&YBXo$hj1L)z&s_a}3(lM{)~$3v7k*vW}R-{KL-4 -!8|uw|~ZR|DwNSxqr_)?DLV{zRNBjWmMBc?DEaYN};%GWqOExy*XJa6xXaw53!RsCmx03>Xqps_U`7y -qfp$sGCjm@-JE#jMCjo|?9a`KM^1#UJ;aXOoOl#Au%l~wh&{MD@hB9Rzf2FYZJHC0LUHTM^bq@ObK+4 -bu6vmt+QW$Dxh{BS@3}-ER~J09+cY+I=^^%)b<1;I@XU^~Zh5W?o{utOc}@k->=n)VN};&DWqR -l&BbMh>@O+vP%X2DtKFf&ZITbvgXT*#Vst;L+ -l{U`AVUy9OiMroa5v~(C09Z1Lhp3u#pR%c^ok3I60Bvna2Thj*}A!o_QQF=Quf$;F-q(bB>b}37&Z -zFy}Zqk>Huf0dtO%6A7Mq9I!cxMlN{faloA86gCRMGmiu29499dJo7kU&T(=gM5MzUkrp;0EifY(zRZ -YOjc=5q8SEy_@k35zGByo!I#8I0CS#M^a>m^5Z^_Nc)#0-Xm-th5rt<8{4*nE^?CI;Ii`*{z=jMu4Co -<~I>te) -=-_9E_&MA+qg{+tfox$U0dkCy8jw*&{7iYaQ?<=|+1aksjATGAGUA=>Y2eX?FaofBd%x)fG#M{B_mOYGAfE;9`3gi|e-VSEBZeXMaiDb@n=n6 -szOG6Maa9%cmk8Q9JU^mCcL3-XJzS6HXWFV0@=V+8uSdV&$?=c@WP=x6XUBhb&_AS2MvO}1;4_|4vAP -l5Wr*_-@r*e5W1bCMD0XJm*G=x2nz7`6<|j<6TQrh(b9Ym7iYW2yzv&-i9WpdUIuFBIpz%}%hkan9Q; -9l;ffbKYhr*mkgkVD@$gBVIpqz1*{K&f8os_eSh4nA^zjgY5-#yRR@30Ab#6H^toE9!4S{+=FpN#oXR -gj8uTIPsB|XbNkr)V?V>(KDIaPXqY>6l@YI>xx>sGPNbVV%p*EZq?@~>THy6FH^6-kC(_LgsP}<>F0- -~{5y!B8u)SjL%5FxWpDWxOv9)4Oc?#^Sn7gXJ4f^4A&Juennj6@|Vn;=@_W~mc5Vl0@rfBw#F;WM@Js -3MFnj6{Pu#cj-k)tQ!-P`laUe#B>_q-kTiGkc#0C<=B_i0c>OeY?`I?e!s8qkN -}9XbMp4S!+|3q`V&3Lnwkj;8H23ag1o}C^n!}1p^S}fn(9c00OR%ugJjgmlRd4f>{0fh`ex`RC -iYZ(}8DzK0`K?B}2FVb6yJsreq&f(npvM!bIJd-+YVuYbOm`vR7y=6l&wU{A>WM($ZyrJCQ!aTWIW&u -?Tqz+%<>fvt={KL>bRz?#+kagJ_r2I2g1ep4)6%^zp3vE6_EH1|fVV9lT5K8G#;^Jm#Yu#h!>j@yP^| -MOg%BTgNh=Xw!w=HUDw&lj+)H9yFVV)y?%FH4Ej2IsGHjD!;a=C8Al$0FDK4YoI&GdMrW`oUV){22FB -LOb4f?sru^ -6hl7cQ!{K|hyx_JPXog-bk}!qV5mCHB3j?q0aW;{sN|76v%;g9`73VQv8y!WKrjH=@dWLCuh{9=0&5z -76^r@Bf!wlKk?OfFK*%mEhA7AD!dpbC6pQngL5uS6{lMRRh!?&L(im4Clv^trq@X}I6l%Sw?rTx^B -{xTwus^%=E^j0@#59U?N8os1ykvbBp5y@(LC#rkj?C$cn}#h|Fo6sSbcj_P@)mznkh3^fFFkRiPe=0x -67XWDyb+BXqs7B`}{Qha`Rrb8w;qGzY@{40y}a%~hu_EX5wO?9qk1SczYZeRq(e%%)s!KuNW?Tp~e;O -|+G_>Dl7o<53RaI;^dR5t}~24KV4@TfquHqF#`Q`oo594>Ea$ -NuEwBszet>RGS$UBSlp1&#XVSDP1|*u5$LDu3M0@@7xzv8iW-V>`l#aBRmI5sr;JXd?8(9aLO*+sR!j5sr;Jj=1%pX9ytqE3%Uhxh9;4;G%)_-nbWpZyaYacFcXx3?Ne^=%t|#eYyB3#* -bnRzEI8e5DabZX|hZ*9!kZuk$#APAf+=IncA>BMAiHkzIc}No1gmkla5toE?b1xBBgmm*TW{%*D?CCI -0B#Kp4uwSm>rz-Oux^D_1lFUls= -ziXED_jdh1CSMMPYS;ZBtk(us(%l0^6>zhQM|xEEm{Lh2i9fZ?9@S;OvJ#w(n;wfd2lt`IN$-zd!bmD --8PkN$l$e}5XerZDL5-#Zj%0rdCpTa?}d -=P4dr1*@Wzi --A)Dh&FY-Jv>B1pUnpoKYC`*F2=Z6Z-pRT-gh*0eESCSPdMJ(BD>N^KqMacV+Xj=zrtNCSvGsz6ZLcc -z1bk1aWPML>q=x`%vPxr;iatQ6uAwpoEG_wH$(_|I6yzbY*uGBE#|ax>p&&L3IHGI0`>x00*Q+4B%kb -m;wKKVTA!4=~`vLzg(Cw;3LGa#()nH!#V>#K8)HJ@Zn*k-W&(GsyFw+VWi&N$A*!5a~~Q;{N^I1!HL; -Wq%=6m8j6$#Crm?;(%`gcSQRtpZbrn+nJr6ZHa0TSDkT_JTcrd;k=fwHUnnvgocIexW`h%dp~!4-;x8 -1L4Nm-p$ZWV6juEdO7r_yN%!Z5K81edX(HkRPKQ4A-#OvpeRJP|3uOAn;G2->(qBcgneq7APh}Vyc*c -kEpaq$`>UOz5cW5nyn#cGUr{kTYtkp^0D>)YXD40;v0Ans57>PfGqKOMZvh~C)~g-C3uNoU<6gKhgm;JOt{#K5e%u@w=#mNy^SM`pc;rae>((2>S@*j5)&@ -sVg!kavuw~vOf+}jWCT^?wBx`bSS{aho)IJ_Iu&n7Ol;+jBX(B}vrbVBv~Y|E3{(SsGtSyZHPCGL0Y* -@jJaCc`kvQ7K2zEzt5-3bX|KULf)j+d1&oBc0^f9BT2AbV6#t7D!FYjXn`WaBaBdT82o95un_frZzm- -jjp*8)_94fBAGsv{R`6N0Lv*;9uZ@pce#XojMrh(j~f97PZXWH4n;`GUx_A89kfDAH%W7gZ*zhWoOgYoRV;Rn-oa -x{=6{l0qbZ|_A^C@RKcq)YxDrY))DupvDXF7OVgd+-OI`%LE{d90lgOdtoIyh1;pr4K-j6gpf93_@QK -Ri0jD}{cz&y_+y+~-Q6AMSId&=2>yQs{>xu~O)Vdt)i|!@aQ-`r&C_DfGkrv=sW`ep(9saKu*% -{csO1g?@$@!SdDEso9-mj8U6(aWfq|W;(`|^+#<1edMCErkVER{E?78GCb39P -(&h+$K+OP6QD)f%WUq^v3jmtBfy@-N9f=|`uf1HBi<-IJ%wL -@5*d&AR9@DN1QRL9nO^O2aoWq`p7gU&1$b;3Fgs_Ii6$Dj2jZRkOwKX_#c{RYAcm;AZ9!54pvRU8OIy -z=zb+s7BpO}$`7pBh+f_g4Ohu*kv1PQex*2y+}>x@d`xzIuc>QS;7n@5T3ESjH7JL5^T`LrJ -d2`Vij2j7s$&4-GM#4zrteQ}aHtUz*A{1jzj|z>VPz*a8%`1wL$LKN?gU==vAINKRI<_rgPU5nhj=n_ -`=ww`sMT=v=(r?~SpVHqVMyKtI_$Bj-Vwl?fbR=+Y*!GdIq)!=Jh}sBeq9PGn>@rV^@Z#w(_T0Pj7!X -DDsgp~LijlB_y-&ZwnOpl=^K%vXMIB~H5{331`=7pO`|Tyh#z@%3K@}cHQB8tRJ@HP)>+&a_c1>1Vl+ -yENFpDz6ooU=+To}cHMf^77!YKNm58R4NVHAfKo2YPM4u>>_ahv@9r(e#xj01YukQ;{@SKlve;mC8># -11r$)kpMCR3!e?-1-==5`Iz5+oCuje0b5doiz;=hH&(`Cs!Y-%a5q2BTgB7-iOt@2y-~Dqk_1>;1X^u -&gDmz93f+24yTrg(Be|#{Cq`N#F=LSDdWN-E`S0LKwwT5d>CJ+nTCNi(4@bED@+wX=gq78rfxKet2EuhFLd%O+UTAp1x@ -%~7!OH6!Z?9y;Ywr~Wc-5`^1p{7hKVA6_BVKE7`8*&^S(f)!Bi8_L8gIYME%SEq${Gf0076D^L(`gw5 -pNKy6f;=-7#W`H-OeD^5BCq~TtOmgXV0pYpa{#mbNjc=(P6m`#6s1v3$x>W-;5trD}51Ge3kC8;Ja1< --Y7m9R|u}HR)DvPHx=NG;?18j;FYyT0bW_JD*%^itz6BBSJs;b^4fa)H3q!4)+(TZw^s3c( -#r3cy{o3h-KaQvtY*R&{$U@4l=NuNMu3_!Whqwg``L;Ptdt0ag5nLZBxFp -!+DG-s%Z>wO=s+uciRu)fD2@vqk~vJ_;yXJRz>}yT>yNEWxhP$Wx{KD~r4~e)@9;&~12t$ExegY++b) -ec4%%^1znHczyj-Pegf;OtNF* -Vyt?`=rU}cdtAEXaSBDzHvADWs6%PnlTwU{)LVWLd`wa&4iPug9$Mv>1Bq|{^Bq|{^Bq|{^Bq|{^Bq|{^Bq|{^Bq|{^Br -72_CMzK{CMzK{CT`z0n7Dx3VB!vLgNbXnHHJ0az_5Z<-N4X-b=|=5f|cFS@Pf77(C~uQ-O%uY_1)0$f -)(D-@Pako(C~s)-q7-b#EIp_>o2stU;=J=K|Q+V1tsa07q7q9d7crs>8*(Ma`8u`<4W7ir_#7)J{6n2 -r)g}4sx&r-)u+|PWd!h}YEjOe=Rv|157?_k98ToF9)V#M-X5j^i<#PVDbJnv(~@>~%-A7I -4tToF7UV#M-X5j^)ZVtK9zo{utOd9Dhck27Ly23$rIp)s0v|1PXWS3*^ymKO=FU4O -5HtKj=2!Ui9FBuH!tg3F;iaJ$#Z8!)1b^hHnp_X2furporn -y!)F;WTqdYn`1bI5Mhuq;>J+{`e322uWrA{qZx0VJVr(`+;la0uuP|b4Hi-mv!;Dz!B0=4CMl5xapzb -Ckmbyq#H^PXeE)vv@F=DBU1a%XPSn47{-EBrJbt01M-@u6J?um%x`a2l0Jc~%Kzl#yevxwySdl<1ii% -712BO{h)5y|y$X2kL=BDwypj98vUB-h`^h~-&Ca&$VaLoClClI!2ah~-&Ca{YT4u{?`Nu74jRmS+*k^ -&eow@+=~`{zHsdo<$_r-_MBUSwwREM;Wm^i%72jI3t#45y|zl%e9eQA|kndcDXi^OGG5s&o0+Sa*2rK -`q|~$NG=hPTtB;98_6XilIv%eYa_Wt?91)}_qZoQPog2zy&|akCSFrz2Y!u{> -ch^Dg(1nhA>%5osP_muMr+L`0fL*d^LXGZDe#5%zL6cuYj_c!a&24IUE_JRV^$XM@KC!Q;0_*vpv%YM -lsnbcDT}xiC3RQb8SiIdfsM6A9|r%b5$4ok-XbYtUSn>_mcR_HyRJWG50lvzId$COeVm`R*A8yfxlE% -z!8Ru3`ixd5HnsFMojnOE{JVKR&{M&!K+IJqgQ#AG7YUD){XHTXH72=g8DtqU=P1dyY)aCCW}DxM%le -E>U(OVdET`noE?ONZ2?>rsfi5ClWT!zS3N7oF)x@e6kb4l8>@SG*=iqkzjK-BR&uPaXSMzY2(H=MiTs -TFC+SN7bn6nbaWFVb{JY*(Bs!cp8VURHyEj!=aB$^pSvKk-yg7oN=9X9|(sziawb*_tF|* -y8xsNohHS`(mPGp9NT5f;Q+<=E4W_>^&7S({-Z3+#6V_Pav!j#;?Gr -%YeZLi>xh=VW5O!x@+i^|s$$y2%KJr;7~u<)Uu~b`nyxzllyg*hp3q6@QQFz-iMsP0Dw&RT8 -T%>I$7{R$n+fFiqbCI^4Vg%Im7{R$n+gL~9MB#0 -h7{N?p+W;dtNp{<1MsT9=Hkt%w;zZ$XR~Zrd8DvE0XNVD@pJ7IXey%Yh^mCmNp`RO!2>r0pi<5r0-C{ -)OXM_=RZnU^wYP25$LC{oe}7#uY(cjr>~O{=%=rX5$ -LC{n-S=zuZI!nhpr)Z2=vppkrC*pZxbWXPv2%npr5`ij6gqq?9niTn(Eue2=voOnYt_$`q|Ei(9aG=g -nronrb0iv7!mr}&4|#?9!7+I_A(;$vyTy>pZ$yo{TyIK=;t6KLO+KX5&AjIh|o_zBSJq%7!mrRxo?&U -{qXpc3H|W+lL`Is_>&3!`04p2|M6%E)#aZ<6I` -}fXBH^*a44onXm&M=Q3djJkDjp4tShv*vO_Zk!og6?sg&*sWwb(oIlHY{40qv`zcc{lRuli@ux~1MU{ -1&+Au%Z%LtBNJ9Lc^T*gS33OWRrF%GM5!==M?A(%tN@m@oW;L>igJBQ%1GP+vJAvnbAwE7)fN!Ftf>| ->#mQyqevcw;pG%#LKtH|eO>rgW{sBU8V!*;J -ep8$hFnfJFBRHh+_C-d}OZIJL1Xq@xKE(+1vw0&UIHa&=KO?xZ^w?2Ga0s62!Px#kcX%HoI7IR~YYto -fo14cO!6Aio%z#612;O8jBRHh6`z9keoNhB~0lWC;cCogxg@5kiE=F+mb1!QSd-j`$&N2f1&=tlG!Ii -T6`F*fXKYxII6*l9~-@L*Iu3S9KJ`sEH7Y5Wb=s!2vQ()hH^TZZLa7f{4_B+^X-@I{*5gZ=a$CilA^$ -S~A3s}nM3n?7}|8{XNBRE9zBwH``(a(;n-@(<~gWPItoS)w?$q4-0#uJRdzn$b>f@S{sVfIMa7r$_bt -rx5PvnRQqVk7+QplSg&QJ!P##pd_9n+F-e_Q`SY{beK>FYir0t_2w9uCa}x)S!8b*}+cw1*&Rw2<%|< -PDbEI+to8T?6rsc9Jb5P_HbW7{Xz4bdIolIgKY<82=jeBW?^Ie!T}zcP>nEuc|9ZE4(2xVxLpx;!1jh -D-TXz>Hg5;t+!|p7e)J0aH)ger#SHSUer{XR=M4u>EPInE;!cD^?c^N4|(hWQ=rN3rXD{sND*$a&6PW* -?87Ctup_5X2!v9K#~#Ik%Z@2RYCA%^Z0n=Q+2HJuGsb%{}TF=x6jYBZxz~Sf|K&&h{`n$a&74U`CPiY -!0(sBj-84kv&o(`VU(?a-Iu4+#3@Shj8CU&U1DL_fzCN`5M``~Kst*UMCZ@p{>;;`{e>f4~7ojeCb&a>IU9tkgl} -XahOVIXb@9A-Jt@RB;VE=urAWKfR*bhJJdL<0|Al=k{`b2RTn2bzPouX1^VLx+#N}1VB>?yDdey*SW0Sad3j`FzO5cv>}L$DEkZj^f?cEUHes}`W2?qM -xpFMRVj=aCv>p2g!FcEc~Icof^=`7#oRV4Uk^-mo8j;Q(7NHpI`a=eA)-{OmP;AMA*qyUCs$JL2cZ6@ -oZX^+xQBpX=gW0Cud;9pfr%?TcvuvLBjB)=(ks?VQfFNTfk3u?ZNZR!h$R14tWwy>|l -7WKJN9s#jIeRg|4Bj`Up+)J=Iy}6UUKendN@8jRW#`J}Q9PeOT`obXF8#bjcjB+LmThix_sAu5cZnDL -fBHzn)fX(R5i|QG#pXMc=wV@QRImGiHY(sBu=2#h<(3|btx3L9%en2sbeD9e0Hq3*!@Jb3cpU?J^7E- -KQ&Gz;%0{=F8h7t6i&AS*u96!955%{;uU5tqJk|T`3za6~Bi12S?jG+G<-@=HQ=Hh3KljGR1P$?D2z)=#n-k0`CWyk3d((I*~}vF|A@5YRd&iVD^VLF#@wcwdA4V@$%lZ< -6403by~d-y4UH`jG%j+>0<=LQ$HIjmb;omlZ?RqO-?YP3sK876zh;X8L^7R0@wV-TZ~x6V*P4iz4|cX -wGM8e*#hcB7&J~^VFZ)cljDqlwNvL9@vJS}VAI4R)ItaA4U13<2PsvNViD?_$;*tuDRrD>1e4b;?%r6 -0n&kz*6pK)^bm5>w(0jJ-UcWr`We(5vqBbZ4`@8O)i{B#p2Lu3dd5hICPr58y1K7wmpYn@!<^jA -1n?v&#)a}aj1Eotrv?!%?leC!RT{EJ%iDQ({?EqhnlyT9V`wtN7U~?KND=DSR9&P&sK%Sq4}NxMvy$E -n++X;C59aoc&1nanm@(*!4lB?8P*S$faWjp`(O!Z{t~wUOF;9(9Hd|gX#N`84wiuCCvPx<)QcKkumrR -~_wYFcq1Dmbj9`>G#(^4^faW$_XT)pa&!;wXcf=adeD8inbfH+8Vu|OQTO7b*iD$N(e>)MQ2zx^;@q9 -DMc8evR*)FyxEb+{Ct7kBZZ04{VOFXliIaC)*JZj_-OFVn_FoIEpQ&Oooa#;D}L@dFv2Nj16pBQEYQ} -@%Ij9}_ -tZQsu9JH;mXh>{E$*%DI>re4p&HA@QW7VkQ*m!?A8Q*+Npsr|GlFPh2Qw;`lJ>AS6iZ2J!XxjkWv`2+ -q`AX~7(q0lVty`2?mXhYiR12V=F`j5*DM`mBSW0TH=lB^*Njhr5Qd08>N6B? -jvd5lJEG2E?`2d!Z^n4LZNzEODj3AoW&9N|+lA61DM8Hx~b3gkxEG0GfA7up5gqpF6rKCfw1uP{sxg0 -meQc|;DeH)^Qe)i;8N@||ugoC)Z_JU#u`cW~ESV|gH>_9))`F*gIq~j(mB{j!6-o#Rpj)AbmGryJlDV -BKV``ALT#53Q|{Rc}t^QXC&V2Nk`3|k16cyx4xC7$^U9A{vON6((($;+en5o -*YX&IzGb^&-`V!DlGBn_)IMEjPv`*B_8(IVu`1lM?xLb{(Ry%s{v~~4&w0)i#&fm*~g)kxK#EsyDO~ne6yYhb*%D -yv!0Cvt2}=`y^-fQSmycj>0zEoV4dgBXH|j{3q1=H9gILrlN=?OoycW|{P1h9G2oebZ50Eam)BM^5aF -xTBQX&5jHl(@bqwfj8fAuJ{m-s_c7vfgSsUz}jTYyFGu40x@7v5Nt(l`nQP;C1rFAqKogzBtT)*T)zA40x8lILCnR1 -YcZW;AtO-o&tZ%a_^_UIRC{4hB5)QGt>a|`M7>ACr~|EfKRQ1Qb05YWXO*?Xq+J^XorTNqMaIol6Gkb -YTB(KC~A*}psKwZvOO31(J%JvPkU$OOj<$E)jRIb`3OVM$Bxf$Gn5N0DyR@FC`j -}h{sg<@GW~x6UGdwy~o^O2PHIKZDb37jM&5bE#hG6w8jxP{fe$57e`0;Bt -8B_y(&FVzS&(}SVIW9rX&({?FXDICXx{Cp?(62W#0J1qYK~2qL^@Jz;%QIh|WvBt@>pct=fFARUqT0n -h#jK&I#X+7o3MQBL<`LI~STsR3yec4O4|Of>b(udDseDa6zR@Ol`0^4%fq(`W5)?h|Vkm0Wh(?=utH+ -xd@&U@1=X5iw0#eOL1az698X(=mx`0#*QvsM4ifK&@}0jU(~A?i`Syr_N)b1?_0---pKehd1#q+S_wu`e&_--5mdbPJ)c0sR#8by+`^3P?2r`ns&Y5c(R{UziI>e -PJOWRwd?VUk*@I}(AQ1<6!djVL(tcV -ZanlgqMw4kM)gzB*QkEV>+A6b{Zt_!^;g$bL3xTj#k<{<7Vn6N!=SIxkL?Qy&Qt%`u -uZ&d}Pe#`6YaR-l*4NTk|#6)4@<{;I=R6vTT4AA3Fh8hA=@0ANky;mV1^5AVU=a4KXCbZ>49{<2b0Bp_+i$1gbJD9Uk=YGEND)xuIh+`>deqpF3 -d+;I@M5V;Q%`J#5mLEJ(V@0g$%)jK9AMmog>#j1sgfK&^iFC8zVg2zFsg{gp43o`+!7Q*}Lup$xBjeZ -K=R|g2F<1qmO3VBR`fJz=So+FQB#&guznDHD*CNrL+%Ek=8$UvFl7j-sf_^pZ2QoolRE!A(q`*thasL -9ciZKEbeOZ|mnv{bfHlcS}2FF9KBd!gXQxN}t8xUq;Ek3^b-xbdjDF~by!ZXCpouZz)AHNGxJOWk -sG1@Dp=yTkhx#e#OV8<1h-2pTsKhaIdSpn=oIVw^bNv+br6=yGoSmzu|Fm=RY2;upf5eAM`4bc)1xxSOx!auJJ&6QzVyUB6SH&u6!fJh?wOpOtEc -4boEzT|vvb8pL(b0mw@{*Err-@RJI7NL_b%@(JFW?0oK#Gp8pzDdQ4VBg<_(brP$PLmWC1iu7$@}{8z -n(zj*XfiGsi|zkQwAr733iHPbdpA6KT{1nTa&&g3Lr3bwOtQM_rJa5@W5!Oo>q$WTwO@4KkqtN`oB4u -Z-mzGw(%dkeT;lZO1_z8suVDslPB2kb19%fYf_IUpi{Qs*r=!d%^GO;kpp34(cgv1~;QC)|1TWTF7~W -dPM8hLJs1{poK#Q2@9IGV>sJm^Pr>hA(@&)WQctzcvK6hfY=!J!3TmCTDP$bV&A@7zF@RFUq53K -Guk~h1j}77u)N^{Qe>qT3>ak7y%bO}16^mmxRWyo?;%)>}Pt^pZo`Tj!^;4;U)KeHOhxJop?Lj|<(ej -#pst}NR3cYNApUTDB-T*(9i?zK=`l(Pr>M8WH%lfHUK$-lbDj@ZgjBeCZH36xoL@#64my5N%Yw -Y@Rv9@=OU0*KN_Efjc#oFFAo`L0UG8$D7^mSc9Vr}n+hM+HH^SM~tQ#PNAwY?GjRQ&xGHlK^Ny;1#C_ -4_SsJ`bc(aHSVPoNK#ny;y$+jW4N*^tbF3liDRGW9L_Hik4rk$IOc{k2sBnLn&7ErQHog|aXDNlLOP*HFm_WS -3HrWe|o0O10?Ul*60$r4=fMP)}9^0{J&lcli`l+=}Iz>#nuxYh@f^o|G1?^;xBWLD5&0Ly*==l_CTEw -P5Im8KcC4$_-;fi5%%QeF-@N{bWoazlvBQ88O{KERu}audNZtuae04*gTmMs2Y?JsJ>RLiCBi@(uZM4 -E`I0+)u>oNg8_luTCoJuY4j-;K{{PuVWXC79p+VNNkhMiT)8m)Q>Y7HxNp_vqP}($b-ApsRV*NTaSZ7p2Wb<+t_nh -a_XuIYvaMOxDmPl*=|id32WT8g=s_r@#Nh*C5{H$S5^j|M4`szH%rw;&}`HAqEp${l6WdFtMr@u;P~R -wls>SQ8D>E-;gBSND%BCGD#QW%Qd0Y`o~n{6!5ClT%BdNdFjpHe_1dfV5zuL6K@TzuAah@SY8kSTOI| -kf|W^Buz^6Z?bn*pxI!Vi~9cSk2`rIMlN!e?H0tVpLM-hEl}jhTRONy7v|(5bGbM!e|d#rNkFGwOUp( -6a?r)Vy`1vRDTmpq^Q}4Zn2&qi)1CUu>+-jiw_MC-MgUhlfB6;9e`>|jUz+*nE;MnWLF81V%BgysH4Ut+m`(eJT5c`pB7EU!J6|8JJp@%pN*DUdykW -I(nv(g3o9ksQb-MhYOC87YC#2Al#F$203UG9s*Q3nMszVEtA`A|TrsiGlPnQZbegd+LA0a!>t_S?+gL -ulRizBl*(CP-)TCDlNNO#dftyi!N31md0JM(xOXM{OfWTM5aVrf5pEr+j7PGJ+|eF_iJp+i%F9uY`G$ -AO#QO3F}7t?-Tn>9Q3?B>SnjJ}|1-;d1?=Cl+}qaQvD|Cnzp~tG;U8Jp?t%|7&^>h1 -d9{*3{z=l{TRujl{BaLNf){s$PqZ(L>ozj1{Du|a_W{P-XPcyp>sEkv!| -Fau&~ivbbKGJqeagJp__$g47d-?+tq*vH0zC~G4?)Ee -c#^nYNvA3y&i%e|-mPb~LQ%)fn?0q><>V!0pOzQ=O!p#L+={owYuEcd?o`z-ec@pmluAH4+)t~kEcd3Fu-u1{HI{pOt+U+o{{xnL{{IWhJ^%leV02{8M;bhCWjHZ!GuxuVlIB|EDbX{J*?p;J3_|Sd}Pxc!k^L_53Q!y`EoVx!3dSEcbeTgXLb& -t61)*r9Wf2cUEt*+)qhYv)uQyH7xi2Y%R;ZKYokkC^UGRd6|1HaXKl>5OeLv$_LK*pv_G6a&j`shs+;=pZ6qm8@X#bPtEjn7)0V*vzK&3?oXpyZbk*)ZDS?=xsf3w`%|NqBw&;RrpiaJXioj=Wo!UWSw>QKLfJW_}H -9poxIXckdof9_1jI3q~gwVz-F`e{GK2=vo_h7ste{Tw6EPx}Q%pr7`Oj0pVVlVCt-#%N`ME}{ui0D -6?8G(K}d=I9;aG9@|DzV>prh`2N@-Z_V+Zcfzbo4O-JLuTSi0G$#7!mz+KO>@_9%4lFQ#v5Ntc!knlo -4SE#~2ZIz+S8_?11}rUDyHl?Ygi7?%Q=?2i&*o!VcKK)rEez*Vl!9xYyT(el9U0^uzw3F7(6xpf2>oy -}mB`=>bMWKc(w5%G4$kkV%{BT+c`dgu5;ZXr{WE7F5qnb+$87!81$?>Sm_8nHChyOm*^rfQp%^PIey1 -^G$W~5RY1!sqU+cWI)*IqDp3}n+IQH2dBDOF{qE3;;V#9Bo3$e7MKzhD^s1UvP|fQona>Q)58e#!}oQ -Rna~e+Hl!%0IyW)`{dBTx&4hl~wPr#;92{jrKkV{QlQPxGE)NAMQ=RPcP>nLxxt$T{r*j7*&`&43$;= -$ETcUtus*4Fj^~h8gdnc5ROm%TogdFWu7yB&~jZAfMyo8F8sV??twp^sd7T2jR4z94nb&Bs9DzU+Js* -Bwp62wzo+|RMSb*hWqA9lA+b#Xt>t@FtRyWI72!R}>71iS1jbHOfm@LaIV9XuE8atF^XySZSO`+P3g< -vyPacDc{zCjVPvKk4t-q>wHD9a|LklKzg_E^uVf@7Y6QBkAwho{)L}9h)FFkxr`#Kw*|NOY9w;-pxY* -68O{I+*z@6bh?{6D>jZ!cXMaOzR~IKU5t3Mo!-l_#{Xa2mvqOCq}T5KD|)Fu_UV%VG6KNjEr0}Yj{ku --UX;6Rw})~MRqeKWrIDgWilXL`5+_l!L{amwd5gD_wvv}kW-__UpI}fd#SPpqzQtScoT`OzgUCR9_lx -1)Kmc!z{^eq~!>T#;qh7DaJ5_eBv7fqgt+VZ?GlAUOuST`M9diJ7u4S^`U!^4~_f=Nna=c$@=Nf-(w7 -u33lrw?$GJ$egpj;#C7X{is`s!tHIsVpYd+n>_N9B@0xlG1A$^*ZfbF#MAia@!q(nH+z6EFMh -KJ3G?DNdx$VEezS)O^WryqgfK6Dvr~k5@tZx`#bPghv(tom@tZwHm>0j<|687r)t)gn99sJw -=!ozuD7-dGVW_Ax!o|{XoCg5A@^xf=(?h0XEd(B-X%xkZ?YlM02HFupbu -f67O5azYl+)ctfzR%4Q=J9>*7GWOW=WY|`@qKQAFpuwZi-dW6pIai#e~4Sb%^xHj;4LgU)N=LwB#1D_`}t_^&i(6~16c|zm -bz~>2#YXhGrG_DPNp3t~9@Z(40TII)&#mIT -UK`Mbu-Ii5*bFo=3K@Vlw_zEgXxA1G(!TrjnPa#lV^7Wmz)C-0Z_Wcy`3ehFgdS|Q6Nf%^s4ELjq`Ut -leliLXoTT$Amb1==Muf#1#LS}7|6s*gFrbUUhQ0yyeb-5t_<`CWuRSlza~fJ0{K=N*?wgz^P@}y<#K*OW#?M -pcLQ2NSp>>;^S3j<+1n2WMZH0wT?YYvcqc$l&aCYx%o{7$4iM&zm1_qH^Tx`xLxg#AowdV+d2^k$BZP -T#owX^#yt&TWQNp~r&e}9#-dtzx7-8OAXYDv)-dtzx1YzD>XYC|m-dtzx6k*<6XYDj$-dty`yGHU(gl -@4jg!%ECCCrcCS;GAI%@O9u?;K%%{LT~R$L|7Re*7*H=Ev_6VSfBB6XwV73SoZyt`g?Q?;2r#{H_z`$ -L|JVe*C&?Ht&S!7Mmx`kKZlA{P^7_%#Ys!VSfA;3G?H(M3^7HWy1XUtq|tN?+#&p{8kC`<9C-ZKYsTJ -^W%4)Fh70|2=n9jkT5@f-L=%f*A&(s6XwV731NQxo)RXs?VQB2os%SR -zeM&EiS%gOYk}vG{d;>&^v6*mzDB)sP3RNtwZL=qL-&(B2zZzimXd!~&Xd|}@^{m~bEJ{%U(!g%EsbS-Q#rqgQmK1I0sS-D)f}muw=S4n*iTsGVF -w6{J?tQ1iH98`tnXom2{RscgfQ!2Q-q}+c9gKp!=?$#J?t1^g@+v{tn{!Ggbh6GBw>|@og%FEu+xM!9 -yZg(N;JJt*3Jm4wO#^0H~UOq}>or@{BCl;B^^EqK$U__OO>HUrV_VAnZ?DPcD2?dHY$?~L?KP2ycGpX#a;aE1waYcxF5c;hlMA -or35z`J7Gbf6-6kyYum!^U9=1rB@vtSrtcNWVmU`FRl*7nyGvNAHUax`SE*0m -><8lg!%D%N0=YK_k{WJ`_RP#{al@kd5I19EVhyFEZE(XIjCGF`-evA#a6ym+g_97v~A=(+ctrIGcf+9 -f$=R%8&SV3Fm7c7`3^)Dm``SvoM)GT-z}p^jH_k9?`W@ybwC+evA?UmCZ3~=~mw(rfTx| -beBE0eNOA^PqDc|t+|A$m23Y2+7W#T}Y$5bW>lzBpB`hhY}sZ2*3=*-Xd_Ok`;Z_7Ph?KLrnsUlD7xj -ywuw!5eIT%UR!z$^ZH85d9c`S~7!GELw<6gzLh(fNA<9Ebmr05?(iCj#6o;hza`Q-t3T;N}PaLV%kb{3`)&X7FzW -II8|T0gj_T5a7u99|Sl?{+0ko!{2oQybJbw0$e=)K!A(Kj|8}Q{6v6@#~%rB@%T>yTs;0nfQ!d}5#Zw -S-vqdL{Fwk34`L!-^&%$XJuhM+Uh*O);teliB3|zzCgR;LVj^DbA|~RkE@C2H=^`fLeJ)}mUgjbu;!Q -4MB3|PnCgMm%Vj|8|BqrieMPedORU{_jSVdwY&Q&BP;$TH$B2HE$CgNyCVj|8~Bqri;MPedOS0pClct -v6&&Q~NR;($eBB2HK&CgO-iVj|91BqrjJMPed8y+BOFmlueM`0xTT5#L=PCgQUT#6;)@#6*Y&0t# -6-vi#6)NX#6$=M#6+kB#6(C0#6;)=#6*Y##6&0q#6-vf#6)NU#6$=J#6+k8#6(B|#6;)-#6*Yyj*0$E -(eCZX0(L6k;!cc*%s`BX#z2fWV&U0db0H_PLscNMLsB5JLr);GLrfsDLrEaALq;I7Lqj04V*x{C$I6A -sjwK6`9qSY#I~FHIcC1K<>{x~n*>TPzksZf864`OeBat14JQCS)#v_p(M?4bQal#{YyEx#HNQmJTq>PSq)p^n5voasnR#F37~M4aeIOv -HhX#6+CuNKC|Wj>JTq=15G$t2M+#yjMd^#7i~AM7&W$OvLLn#6-MHLrlbrG{i)_MMF%)D>TGJygx%s# -LF|pM7%jeOvGz5#6-L^LrlaAGsHx^EkjJit1`qyyeC6U#7i>7M7$wGOvLLk#6-LsLrlbrF~mf?6+=wK -D>1}GybnW6#LF?EHh$YxgMl8WzGGYlfk`YU=i;P%;Eo8(J>>neRVDlKU1Utv5zru -E|;1>k!-hM$~rvfhIM9ET!&Xsrg1$Bv~P|z#y#0%;TArDe-2u+Yk3?Yz6jMFEmH^k8s)Ena538J_>_^ -I*^n;>@MqzPg-j+vm25NAvf#c{v{Q5>gB5XEt{1W_F4N)W|ys02|QCrS{-ahwEEywY^aO4BJTO{c6ho -wCw&%1YBID@~`YG@Y{2bjnK8DJxBr!cvetCUTGJ_OO{c6iowC++%39MYYfY!D -HJ!57bjn)ODQiuqtTmmo)^y5R(r%dve9(PM$;)9O{Z)$owCt%%0|;E8%?KdG@Y{1bjn84DH~0 -vY&4y+(R9j2(dcKh6T!XxV!+OCD-x%P6p7Qsio|JRDRKS`DAJ>e73tB$iu7n;$^3=@MSnE0qCc8g(H~8$=#M5=^ -hXmb`lE>z{n5mV{%B%Fe>Ab8Kbl0*A5Eg@k0w#{N0TV}qe&F~(IkrgXc9$#G>M`=nnckbO`_6R -n~g8mlOW1||nF5he#QF;#R$1CxW82$O@Dm@2BHNfp)6z~mq%!sH+(rivzMQbm(AsiH}mRM8|2Ob%jVs -tA)NRfI{CDZ-@56k*b2iZE$1DNJ^zgnRrAfVKjP0BSNt05zE+fSODZKusnE(4PTCm^3g`i1C@CP8ygg -#Q02+C{3nFlqORoN|P%RrO6eE(&UOnX>vuPG%!<$@wuW?nq1K-O|IyaCRcPylPfx<$rYW_ -!xuR2=T+t~_uIQAeP;^RDC_1Gn6rIu(icVeI2r70Di(v*r$X-Y+>G^L_bno`jzO{wUVrc`uFQz|;8DHWa4l!{JiN=2tMrJ -_@sQqd_*spynupy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlpy-rlp -y-rlpy-rlpy`yArc+j$PFZO>Wu@tqm8Mfxnoe0M`zcMOiItTmR#uu=S!rTrrR=9Pm9n4GRGO4oX;NmT -Ntv}KW!9RMS!+^etx1`+CS}&jeo9kodS)&4%+6eKkG~(#RzTBDYfU$;HQlt1S}rl~biv(`k --MiVs~P1I~OQM1uR%|;V78%@+~G*PqBM9oGMH5*OTY&21`(L~Kg6Ez!6)NC|Svr$A%+fzhM+fzhM+fz -hM+fzhM+fzhM+fzhM+fzhM+fzhM+fzhM+fzhM+fz1G+Mc3p+Mc3p+Mc3p+Mc3p+Mcqh(ngBBX(L76w2 ->lj+DMT%ZKTMXHc~cK+DOqjZKQ0fw2`u@(niXrN*gKCsErh9)JBRlY9mD&wUHu?+DMT`ZLCP6HdZ!O+ -E~#?ZLH{{Hdgde8!P&#jTL>=#)>{_V?`gev7(RKSkXsqtmvaQR`gLDEBdI76@Apkiau%+MIW_^qL11{ -(MN5f=%Y4K^ii8A`lwA5ebgq3K57$1AGL|1kJ?1hM{T0$qc&0WQJX0Gs7(}o)Fz5PYWs>lYWs>lYWs> -lYWs>lYWs>lYWs>lYWs>lYWs>lYWs>lYWs>lYWs>lYWq?j?aUST_*)EZ1r#0D_7xr0_7xr0_7xr08by -b-M$uudQFK^qlntWRDEg>1iau(MqK{gm=%dys`lvOEK5C7kk6NSXqt+<;s5Oc{YK@|gTBGQr)++j_wT -eD!t)h=wtLUTFD*C9kiau(sqK{gu=%dyu`lz*vK5DI^k6Nqfqt+_=sI`hdYOSJ=TC3=zHdXXdn=1OKO -%;9AriwmlQ$-)OsiKeCRMAIms_3IORrFDtD*C8R6@ApEiau&nMIW`PqL12C(MN5n=%Y4M^ii8B`l!tm -ebi=(K58>XAGMjHkJ?PpM{TC)qc&6YQJX3HsLd38)Mko4YBNP2wV9%i+Dy?$ZKmj>Hdpjfn=AUL%@uv -r=88UQb44GuxuTEST+v5uuIQsSSM*VvEBdI-6@Ap^iau&{MIW`fqL12K(MN5r=%cn!^if+V`lu}webg -3;K5F6T{NE>pxPbh>7lg>2oEDzVN4}41^p8&oajpJwjS$!DADb` -f|tAKj^5of+dEf9;{IfeZ4-*Hkwb<&SR&abf=WmJk=`kM9U^asK$85SMu$KM>+l@6&!l91lJnAS74Y+ -VSAiAwnDvJ{=~+@!-=DLL3i1O%dXF@aZTajt8Hny9hj^Psa#x@%hU%-43=9Cj)#VP6il9oD6V|I2m9a -aWcR=;$(n%#K|yF0|fi%<10dts6j_92p{JuN^fj2|Lt3g?GNn!@@alBV!}h@>gZA0lZA_lHQD! -u}zWrtp7=q$vy_B54W-h^Uyt=^-knuzHA!DZCz{VhXc|sF=d-Au6V@dx(lD{2uPh(^gd{3VXI -1_(FN3f>Zv2j-G!xIznEq=q}v(lL#+bW9^H9n(ln$5>6rSWU-RO~+VG$5>6rSWU-RO~+VG$5>6rSWU- -RO~+V`Ppqb6tj#x4I8L0Xv6`rfny87IsEL}WiJGX1ny87FsC+vFh)4?WiAf6ciAf6giAf6kiAf6oiAf -3ribPGRD ->VIRD>bKRD>fW#tftOV=#p{+;D|BsIZ0jf#3^qlNvP)Mokl=rioG0#HeXv)HE?_niw@rjG87!O%tQ0i -B;3Ys%c`?G_h)$ST#+o8Xv2siB;zT@QOHrteQYpO(3f#kW~}NstIJ(1hQTN(d}RwF-hSXF-c(?F-hSY -F-c(@F-hSZF-c(^F-cQ3KroM(q;QXzq_B^er0|bG($180k3Z7V)&Pl0$pEK{lL1y0Cj-1HPKHbqI8wM -(OnJCcOnKN+OnLZHOnDenOnEp{OnF#SOnG=yV$8_Y{u1^S(+d6+lPA;k*c1+xXi_x{xtfl-nvS`ej=7 -qSxtfl-nvS`ej=7qSxtfl-nvS`ej(H#*e@4_a$<;K;)ilY~_!MfI6zY5ft`#Rxp(aqFCQzX!P@yJJp( -aqFCQ#ue5Z?{~B9eN>7Ln95wuq#ju|*{Hj4dLmXKWEkJ!6YVTB-r^3@0L~XE+f_J;RAe>KRT%QqOQAl -EQFelEQFelEQFel9sCQ;2BOtMbB0uDtfjOQL)tI^3=1Hh&!;An2K9OfR^Zn9+8V1-(pRN-k&(=#}4)q9dtsHd$SVJ)Zu8k7{Tod;Ra4l>IaP4aeaLt<%;955$z%_18K&Cu -7ydr+`LVNO3d-8$y~V`+eoLtcoZ4Fy@_R%@DazG>Z*aBO#DP5RHSyiux -)+J>2esEk{pwyUzW3H%6W^SJEQgP -x8l0xV)-4~I&S3sV)-4~diNVo-EslG)Y(9`Up5fm1+KG7#y5x6Ik~ZTo3O~k76^+yY>}|U! -)SRZ|NXmUi_905hlLFUgv!=Zq&K_er0=2_CL9i{aJ21{f|GVOZWIIG4&u7<5)c?Wj|N-WVyii+3Onp+ -~*ZSTq}RRLx{_f&-V#&8S?oNA>tVC_L|F!FVlp$toZzt5SJ659}wa);`0kaTt0j`PKe8fFNX+mx$xyM -AubcX93jNz!IvpQTo!y@BgEyv=Wd4A=)t}mAOw+FC&WeO%RxdAnJ0v}p6l~7LR@@4-z5a`xkm`%^NMA3rth}CR})oh5>Y>3rtNYrdd)NDx9Y)Arao~U_{sCkg6d61}ikOX3qs2L!SP; -IZN+)w04=Y4^mnb!}rLSN-lKTz+!%B8^C!22FNvnVh^dtZTP{`6J;^i}=@)*#+@-ac5?jKnoj{8G+e~_r-Q*h6%K=QTsy^m?;>wC$xdKwQ5gjRX*B4+gi0Jw1Kv@DtoOuJFqH; -ZJyh+u(k>9~?uc>*Hsy%M1_PD8nxUxm>7RE&{8~LS=EGvb -a!LT&OHAR2CNj78fdu3zfx%%Hl$0aiOxfR9RfAEG|_Rmnw@(mBppX;!#wnDvRZ}) -#|#Ds#4tuwZ93t3{5W<_iK7moo{-%;C9n10_6%ZmNdOms6tI|AZBmvwLrNlP_7D;Ys8z^P4rbuN_JYY -_cw&Z-;jur3d6Jwq-!`~Hz37oL6p#E^^6zJF%O1!&)I7;+KX_b&{&rrY-!jxV=ywFgh}Z8KLcIRK5aRVCLx|T;3?W{BWC-#4Plgb$KQV-O{TD-s*M -BpFc>S3n*E?_QKlkIt{__O8F8bF5xDoUZ1h@h8*WVL@gZ}uG`TVLdGnKCGt&!iV*=K=`npCc+y%za=1iSWgq-eV*SF5I(G@ -1;U5*v_SZ&u{qGT|jM!y3GX!zhFLGDL}GbaghoZdKelpx3I&DZ=_j@QG*D+D=ak5?85O7T~#1Z -DWGMS>i^N0;Uaa)jABe2XB*n9+sH1UbqKkDVgOk$m``AK9^dIK_X};I|%kAz0SK>FWf!cx|m8B*?{U> -)cI(T)f6lT^n{LpnLqOnYId!7sCa9T=3$|D}o#^MsLp%sW;arVQQf^_EFkYuN#5`eiY>puCV)+0;ju%_2uL(lD&P)^J;x&3rZHO7e=o+; -l<_x22d_xznt#i~Gm^TcMJtfG+Yk1)~L5SB8N;_$6H8Do}c3L>PcAqDPF-HBiK+6tUAodHqJ}C<%UV- -7mz#aNtfvtnDWC7z>^4Od#VEsE>y(kN$ejQF+mjyEaC(Z}%Aoe9TG2XcyzNZlcw%Bmy5kXSRh^-vmV= -Fa)B%pch9x>D{w|hztm8*q{-dL%%G`q*0bjziI-`#zYk;-L(a+PekG*G|Y9m?HufgOqMwLrNjP)>TSO -%|9%Wzqv|vOqktEO5U(P%bbF%E~~wQt}`VJa3-L_RGKeD^yOtOVM7FbGpLFxD|otEv$TwA`O&Fg@@Q) -6FYm`Yr=081(9T5}xS!lP+g_8?$D#_9lRI6TG7z^im-j2b>W)-yAls$HPWm1 -Ju3w1=aNTfBfa`t}0$jJ-C%|>Lh5*;iS^`}6niAl;)rDiKkPi`_HQI^_ -!48^-1CDItH?y(ad1$7L3Y`;}QN$_;iMo4@N`6YP%bvgWVE#y|I(KhM725flM>NswdE=Bon)VbkvX;o -F2^)$?wiAV;Ll_YVoOfZh;~`bq4T7*&cm5LB1Jm2i{ -A&kl0G;bXSf&n-EE0s7#E};SVW0K`4fa8WK$OjLdd50$*>@K -bCMv0Z0bHi2-(y+LD*ltdh{k?Sg9W6KSRi-4-w=-wlPOH^jE6HV60S!$EFB^ve22-??4G43N%)b8|Tg -wgovFuN{|I~j35N;1Vsfa$l(d%Hdc_slSc?bz)tcbBVZ?q9Nr4@6xR^(IyFlW;&ti@K}fMvYh4H{$l) -pK6tRLFp87x#UP+k0LrlbCaO=Qhg3$Cc#|c8y&k&`s7@W*MSRf2-KXZ>DwEfHzg3$J7C@j#DhuptGO& -*@1K>~X6aCVv?wEgUzE`(L)@GK1T+(36MPD2*T}4=+=$Ku{iDe%6J&)$26%9Ny}6fd+2u>KxuYO%Q@| -lYa|AnWu3D$3_ir(6EAYqlWX;Sz-g|aGp4ilcR>WDPyq_baHEs`aK*THC*5yL%*`&?K#dh@c$`-?ymOTxP~#5GMsQ-)aODy~pvL -MlLC|`x5_v5z;dibfhwoF527hIE|2;vNYTR1kKSMws(R>1XNQaO3i4c%S)DGB1I($UkOzOqsKHU);Nr -#VUSi|vD!^cD?A?xg1Lm(a#S+JLM;|8r7u$grDgjNPPoND-lVuVel!?pbcAs$bu7s0mD$*mWZ+1OY*T -&H;8=&1J3h)39AI$Y;EB3A1(Bw(lMaQ!Vo=v80bA_$>+K@E(}rW^B*2tw+-peYTun+{)63S+^>d7ruhQ4pAO&9qzn5`CwEVu?PBmahi@p -$u?2N<`y>rB*lIeSCgNkS>F_PJ6}FUaEYP5fJ*AU{<23EUuF}c!QR?KeuXOm1e+>!qj(Cclr5g)0O~M -gU!}t7KX#EeAfjC5J_<`mpI7Mpsf!3lpMrw4BS`oWhHx_9jg6*sui?r^=e%8@JT4duWsmblDjKSDBL~ -+5+*3l8_>9MtSbc9y0*xNcfLh~eSZk2yk0ODtcK|94a8~~!J3%0qAc=?TeuA?dHSg_G`bd=Tx*y%btO -237zuA`%rn%L_)nx?5dHoK0dX=;GouA^hu2|~P%Q(|Jj>*xeU85>?lr??u(u-o@3D%kTnI!QeyHocBc -Q2&NqucK4c1>>V8qtn!3;`pi23-{TOfjXQ4i>&$ug_QN< -kRRA+)xprX2EcMwq?rLNR5(6sbd6?pI6!N3g* -tN_p*24EfFKvJ%^7-R9HTYjl`;;}8eOB#21jX)uF>oahiQ$jQ+J5tv_{uSh`@nblZ6?|a-6BP@ql89Q -?*7nX$Zi%TB943$v9bSbc>>bv$aOI`LCesFU(Q{1&3gbR%j`TW3Wbdsqe)>Sd)b-d`G0%J)#Fr!`gUE>nxmyHM&pCfsZ#?xJ|1%Sb3ud)KoC -@Hl9%1!_FH$pz#-m-smBP1(x3EA(0BE-smCe*o`+My-U+QID3;@&q%j{xi?w3$3KUliATd4Zs92nrZ_ -HZ^pw`JI52Cp#+Aer=Lxqyg7&N{!g#j1g#22(7W@QHy0@R -;*{K6M-^KZJR6nrd)N6vkiqn*m-YX!l?-NwwuZR@z=r&$c4+SG`YnE~xM%>mcaRNr%*6b645dX78KyN -GO9OVeMa&Ek(q=S98b#9R$94NN<_&H$~e?}uf;)UhuS;7#O^N$JgHL{z8AzLpIwc+1Q7T@0@Y~W!O88 -~>8r2~fv!%XJGTY_BFHuqB^!NS|RO2Y~)yvfob8X@4}wb)a_khe>RFB8`HFuo@e_Zs!T*a*6HjanHt- -qv*@5Ny0n?(1RWZIL^b!^Yb@K!XQtyv>8u*Y~~1oViXI^63_hRPgb(Zg9n*&d11V -u-CZFpFvP(@+nhc~ke4hE>2YD8Z9O7J!9d%3bc`Tuz?!DISuY|_sg>ZMZLLv11_y2P_&h<#qBR=4VWe -%X5!qm*Z9U^^pod#}&kv3H$U5HN(H*EU%`!5Cupj>dhMZIk7bG-knV+j>tk9k^{CgET6^ZyO&zL=d8Oh6V%pZR5k#`kvo*mnOBD7qlCc -yP0SC+~B|S7TBlf2=j!;(=_#h<;I>>4$EykO%n!KZsYfEyzNYe_x9sIJ5_M(*qo(K6V~4NI4vgO?2V6 -8yx{DOkJBiO(`AOoY3AX1e&jmj@chQ&ufUDtw1$HnI6gsxuxAG@->2~icHsB~P1oTDj!*J~A)!xEV!{ -m^pQZ&P+`#ck{womUG(VUN*m#DB4L5K++m&Z?xPjwSL~FQ#W7<)d!wnqI&;$r>;CO~|!E*zbdH!E`eJ -~mPIqbm6@)~t3@B=r`@t+}Nb3`5Zf#W%%4*bCJIhu*V51g!s7D3F;)3OJK;AHtNtpec)Zk{LZz!BU$P -wNmkf|KR<^k}dICo6|&mH|(2^E@>cJi+m0S`on$9ABo0!V?@{CeFbV9A72{4xZrn3PXt56?Kgim(F5^E5QT3f#O%wT4AEzC{B%EV}V -6>NjA~O;$Kk@aV=1)H}eV8!ynh4<6m-B^oQ?(Tx{qh69goI;0j$(@VT>%pjNF$yT0x -^3FKlEbOnK16*ijJoZ^Gy#H9H@S0-SPZ9b`!GcfPTlr2Kbni$_6bT)ICa}6E)nEnwtb8O1E+3#hB_BG -b=zl%#4ze6cOFqz!>QZ8Km>tPx4BGrgj2VDftcEOAzKgc|J~cq5be|feeeo(IdBBGuhN1Aj^OY)Epy- -qZeOGB8IIs&m3;uu5?r08l^-m@%~c{B9Kp?1nrp!j+`dVDpJxcJo}nHNhT!I1el(C{^(@Vr;0SKdcga -z}_S>FcB?t^yz3`kcB<;d&f?Q-a@2?Srq+R46Lu8g$2tv-T93%)iyYikO#O2Xxg3!^f-hWIO0(1W?K} -gys`w2qQuDv4&W8)gv17qXb1A<)6Zm&~wc(&i&Lv%;jew%AA34&;I_wW(IFgo6y=38RLaF>>v1&qJ#* -KY{ItB&gz2tv@_ydnr4E$vDzVEk>r4Sy2kk3ap`x8HyF(|5o4Kfm~Cr@P$u75Uob_V)eq>p%W__n%SspKZDw)i0*{#ZX%Uc5)L -fYFQNJ+RKJAkmr(r@s$W9&>r?&uRKGseug}b*`t_-PeX3ud>er|G^{IY_>Sw5ahU#aieuh(n>Sw5ahU -#aieunC2seYE~XQ_Uc>Sw8bmUEZtXQ_Uc>Sw8bDb+8f`lVFAlOVaA|NaUv_0~WN&gWZF6UE -VPk7AUtei%X>?y-E^v9BlTmNeFc8Pz{V7h_OHu_7@q{Y%0YL=_0oB$$Ob9hja_PJ}cI0!{_1ojvPANl -zK^~Gj-}&#~oi7*Am{#iT9QLkz@tSqk^uRX{Xh|9kvLB3fklYl-#|8)IeVXF4Q^ZY6%SX394p_ok_DD -|j17j&=@h{j^!#TrF3(>knVQWCa*IZr}-KBXwuKz5EV~1-zOw@K -1#3gi^I=b`#0U+h|MJ>%m)f2{*w&%TN)d;&~-vGw_1IvnrC!%e8+KBvC&NOvV -9AwlKm-imj^i3$#Ps84DBKhsV~|Igr<5ewtL6*kSlA4+SzxWALse*bFo;=JZGPx+X4R+BIJ0)&!+f^= -j0K{#1UCEanH6Y^uJ@Scpm?1iN}p)Lp6>=bgQjA;FdQ8;*Xa%FJE?La&u{KZZ2?nD@!dZ&dkqKuvO47) -KM_dQ83cv0sv4;0|XQR000O8`*~JVJ3F1ztpNZ4IRpRzApigXaA|NaUv_0~WN&gWZF6UEVPk7AWq4y{ -aCB*JZgVbhd5u%cPQx$|yyq)c<&sLJAArOK5u{!y5`}AJ;x(~w?8=Xp{yl3u4^u!Q2gmWu&W?9Ctzyo ->i$1XSqxo#{;HA;^v|HX(K+j^axML2XO>AK-IMXBGNONGBbjabW -fUn)epp)*xTT@;+}c-AM%5-J@ZX8RlhP7u*v>@4<%C@ePl{<=-qp<==l+h-pm|F5_+1~k&uc~-fCd -puaA3L&RTzEBfpov}z&mMehUf;D+*KD}i{EoRD`f8u~FMg%p)eq%f&Tq>Fr;KUlnW!Dif4~M*%E~C|% -7QA+y@lb18oVUn&l36B!JZVL<@`8H92D2BU+)%X@tLhS6@&|0|X -QR000O8`*~JViT|rS00#g7$QJ+r8UO$QaA|NaUv_0~WN&gWZF6UEVPk7AWq5QhaCxOzdvDt|5dYtw;^ -tz5lscO11Zi!=#fCnH4qK37$=0DL6xyb3ZL;KwR2rx2KKt%Si6Sl8aXTRoo4WV??vCUmm}av$U5ucR^ -W&$Td7iCsDM|QL##t&LUahlSLdsTrCX02#JLSrZNiO4+`7SoXe3W@Hm7PxK^3MT`=}mCUbAj@rIGtzj -!uK#9!=TfN=YS-m4&W1~GLoA$7s%n^mmVZpy71MzsE|`!zQ|JE;fdf)4*aavWWLcaEw;+ifd1U0i~!d -a5x<-AwS*s~%;QNR`O7@d@`w37XJy6{$-L0_(cx*rgn*ZK@(S(p>Y#bua;@8gE=HV_P}cQ*t*qOv-b2 -pUITt*Y01|iTs@*cCvN$t%n+y0}_<+2k)Qi8^2Ajh!*MYbV^v5tN%#m}WGbB*R(~F2iUgl#DY`<^QkG`r_n-=@78gY#$)t!Pti5;Ll7< -S$V(GrO%OFl;iD?u(nfEFsJzK*#v|x#fUL!-ylK7qjvofZWTzW;%Xw7oABKTKgT_|1@s|n8`n;X|K{! -^}iaDDOmE%0?VU4j>eBlQEx;!zUr;I!+4#31{m{f*&{HB!b|gG+hZ4;7#~E%eO=1 -E6ABrLgX=FoHoII4C_q9YAF?gozOkK$>71vi-%fNrLO8Im@nkz`GV(ryPy^0FVJRZW=mtMLQi!^o9i6 -`vXxRz=n3y$gg=?JI`6Hh1H)F -D&T&u?mPNryn3KuLe~aRR6|r$uLz&83#qBrQ52POe~v_D%t6!=j~6lUyA!d5bCDdLZhG_qu#{z>RQiF -^^U3vyRbb>o73l-CNP1+n&wfbKZVx6t9xNPsx5I?y9ZwuBPhBmv13W -uzQSzo$Nv)t{{0_pqP(LTH%4d&RCD*fOaB{lK!v0{iOG0l?R9=5j7F$xQqf_-9Fd^!UD{xG)|7U5UHY -L-XGg+nnWYB}NQ!M8khG)leFcG^3MOk)@w(PWIt9+}SYs*BqB;ZNfiZ(#1icRqyT3s!*Rn~w?m -{njX;N{z^*17^uI>GZBwV>A|yrwwUmRd7hPXrP{s>j*bXEB7-_$CxeD4b#yc^w@P1Ibuxo2-4QHo^X?WA -3p()*#JUnZJydhN;=XMTyPVgILLV_NgJw+PddGsejFwzWtE(7dP?B6gnQ -hGFtZ&%r@a;Rre(9Em${NUsg3oY$&cUOM5SQb0);hxh6xN;7#F_$vlI>*YP&X@6y~z6{z -y-QQ<2sH26*{tr+~0|XQR000O8`*~JVtE}ctBpm<%ij)8V9{>OVaA|NaUv_0~WN&gWZF6UEVPk7AW?^ -h>Vqs%zE^vA6J!^B@Mv~w4D<(|kLMjwPQnD?pLT~9At4@BbEM1by`5=%SN{C2+3xJlCtNZWQJu{d$Kv -0y=&h4Vg76J71boX@k^t>94M&l%$PKV*>mdqC^CGQ{dnn`oV`GvgG -cpcynnyE=ut}P$`!FNH5!_tB&4zfHJ!JA{o-RiD#>U3`-}+>LZ#ti7nn5)|6i<@hyC2D5K>8cegaE|e -27xPz^B{ki(+mnU;igTdNqpVfU@ymXLdMZ352Bo=@Nbqi$1PqCuaxGCG$!MYdJP3P=rp4)a`?@jE#hd -Dj49HWr|ijn8phZ7fc;&$W>G6JWiw2EBF*hKc_s>eOD?ac599`9-RX4LqjNf1F!6MR^M^o}bwdjIL!L(Q_31lckYv;3j -sRirAKpK-jW?4pgO=?O@;bSrfM}1we4mlzE;bMyETZX{rm{?y^QL2#5(JEppsP!-0~W=j4>ZGaadm+r@SH;q>j#!H0`;_;YZ1dUkYt4*JLCjqgX(Fw3O -Kn#u4FI?A;MVr>YbkMbaBW^_6M-5CrU4Gn~uAee>u1TTayXoOk~>YAvzS*ul9s1wfTG#&$-0j5m9c5T -Ad6fE7LwHN%jyl6%PYB-~_Bz^c4(YuzXR&+6)JQ-rMHf=f`TnHAXY4}iriledkEfI;q(KXFKh0_IR%0 -e!z%s|8bT8`AOEYQDXR2f5*7@7=r7AgheH0kyM2&Um|I1Wh`{Ym%8?;kfr|I^`m4yLVaV=9`xA(`v>6=ueKH0))3j&50}7l12=9tzEo-ZH<7_k3^LV!Brt7hIpN5|eczL)_3HUNQ}4cFdWI6zep25J~UAQ1-G9>I{cCxhbU(TIQ(Wl -`*1K^s2ceJ!X8`33~2)fz?_CI5jc&RV?L7=>}1H* -bA;DG-U&Rq1y(j8T)q)?Yh7)|MTPir*zIbRgoC@@-4A$U>E$fBj8lcGR#gT{-(tQ2F{Nggl_L=$bcY* -wWHHbEk7y&N^l8Ab(v%H!gGo4ja~=Pf-Vn4pTMInd{nrboA47Nyy^hzaWigRf$N0voiIeaPC@W#Ai`IZTo%(1K{Qd{1W<8ip2p7Cw!zhzk5vG5W<)N<_?G-GPG3atf05fjXHO8hdC-!@K2V0FZ`SbM -nim0CfX!Fb^2BVteDri(FouZ0!;ni)RIiEw^#zJnvC~040qBcTUxZFz -v;o7$utRbLYg0d${3W3DHAY7j^DpKd~HUoeSk;GNb_wNCs~aW+fbsIf#}Go2-3W~IBdcAO$e%A=xMf? -W0Z$olG9c^A_`{VJh-I~PO~XVf)|LV%%6}X#;8jP^l;3i3 -KRfWG`f2vt0SYCFMET&`ZEsLT`LkiErUS$Z)^dMwY6hIjFA|#x^kXCiCQu}~Vg{z6(Ed=ffgx%N${gE -YctCIA27^T@~VgT(C=b%7Ys%sI3FNE2?~L~727WKsNihylba6T`YjlN>K@2B#U_YPF!RU0#Q2)@3#P_+#H+2}GbzDc8 -b!aLxcb#?ErCkMBeDKteG@v^SyE|dRJpy;m{`eG{5Kq9)V!B)>g&U+8%xpd^t8{vGN=fYpg=PhG;!Q>JZ5gFa -H?9hN2kOpsx#)ghbs)8R0n|2P+l2PZx;)9ftGnFqqMG;*#&C#W2+MLf_pn%0Jj5Q#IF*5`Zdc@lBOs^ -{@28y9dKKQ)21o?JP3`4P_W;rB_J6X7|jwkxzff`EC2^K -ZYhP)Zi-qABK3Q0Zk)+2qZ%qnHwaqx6)k)mgW4c -rFec}V^;#Y9SmQ@S2aYgjG@(*pTl0DUdBSI8pujluXqXOVXofJY(qRQ&)=ONtAe)$_!>QOwL91t0 -nIhf+(Bh{@eNCqz?tE%)8a==Bz}Y4(?>(@l(TY0{oN15Z&Q&=WXa&h+l2O;sskQLV?#MMU_}X9+kva6XHan4HubK1;0(4a^MN!-1BP -eQMHYRc4Fg-t_A63ag;gyTQUU0bjU3^zu{GZUgHd0=f>sT6m}L~V`{`k?d?^<4{pRPt;r2OP3q -lGbjQxRuu#GmWbXh7$)PrE$k}@wstRaQy6hbT&7>Os?FJU;3A%90F)*DT|B3+Y!F`XKb{qK9;f*(Bt3 -BmK$z+1v!pfs}k7FXlY>r&7;2xjRXIeq*7Z2#~$*grZVKMWw=rdj+whKLb&-*W8BPXg=nJ6hU+Zok;xAqrPmET(}r0yp(rNYhCtJItw90;R -FA{a#y!SV%>UA&=BWgezRA2tQ#1v$HkiD6r18fcTYxruLibX;yi7*4o&!R-wchE(o%oK-L4$q{u6uMb -6`YG`j=_LmVPgR_HzJ7F&|)S0#y5reklRTdXp -Z#)xk#hlm=167&DgeIY_fiPeJ*)pSU$LxLrBl^K?dko7gYpyhhE;e*AZ?oJEr0WcB;6I3lxC^28bhOC -nK;Lz$`48xgacb1qP%2->Oznz02r1znL(~%g+EY><+CEa}t;9|KFfb>2hUDK!ci8Ll3#TwFCFKMSwK- -i?s@GGc+!j)oh$i#v;V%DOK0u6HjkpDk -3;dw;SX85KlECFVbKP7-v_R?06Iy^!S;gNox2Sg~#V_ye;HxsHojSq9kh} -@*Qa!Z6sc{RyIa1wr7tNTi+F%u_w0i>UE=SNV07Rto3`hTAKa8qBNgBQkpwUrFpuZG6sx`XRl9fv5JyXtY>W(?J}{Rm56m$inaIradO?RA=g*0*AeXYUs1 -3xrC_)Fj}z>(I)Xi2N3hTSih|wu4Q)z%Cr4_DZ1Pmz(;(}himcrlWR2G&Yu8WKVHH`u@+Md#;4$hys; -a+VLo81gIT&Oeud75))&(16ovf=wfBzgxbktp0iA=JV_^4%6`Bm$vx2|g4d=Aw*>aVL>(dSUDqwRH7> -mQ#*wRTuHIf?FB2vWDBl=G_k()LfJ_XJ&4Z+TZ$J@{$+O`w-ONoZ*Z@0l*V+EGS{@cK_6y#A`f+pZ_P -{yM_j_6zU1F1(#Jg}3tr!rNI@c+ctyZ)Y9hJ@X6ig)Y3>1F{m~?LL9FdnyeTAYtc%& -$I>WFg5&|;mby{}M|hoz!)mvg=@z5D`eNnB({j~3aN*1s<%vdZ!PmsX_zo8>x7RrEzfLsXS>5MNq>z5 --F%r@MH~+KEU^nsXZ`cQN7HCH6q7-`!)FHFUOUK`m_&Iyw1dYHzb>tKdsH++weWC~od55f8D&eJR46< -7+64Z*(71rvZ-d=X8X}+?jD&C)5Q0@e-<3ezHoM0Bj|Spv9Nb^DVh}cl53}-+xEZNM=zS0)4p2Q>HQc -o6Z4KnsPZgJYwcHmvB6G7U9lSp5U_?dyWWdB(l9+c+88Pkkw9R;TCD4ZL{|x;Tj_}PLSlLFciD*tVf` -5dSbk$WkPlM04y^9pGAq%c4r?K2lbvv@Kzx$Ll!YNOls1^)NePK-3m_?j@X&S;#i@6Vga{YT@}Y>u1I -qGH=jCT)skF!-UfXLPt#*kVJVQK)L%k?J!WLnR*KZMh -iG#b<%%wHvap6ye%BmXkmVEtekppAt`k01R82Q%aDi%&zSImfTlQMR&0j!)4`ary>TfdUf6q!CI&4`? -zGHBP(AjgKwVRkCefRq~ -ZDY7r<1(jWS*GQtV8rXVDG`R~8ODlBY^`Jm0A!}j;_}-8F9QE}Sdt^!Y%KCgu-j0Esh@K6a*Xj+3H=A -BZ+-0gNVG~G|9asb^cM$W*^zE<;xW8(-1#i?l6Ck=Bc8}l)>@$)+R@2@Z_z{JBF9X=D53kNi;%_r9+a -M?058fQ4&!76yxTSK7KzflXnopYV5sL+)oGRDSQ&0r@`+yT|JGaG-OQ}%n2O<90(OyM+J9C?2WIEX9A -G1*rzlqwso-Y%EdrX#|N3poa93Z^*A`PXMg~OAq@O#X5GGb(SvQ^?hCyV0iDGg -YLO+^YG>Ev$+<~fVl#}{}OPDTjEcInq -tW(GjhdT|CN(8)yzp-r1%5rO<0O2mZ9xRq{+X+=hpxu@!|-v!w3{_M^hT6#qm(T*l?39O2QP`uxO|VQ -=pC)BM67#QIVDr(^-;!r_01=(z`K5){wD8YgE3*IVF&;s7G_$G|YKgphYaxsd8=+9yq=W(=n$ -=FsvlCKOVCbMeHm)8=aR|p0R8QnwVKWO`eu$vY3jTJLvLEFA6GLMP_(?wny_R?+2{&aWIr!Wi|7n3bnIkc -RAvuCGQpEbtE97Ofl4j`RQ~p$D>xPyi(3m)L_Gub=_6y}qL}M)`wzU_wG#C$HOm8|K^5-VN$Mm^iF{S -Xy$!b{Qx&Gvl8G5LCW;Ktr88o7ewiXKdfDobmSU#BU6EQ*@F)NW1v#2EuzIiz!jx@;Qc -s`f@D$DK%GFh-sqpq8Mt53ar9Zx9I(74_L)n2?$-+UWTjBhsmXw1DF-*_?4XWs;FoOtvm0A+~XtS4e{H*Eap{iy4rVk)i40pfEc%w8G?4Yq#mpYq)5-uLy&)~ab(RTJ_dD;ls<`|GiVeBl|K -YVLNQjJsFt{30IMsO0kG1ZvWlT$9aL2d_DwN6Yx^YMgGZtu?O>ZaBBSCEU9FJ^|0Coaf`^Ja=8b?X&+ -Y*B)`s`%5`5DT@FBANh5NZt -pT{r@DNsMd;tSSWJ-@%?dA(3;@+9zme-^LuWnh6F%j3^wbvSvs@NzSL@%=5B3a|!nX=X6I&V+TC2?2| -Xoxd+%&@YMN$2%U)XAAIW6-)VL$evXq^PF<4LiW58ndj736|xtV$b5&Os<7>rW2=n(mm8Rf`Cqijna5 -7eIv!t(h`4vlYALgj+HvhmKju2)E7qshD(bfb|3*N~$*2i~RU;U!M=+MpJ$*vmU6cwXR{4AMv8_)@Y? -0n07-Zk(TQcLon7~zdwzJW{o34-p$H;c1u-;faH0g9OXj{ewiPjt+hR+(+!4bT`4xm-!iSDQcr1Gd*< -nl5%L&;r0-e$DoYL|2wQ2Qna*BJuT4*%Og&%`o2znY{Y+y}4ESxX+=8AZ=(J%e_V-ge}7-^)&ll+AJre9pEJ%T&0S*l%Z^-vZ?yVn)ECz}Lr -^vk%BjsazRl+tIa88fA|1s}X>;73qoD*RX$$%;V-bHk#(FvlQqoUS)l&DTl;}R23+Owm!jVey9NEt0U -~2MC>!YxOu?*Isw+gzZy~$f#1FUi)HIg5?pqP415qh1-yec9uThw^b!0gEO>Vh82R>$CFcy}GV>`17I* -bckPkiB$~om$B1teF%xQ(a6;DlOM2YxNpTLS0+k>o4ha?_YK?d3~$p!Rs-k)s`EM4%-!|*FeulKu1R= -de_YN*|+hmIVela%loGFg}nPYg?#*Zg?zG1$Vd30@Z~brBfD6{YVD#^Qg|gtvcGt4P)mC`^-^oN9>&vIeOW4t18vg+6uZ~jrB2@g>D|$nEf -Zeb!|Nlok?k{Ikkyse{&|eNB$K?Rx#CY%HolRD(16xR%yId7mlB;+J>vH(mr3kDG#E!v?}X&(EjFTy9pM@78|HB*-{Gl$Ajrp&He!%f^Zn$$s&OBgRdYwpf3RJvGn(HdV{M;u5>FraUO~0vj -_nBQ=(xSXg*jG{^a0KMeYtqp(oHgPh}CNX1-Sfh`u6AG!^JuLIk-GMJ32ncZDH?!vj=e;t6G-gM|s8S -L1m-+WiIG%ZTv4#O9KQH000080Q-4XQzi+)rey&D0F?p&03rYY0B~t=FJE?LZe(wAFK}UFYhh<;Zf7r -FUtwZzb#z}}E^v8Ol1*#FFbsz8`4u9iw>JG?mkkEn+8zdF3_5lhBe;o@h>!aRr@$xu`${)umYyt0RENPX^U|XygAXFX0o=*E~OaUjv2# -?RZ!^`P#DX1bdTJx0KJ1{tCORUZ4^EKK)rTal5Bh<)(q%E?nEQOS13HR`kfq(L{WMYYBnJ=!uy1o}?O -p!B`UcV>XDH4u5ZBl~0ubG0&a8gw1VKmfs9N}m1cd_Fg=6+aTq9xVlS>h^u*=d8|m?SZwjm^G}-hhHh -gEmN_>k!^C5B6@gYKc2{F8o|p!#IdlQXosZ3X<)nSVwwoEWo{1Le_9%*EAfrMXIK6io370Vnb)=7Z9s -o`VYD~>K<&=6{KV0-*JQpzwzaExs$?K1mDf{qb#?<#J@2`Z|lv?(!XwDZgZH!pULb8P)h>@6aWAK2mt -$eR#O$NlA!be008j;001EX003}la4%nWWo~3|axZXUV{2h&X>MmPUtei%X>?y-E^v8GkIQPrFbqZa{t -7X(X(5C%`!4#JMPcYBLpP-uH&Ggc9R*n~N|o_V)Shf-=doqGDkd$0Dp`m -X;%_Z_Vp8tg4c%5lNWv$IQ&{UpQ6V`r5pc$F0%yWok4 --q(37Ja(CrAUyV%x-lA!p0Ky8YMG@h%0$W1tYUeQeUy=4dCaSMRwPWO^be<%j6;-qK9`{sX~h-=PwSW -$vRbh!123#gD>=v2QZQbGOIhLUbs<VHVCOQe?>%<6l;M)q*k7$aIX`8ZsX -q#1UY0Dtty39`*VtGTha-m16Sl0Xygc0A7O8(HZ)Gkb$^EQYY}3{DNqfYAr%`~dZq>!9;{1fz1xv+Gu -`?T;r5|}LBWpkRXCBw$q$sjttoNpm<2dd2qZE2j3$^fA$~14dONqnOY2;fg)dgsSH%G6|j)RL2Z(g3f -V=vg`{^{x7{@(H4K|^tTesp!UyMt#_>(;>@ZtXO;emZ)8v~zy-vQMV&y;1iJ-J4+d*#Rzho6D=+=M#F -|K0kW*2G@I&=K7z{4g$5)fgw31lHlm&*_)%QzbS!FPbca#cpm(G@@`)dO!hYuoSt7Ey$@a=z57X%(Ps -~2iY^j!76dYtRS>wPi07VC>mg$7dbX-Wc{65PKd_4|6`f6JQJv;IymjYEbx1+1sAO7oMD(eOm3>34<` -st|w2e=>GZoU;*vezR@85uvS>H+IsXc5K;yx64lW%-MUbZe;!DT51V -c>6eR-1bv5dNu=@p^JIYM+u$eG#aX5hQ#eLr82yb$&Q=IzDo~iTF=1^xdHo+rs}>f?K=k!0beO`G91a -ma-_a+V72cCW1U>-8k6$^hb+o9OyBUD$=2#O%QjIHX;0ni~3O%C8g}m(psch$!GF0gdqD0wmFZiV0+r -p>Es_Q@!a|TwX<1yFJ7>r-k6A?7Uzf8&XAQlI(@lIAtmgORK0f7AuG0Sm)t&DY1Iv>paC)r~;FsCV57(da>zK}=XP;6<{WYdy@+0<=()7tz){&Va9h -On`+`HWLa1hn0vrU`y!E9M4A7hL3@}1Atmqz{jB{JA2!UZO$R@?v9^F^U=BrhZa; -M$Q3;*0Bait88w`8WD27fD*s^Dty<4By=Psv=`a6wc_t5$_3E7Jk2pz0*+p}35bm)I;Ew= -!wS~ec;U}MxjV5=UK{nCyDN(-N@d8<5lb})3-|GNizn9u8O$v=t!0hS}=RPQ&FE_K3PGtj9LdZI_PgM -+yI_%wFbw6vBw)>e{{9$Kf3*q2x>CE(}WtM%f6eo-_azqewM3(K?|f1dgR?nX#Bvrgf#S@u=aw$unjEny$2fA>cLeD;0t>iq2Gaq#Bs;_}_08rJBR4S%R*>y3 -+i_!Ea`5ocB5RYoB85X=cm$T!a^|9P;&SMIcGR7e>)|aFNec2ReGGYU;0=Op*4A)KP_&O-nxM -S*bGj@1e$ONaQ$x|#6M4BGKWX3lJY*}5>->mcvf8S#Er1ymK#jmx5UOAh=G;{b0+^m9|v5??p>Hl(3I9YWkhM@I(+Gtayk-Si($v&u -5SC<7jc#F3To*8QJySI`SEID;(EK}#v)T4MoKK_4WOp{-n@^^bNw^K8Z8tpqeD&mf`gDKmeEMv69sl( -8;yTAy!K;f8f$|>|j;piF3t%7}(@+I7swPb^M@Q(6KYi+Slb?Z6UVNE-yA%#5ifYI~>Tb6MK=cVb)`5 -1L-Y@f5R<4c1judAQ84Pw*fp%6`4JK=G+Qu=MrJ^wzd&4=Dko+T;u^KtLpn`Lu0aTGW_I-btK8KLboa -5opQQrB*$U)tUXF$te7dpoEEl!rNm=Un1>nr$7JvgxkJN;uChc-JLBf4C=Kc-wEid5x1n!T1RiWL~Y_ -;7wcM*2)6Lf|U>sRxEp!qE*Z5KnxCB%(iWP;*ze~|_LwEw$E()GF -CW7zyswopF47#Mq^cHC%%GA|eR@_n+n9GHlKG&7urboRhHnO^C@G|SCccpI=9!6KZW#3k*-GN5M{Ev9r^><*(%q4gwE3}pti<*VCsLBQ=j1}jg{dx4rNkL3ReO -!8#;UY2p(7}62Im=!XBe6AA||l5E_?CHqftM|%c{wa>WuJFl0@0BFsCzoutl1oHLPf`P77Mb?@3d{RW -vDz7mJ!4U#DM4g8()!JS?H8^aEO`_D9(aAOl!Td7dn_5Kx5BFD~HaO6OH05_MHz5z#Qb&}!o8N}a?*U -6C>Q2JKFOqE%QTIMV4|<9}ohY2>6GL_ysAW!SN?iH8s7@Y@k_MF)OxSQSU*tBp&tlRi~l06 -75wh!3~aHst-pz|zptHe`>FM6mCUkOy&FpZT!Eed7_wWdc!lw}F@SMsqkhdnWwg!N)#&$_l)p*D=Do~ -k3fzvrue0E$wLUqr9z_|IhXH_U5#*ym9aOj+WSh8{LInu0+F{%HyUQA6FHy%)o4iZ!Wj6||cmjqoa5if9{=LxIw?Fm2^$E`rskx01Zb -fNmR}&2z;^y{5G7SGFOsGuhgiO!xfh)EWEiN)}_gyeAx$Nll`_o`d?;d-+5{N^v -?fBEVb9jv+KczrJ%_dP%94eZRzX`jAylMJ=@$$hS;S+)zRW@$$S7CbO*x=3H`?m0FOw#ceO_D$)qZ~E -eR7S+2ttz;tpp=IfwjfZJyYwF?uzIVgH+{9lLS)IF%lEWE~y}@k~47cIPe{Emo`*BeRA%}V4;qB&Z>4?d$1{HiGrO&v5LuXoHt{T5>Xrt7+E)q<^XwVZ0a -&9(Fvo-#f?`6)^pb28J?iQbd34Fd#NcHr@Fe(U3bQ|ATac<&P8a=~oK4_rD3awCvLK5iogV4OIqQyfV2_Fi^IkPI{?rHimnp;JZF-;^-RvP*jX?Y34%~lPradhZ -dfS3#^KA{V=)T)j67)iSGvW7v8U?>#*A}aru3e|D&5U;~$c{F@$jSx=Tl4C2IN2-F--QB5MyOwP&FaN -(=%x8~xy0Tb{wSjtJ9`Ja2iCmcrJ7yyc3=&OWl|?L@+qmCRIpNyGMY(fUFCIUTJAJUXO*O`U*HluI(DVgW{{pYWgK>mLZX --&yJm8*e8vNtyzXXsF_Le%VqKx|m{oV|%RS;&`a{W -W{lNws%jvq@dh@6aWAK2mt$eR#U3$Z`m*h000^h001 -KZ003}la4%nWWo~3|axZXUV{2h&X>MmPUu|`BY;0+6b$Bjtd7W2VbK5o+e%G%!%RDUE$Z+h~PO4e$>~ -`v`HQOf6#6`5Et^bkw$eaV= -WQuKR!u1?p{#+j-pCAW4Y{x)vmgtwTLD?CHSU*t(5+HZZQy!ux3vOkCZ$yOA_uS -Ty_pocuE6daG9WB3K1^X@J^LxV=atBBT6D)0J~S3}=4gytm`o=V_}No}txoF+&c^_1+4znnF -?887Y#$(ceAQQYt$I#@K2$0erNutyV!<=GnwZhO~@0$6+VqWoG2Bg7%Um>4Z028ND -C*s(|w8Tf@F00k3Oa@BKUIdQhtwY^`m~?D%*ijb$5Yx -9yr`b#+|sjmhi$xHvyKfAc*TZ}ab8XKydg-|+MEi!&~Y?6fG>8F%b1e|iU7G+OAMp%#T{L}nPms@)ZJ -t8%9ztM%p>Z9U%bwLE)yoNdpT@WgCutFp7v{T9p!aUZ`*+?UmR?*e*cjyQ(-qc~0>I=T2E@)Tm1iKc@ -T6I$20=MX~pzE;AmVTw4W%h}lq{xSzkagW#0?e0y17DmYo-~}wyXI{!2Dvk*@1yx%zpFPVpnD7t|(bd -%=N~Um<5YtchvVlTk3L~fyvK7y^QVE}aA?ZTsMoxGEB6V1OSL+Isw5T;;x@K^PHio; -oY@9ima?2p&U)q>!jflX?Z4v@QxGtk)13qweO{o`F+q>LpG+GmO>O!#+JHGl-)FK37*)5r%CM9(<2qp -=YLWmY|1Fnb)UBYiZet`jn_Ye*D-|e{@fvKApmOpuRX8Q-53fFrTefLy-Ji>@R;GAfhy)-)+*3AOLnX -jY%siT*4X@f9EVte8gVBJ8~W6BNh>%8{{i`-;)D_B#DLFRZ$hx9R)j`qFb9SI(|y;{*P9u;3KB|hia= -Qv%*E>|Hul0iA2RgbF~q1C*ma{w2E17s`bEkK&%rM9Z55o$Ri>%JE2l&>|8JHvtHI_{r-M)&Fp< -WS{_Q(|%^!$L~A6wNysi&DdJUW?VB{v4rpHa$xeRYZZjv3-w@&BP0iIM-5s{0)EFxS9BiHhsiHtnEw9 -@^JXROn;a7qGC*VW}L(NM(ps@JQ0Hd7jKPrL>MFP7?BwT6I;s6e@N*3H%LC0SVpL^3|#<<=&sqgPc-E -3x;Je+r~Nyib8ywzf5kK`@y>6xt^?&PB6u%dY)_e9=X|hb&g()E4I;f+r%eSrkr<2%cp)?_{U*}Vx?} -=3>$-p6CVJ~jj{XtKL6)J(2o*_x$vl)3*B&mA68c4K?`ytC;sJVC`}xwQg+{s9Mtq>A#uFuwfQTa-78 -+Hsv>~bgVCrd+P0^Fef_ -&R&ON)`xEcZLZ{>@;wSvH1+cj(t$CqsN7D@!|5%)kJj7{Q2p01pG389{Dt~lg5YPogtFqWtrn_Z7ljm -&j)+gi|flNv`SJ$d6_El#iiQ(MzUIRl3RjSYWc>*TxZ)pAG$ljc%uH7P_!&u?A^o -)M2iut8U*N?CbN2YgF46vIWa@cF!Em3r)>u^qQD)z>GWC=XW#|32im^7bMn%=yMDC5;aTpmXETKK-JJ -*F-5tZs8=ep62Ydx_0s5iMY%{$-B;GZ<2c=wS(1Lut0#zxS`3! -94=A?s1e!cErW_W-a14cbPia@{%HW(a(tobVEFvQOMwACBXPNygRLewyjTFthS%W<>Yt{lpGZ@fWAjPV3RO$f$XU^7zNz>O%|Hi!4rQXh+hS%Ha^pQuF`_i; -RvEdk3ta4-g^?H9hZC~^p=PMm#E}msT`?k;t@IfMwwfAEL?kn8YxJ88H^@5L_`_(8s3`D6B~(6-Aj~r -kb>#1HN@B0Y{KrnaWJfES-qWCmZ}V|<6w%dad4$b&5DhQM1{t@?ucD~m08mQ<1QY-O00;p4c~(<#0(1 -;%0RRA91^@sg0001RX>c!Jc4cm4Z*nhiVPk7yXK8L{FJE(Xa&=>Lb#i5ME^v9}lh12|Fcin{{uK{h-O0=n^x*?jYGDVh<%IUerNsdPZ${v!FG5(MEOC`$)FxtR;t6AIqZu -0MB&}Xy{QL1D7-g0CMYb@6aWAK2mt$eR#R>K>OXr5001W;001BW003}la -4%nWWo~3|axZXUV{2h&X>MmPZDDe2WpZ;aaCxm+ZExE+68`RA!Kxo3A*#^s-C_$PF5q^(hhX!zNg5o^ -Fa%bnV{K(oOHoO4Y3{e*3|}NtlG9#s`4CIw%y4En96mF8Nv^kfO*VPK2>h-aT96{oSXr|*Y05PdL~a? -8OzdjHH|k|x(F}f0G+9h;n5c7JPF|9qlGibzY|uckir2 -a1;sNAsxF8wdRRxobteK>FQ74m0zN@$ZmWl}qlStx -ct5y9)N1zyKw8C;-j~m1NCXT^>MKH6vH65E>WieQmwjRf=JDOMGBkY)9O -KPfyC!`ghNE+EEx|b((Pl&-(59Br9q0AHBwiX-mJlG83t8Q&(iOonp#8g63zA4PD=Zx)wV$LDA1htmu9oX=)6*r2ca(LTLvpN`t67wyx3I=Yq -&f*;m6onQXGxJ=J+1$R9~gI88wS`K{b=)Ywkq&aO!yT;A@o&B4a7m1`O^F#O|T~si(S@SbDHtl^sjKKC6|wHQQHxcjg!t+foYIUk~g|$?09sGM -O0MvDmK?w(x!4Tkfw(I3$5*DgXle#(sj>urTm*k4?n9O$oRPPzt(mMv|83>vTEMidIer9C3M2%MDew$ -}oEPR0iHV|u4xRD9UHp1?=Oz6_pB>;|GB7%XR11OB|OU08wwV`ox23P@?zo#YO^v`)^#Ho6h)m^>2X- -ajdNX<1TFs(VFdq|^1-C-r;O-;fG>i`S0UlQI(#NLUGkFedk5pSV#QA>aodcjIF8z&KT -^?y^!=UShNXu?^#}8yry-rkKmJF-AurJI6&+In}v8v;EhjDBS=>%*{l(i#{h5-qZKY3n;YMGA7kAIgT -4B&|O*l;8Y%)uGAN;_)cp;pjxmvne}V9hGpe=0Qv9Lm)8Kp?AN`$Ct&wm@EWFek@b-{kFL3#mhFfd!s -a8rYh?g1sj9km^~7{0Jzm21z09=*$~79SOYQoniKu8GhjnmzMgSH+ozx2Q*Zy6^HIs}3=7P{cz0Dax`< -(pf?q;{&3T64aMfQXt5vC}=gw_S;SZ`v}b!NyTuU?2JOf`?75k6uR$33 -0qjY<$O;mN~mmj#UhkoC4DReq6U%go+w5nLS4)=M%vpjN>{P*Q*{8NF=bpin|*re-{_`(!}Er)u-jKI -|Pu*SkYMhe@Y>N5+6Sy(cUYIuS57cEBl`qIZ$6P;?`vnM7Mr{)wHH|p2uxl7l?P#0xORrwDmncOL;Hs&F8jo(+WZ4z@+4LfsKW(~P;##Q&RwRE(*Hyx84@8#)QH)xAPXZt~bICPW9nHw~ -2rljBB0kAMaZ~KT2y%`#(H$6g!T#t-XceeC9Q$GDj)6-$`yO856?dvQ(-?$PGWCj+1~roOzy9H8!G -1-efQSD{YmoCc_QPCj&ZB++3=$Ki2VwZ7fk3$#jIbN>B})pPktxx?hve-FB=`)`|M(fESJ8P6{E?3`# -s{NcU@SB+d+R}=7G3)}4bcwnc@G<@C<}sx5lE=qYms%yP?~#vDP!B({T7*~Ls<~UpF%~!xdW*)1P2DXFRDp_h+FGTsJK-c!Jc4cm4Z*nhiVPk7yXK8L{FLGsZb!l>CZDnqBb1rasl~ ->zx+cpq==T{)~gUPYNNjm9FMyca4GLveeiz~^wRO4`95we+5qyj*(UC0041AwGJN}jfQ5dkdD?j9_57 -YAf^oeHu{Ge+>AY-mQZbis1L60*q?#)-UUL^8e-q*&@fxTXvI4UAZad}LgtMeZDsUxH^HLi0q6y`7#` -G69(~rY8seUNo9UrNUg#@uH9h8y0Z3E<`E|eh0i|!O5anturPWNtmQ*CIZJvSL=dHh*n=h@-)AyMjM` -G>1D8{Trg+JixrV~Ypj!UeUJ*-A>*}#(QuavGOGq17%CmJX`QZv_y^0Af(Pq6g=S%RgdC8(_)Z;yU$0 -nQra9~A!5{@&26td)YIHq1Q+7qS3#Mny+=N}RTl4v;;EDPAlksBAhXpTz3!Yx3fSIvd`L5t*(zqVwi9 -MW3%4Ns#OHYCo<$vLjD&vRYAJ4|4|BwT7RO=M^5h28Eek|nhirthMO+t~Ovx-&JQn0EIwQY6dl}SV -juQ^feh)|ILvpMc^b8RJ6^k^D5kFGl9F_b837C`u{0!bs!rq5yhHV4Sox6S@HtRKKLU_J2X)Ine7YAq -IBpR6|ueHxw@|#P&@o*TLl}>YI8V&os@nBr;d(~VGOC#Pir=7F&QEzr0&iZ+lH2BA^0GulClA?xr}hH2060A(`)MTVb>@eM%cDp@Z6lP80?BZ&n97G$#Kpneaz@f;B -j~Um0TRTzUSg1W5fAmpsb%kCFC3fFpUTOsp7=IHLNilADN|6;S{1?eb`6!mGmq$<$iVK(2iTisel2_$ -7nJIJu$-W#fxXPrFHlRF;^2Yf~$id4Iuw^j3oC{4f^j#<4M#Dr;%R+4Wjp9?@JBTx?}$tuEraW6xl5^ -zmlf9T6!3czF0@9FOiM3mrP5*?~rdrnosprIz}8L=O^DNGQB*0M>8Z{uorWB6RUIr0^@4)^epN@b4|W -iUZO3$whhOi=Y6hg8C_i_l!y*N9+zDrgW3q>PUH!TvC@)OO7$A}@S@I#uJ8OW)$` -{U&}JptJ<61MXe-i()z)TGxF_cDxKe`XK@A|9g?bLf3})2|rV(fkXJchtoqoziOzlV=*_ -C6rD-Ut$St!^lff#P}h5poX#Mp@g(}=yg!MC$Q6*)S!L()s(b6TaJPZ8odjqT&MKCG*j0G6t -*&TBtR6t*hZ5}yXEY&#F43E8xL(LpnWCKiO?7D}q98X|EJ!K|qgRY)cbH@hXsHkzMh>1odtn=8nig}j -Jok2fp<=cytX4&>tEdhQLhb5_{;2LUuyU26g=th*;}dL^$}T~mQX-VZW^t{07L}$ks;j(41G}cNGf6Q -Z6PgOPuh^3!ht^A)WOq-|zO7iJv|BxjRlL>50h+MWahLRHt~irSpbxla=npw)T;anokA5&mAD9F8CRL -M7?@vlF^Xo6w5nLBH>I}?D$ZIN3$f+LY3@BhhE2CFH6;gMku_7TxPuf7!jj%QYG~KXFw|HvuhiQOM$R -~gkdRc4?Oxbpw>~k;&H!4z0TdeeL`PIV{!%M=JB#u>J#jz(?w(QvM#)iwT;?yTkUz1UhGZP)!Vt%(Bs -2{efs`E&|?E?GtZl)X7R`aR|U@Z+QF!nWp4Jq|o9-0_ssxc%W&b{B`+MEShu|OU(|3~EI#S{MEcF8>p -R^sa6uz~A4^-ysYC?YjE@EX{9TuY=&J%=u%>4K&ti7}5|_G3bc>JP8n=Qs8NZ@V=Rcq8FZkB^C~9y@M -#hXBa8(Korl-{Sb>$?<+O|CMpKoYN^*=K;5AFw0;ng`YqttUYc^*R^!uug?w+KA*pJX!F(}Ow+jZ -(uhnBWSh=7uohn8FVVP|P>XD@SbWn* -b(X=QSAE^vA6J@0edHnQLSS1|I;L~<0qB=5dFzsR{X&E0q=mt>k;GjAN9kEURmHAU)3%8DANfBV}X0J -s1_SxVDudRNUfwMbyGSS)rIyNlI+^!75Vql+w85&VC;O!6qtQdQJy8ZC>ds-osnMUATFbyQx^m#2#)h -5xDW=ryn>XK@HgWVuDSG(g_}TN9Z=UM`3wlm4e&0? -DoV@wB3F%yrm9J@ypH$wviYK{nkYF>&7T*`B5letug!xDFm@hP%KWuTsv;|9_1;BQ&ZB0%fG?4LH{6T -h^DN2pHvHEy0BxVsH$arXy~t|a>eZqFY9u*-d!01UuHY|kly38OxiDbft{3Y2L__&o0bR -#?qdi70UN$gegIt;vWs#*xp8crKt~JrQo&IK8zEHCyUF&tR3p9=vNmZ-8z1R4^seT$wc+?Zy%IST&_q -SrPL;G=5H`VE${-!4fee_?$f%|4Kj?PQqvxzs#XwNLds|Ax8V3_P3z)9M6>liLP<&NE~dK%dW_%e`-|5Q3QXeDA#*&A2Z7y5_M@JFUvxO(Fd*Y%j3R*htd+Zc~%(2L3Pd#QId@1tPZ`E -0C9{nx`oTWRcJY61xzAi5i$dDQqUM^Z|I?6GOG;cQto%*)EWNbAaP@B?nZqF&4(kz7YpJv2VwzoNXz; -bl%28dQq^RdvsWmW=25q4eVhvQeK$?%AxJ{*I>cgS2c239!5JD;|vc`%j#FZEf`VR(MbD6t==1G%Y!f -R4PJUr~lq)IP`2!CW70^i|=qV{cf{8UL+tFE1JF*x?Efy3zTNBr{X?qIa%uvdJ>vmt}dq0G=3i-wP9R -k>UY2Zo@7Q4Ec<#c(w4Z>&wbP+Nh^e)I+as;=C2w58VXNt&ufBkAtVNxXrX$aZ2)QD5{LDA1=vg9~@9 -Cm(4r@#IeRY2fsLwd*1TGs3CaO=&(Vht(82Pf({K1rZG%fBZE32%~)bG&&l@U`_y~hdja=ZHu!r_^U| -fpi^lfd3J>MNX~teZDt1h2*cPx4q9dzS#9F!zcXv#Q@a+t$6TGETG%HKK$wNj2}9B5;i)s1jW(Gc0o! -O0E|LzU9Ska1*{lYg(zjoe;&Rce(a7bC55(>MKJUWSbA4Gh$9UwNgD -Iq@ww21YBnjZ{AmU9S3^lxjE^VX9gy-tNkTl=H{QI5eA(|7V?^sHE^qr~3>OVu~1t~8guzyHx(J-B0> -A1;Zr!kNdz=OuDTsg_ZTnx_8^#SVqj0I(@hfm7I=_e`Zyoq(j786QKRi&<209%#69Y+2s9OvkqwHb4$tjoj59LA46-Kvb(8UZXXXIaW8N -f^#LJ@I%fH;F5w93sS7E!dxqVEVXiSuUhW{+#5sk~DgnHgIx+W7@Q&V=as7tOj&o?WU4~glp%(D`3fU -Tx+ZQ+_{}X;T7&^t=N3KT~`b}ifL7Ah4WW)+O`9)GnG$aKTq^Bndjub^@QEYY1{8=g$LMyXfQZ*`MM( -z%)x^X8*i(n`s8p02!`dC;K(UJhruyVlAgq3f>p -Z-S`ULKu}HX{waPXq1l;8n`^OdRF0gTQaB#I6b*;Mn=os|!vg+HS93+CxzEav_2zre~Buc;lII#>UD- -}wLyFk>M4!s%6#2n~dAt6%S+5i=H)hqyv){e+ZVkhiy5+raZ-6KJUot{K|!9IXtM_>5u1GMCY_Bc6C7 -7JBOyCzdR%jX%H>@0nlbKNBXd|Kg-+6E-NDguz$UEm9Sj-Fj&uUS@LM6F9aq;z;j?ViO=)ZfH4{urN^ -<%<1ezysIH?6fIwH3k2yY30YR80N-$T&Y-L*YUm_n~ex$A$kzam{I9FHkBkG?wdx-g}Y}-jjb|Z&lzy -^rAnp*#qQt@x~j2+A?KjK>@F?6?SD(_072b0m(3@9P7nkH+T6LH6YDNqsZkFm)S7QO3MbpNxk6oF&7tNbqRb}FOn368*KD>c0VoivY?mqq`E@)8=F1jUU*5A=4DMg4a`-rg -J|k{Qnf)_71)a0yS0II7@L0o{JU>ny?Gwp;FsgSynOZD^Jh=rJRkXPWz@p$ppyw`;Q>XE1}&uqcjz(fkh9LK$Mh; -!b$y0LWVgW)HJDWj6+};)#|{jqx8GU6{PfE@xPe(#GxxUyzrRE@Er^b3I)Ccsc|4?GYnGhPdjQy>K3PZhP^g5pF&-6$*)7Z)*;q>TrLp -ZI-w`KI}W}k$f20Lqrv-B($Rf(&HA@svenuKAJ{zJ9sj=sR4;pRJG4H|Z*e0fyv?1+lDGrv4Sk9S0TKgb5Eb)}ks)cRI8xGlVg7lCzT6!iM*c6u(>O#HG2;!l$s&qjp -r6Cr4OZNuS{(1G(U9;m07z0$0s*0i7lCD~wLJE=NN7HG?(dB-ORP;~)d(~_EhNYcibjBFF?JSd|w*5w -jF)R%PN$K}cM?jdNOQ!GiV;!1*MWtD~r96F2495cYe!zT9N5;~2+Aq33J1ZJ8>bCsZC<-=H})4A;QDW -fy1Q#gqyGjZr?ft`ct3wB|?#3Kh14^;V@@=aW7b!Rnv@_2#^hEQH%z5)}7Q?vq0(E%XO0p@c8HYM2ng -xWi8pZmu6IBa$ISi%g8ZQm8A&ro&+^dg%Gf#3?mLK~Fbxw=Q|n=LtUSiqJf&C%f?QCz8~PR*cIn6k<}El)w6?Ag{<^o-ieEn -o40?F-;Qs%d-n%n;A_rBAQl!F|4>yHA?_>URbU>}OO&WskKZ>-6skaY}bN}ppeA>m9U?w-`H|b54-X! -_r627P=xdDLq-*u)|UH8mw5NRr1YK?-_3eTskn~f8Kj`uaiZjj`R%@F(D0NOXzNIzm+x^nFgJNdT)~O|sl`W)}_L8NXn3kJLYU`e1>QD6m`pGvto{`4`E -d0Lmd$N~JZLd!F}bNcLtX&D>)JEgEAbYGijYG2UA5wTqpqJS -O7(MyA+_Dx@89|F*5TT*Jckx6xMW8Y!3UT$wP=4hwT=zvtpAq3vzJ&e))I93c9Kr1kVablzi>SaahV7 -)cCX<6Jg7ACBnihnm+eit5JPI&yDB(txY;$f1UsqDv!+U_wd;caQ=gBh>&|M45prg2; -g+#D?MKgA)5L}Zb(|2-K-qPYgY|33)3Y8gl^c8D449~=a_=hs{VkUW3H$!`cG;TnOr2oAlaKK|MEPLQlgsMl{o -sBa4ZBW0V!u%P3AT6W;B@vZgVD}=uh;v(+qQeB`OVl8oD@bGbHy~bq&4=6bfjXJ7-(GMF>;86^!%p#( -J`|arI3^tJ}_x>XdYuLD|pm#;y1=qD_105EYI^S9eFssL1yCnoS2-9@#%B4_8Og{07x~8BHnCBR&;ke -q3gc@PNd8+yeayRF?8IlFx;2;2Q+;$7kAFW^C2eJsp%os2flxFN*=-QC|1bV2CF2?k#ulw?VYEjlyR< -ZF>Mc?ON7suwg+zm5!|KqY0*@2l}Dq_#4*276}?kuQ=J$fnFEWdVuo&y+VX^tXlW8+C`Z6cKUCs&m`T -J!re=NMfeNmKmx@Sa;3=r7M~V)4P7V?OVaq$CQ=c5KQTZ%M$B=)a&ZeqnSI>_MAldoRS>+t`2>TcRBG71xZVz!ZV|HIO=-XU<}i98{Si9c!WJloWnR8|8!b?5HBFmj;`@j9WzrY -$kK^J1L-8Coj!BbR78$-EcZtbe!8n;`l?He-_9?xAX)yU9u&;KY^^VyHeRQWj|7pZO`nkm4m}n6oHJ; -5k9tk}e?OgjX5&W?YNMa7Nu_sq$bwx@~o?{xm{NJICHp1lO5jE$m@$6EiR|vUQYLg@en*hncH!wODh7 -Cf-s3ItT5Cu=@rUn^Q6ttE32he;ar_?&irzPqy^#M#oU>G#&@m4#Kp_3KsC9ojRhlF+Kn#Nn3-L0LDI -9W27wMj9Cf%W7aGl|NZI9OAVytW3EwUq5))L3A~D{C)VcdSbh*EL}dvlevXE{k>(wm1z{Jd2~p{~bTn -N!tRRz2P3g>v*cJAMoK=XLp-d59rmXLtZ);b@STgj&t6+#0MauJ$eoe%?2Ey?v%{xzU{pi+F8(JlgK? -Q8?vBFT28!oLdg)?h0??3XgUgDU9Pu{4`BWWEw{T%t{o|k<+#;ISS%GTTE$6{8l8@(N` -d;%DUN7R0$ZNUc1OD0g9#8Q7g3^S@j^sp~AxI5x?6Yt>E=0an-*5_2kv_LPw=sg6p85Ye9L*WtP0o;< -|($bAcFg9jo-R(9uD;PsSV{bCS5|Vp}_NMnhb0`!|vAL2I%0Mc@s1F;p^K0N-{sWjJC|a-% -v=p=1*l>P+QH`GPS?{rYJyz?*1M=aXvdm!2TC=zE}L^T52dSs1@?L|1=hr*AUE0UwjwV<`Pp+5=iapqt=ZdQZF9ftlx|tHgpv}~V%ojYYi~Q)%bAW^%PvGhXAzy -CpMFfywlM|e#uVrqbCB%R_ZnfaM*A&TVW2M5Is$lJcxUO3Hn~mF`R4o7jQZ!p931g3(D{vtteu;G5@8 -q`@g>?B?YzkWkDrzr`Y-Y;cb$e+t=w>sUZAY$TIpH@WVHvSrHc-x$---$X}NVOHrd32Ra=1|9ty$tFR -JgbcgAkto(S|%htY?_Cx<38V5Fb@VPKQ-T}QXQrDG%(RzW>!YZO(gUgphV`u}Zm^hMitB)x$A+K|Nw3l6o%G9<`_>n{31vrKUtH`=?$~1jDP -ZA@CPk*s;A{zaRZYab(Ajxs=Yu@Y;iE$lV)&&6D~0G>JYOMjv4NQ6l1SG$sfmy_^E8&jm*yMka0}CsS -`d19)UiSCLB4RJV6$pIH1)L(uhPv?>M+kq!RKs`B#{KWIa5CJLIx@7%#qqGNufWggpa^e@9(pau5o -KSVZ+CbT8?Yj4IgGwg*jCP`*Q(Ey|(ss4;B7VS6pgLipTbXacH#Ctt#rUIQxOLb8BX!pZ6(!d+D5k9d -<@vCpGUu64@1t(Xxzn}Ll_lcbLVXjy6ey_b|qBZS;_Hb_DL;zkPZ>~_t>Jo1p73AW0;wJ>8N4FEdlI7x@&T|aGF -ks=H(?WnoC6V0m>Z7x!mIh(ic>203wuWlweBISsWw;Cv7p81xf9X^kt?n?M5Tksc-m)yQu9eEZ-rI_! --mU0oX;QAQB*chQ_w=r=a;RGhaJcX6&)UF{O_nb6q|(JTBMj#`Q58H<;r&6pCgDY>q>`Yk8CSKgk>LC -!KwI-KP=)oEDu(+mkeJM5nNeijlzwz2%K19V%kqjFNMA@tB+xIFKVbR72DUrJ`$a_aAt&cNj8-;CelCCP0|iZqzjxyD4}a)akd -7uAMsl7;m*t9B1dv#n>b(HMv^j@1*Z6Ad -dh7i|*c!Jc4cm4Z*nhiVPk7yXK8L{FLYsNb1ras-8*Y<+enh%^(z`Q7NG)(v8 -`+-XGFPm5@*LiCN{>-&h8loLyIlDGZe{TleV>i|NB){KiExDe(cWN;m$}TKB}v$>s{5YEp~b(BwLD%G -x$!MBx9La@LckgHF?S_R$p;e^J*nov80>*GFiaaCj~ZPf8&)DMLybMyV3T9C3#9qKNS8jZxNEUKDi#pR{QFU@V4B{d9KnR~g -G&gG)Wm*R4?tcn#=Pcv~5tzac0=ha$2V+Vgf1^Nz;KF23tK7Ks>`@xACQm;#x%0Pu927e#KqMp#z$Ex -@z&ri6%Jp^_o7a4c%U_t8claLzfsH}l&Nv7^j_-_r*7kbQTQ<~vla)45oiv-7xM~bA#P!q3`e4UBBxt -+juBQv@ljgUVezqw&lbjj;a^el|yJX!HL23*JM`l`sa3<1~>awhWneDqVYXkQUk_>J9>A}y(K -OewneDdSb&!_R{{htm__J}oSvaTi!KF(q2cUnMD(qzFNUIi1zw%A`euwN`9^?uO;EXrVF1_3Y*u}?%* -`> -He26ndi6?Gi(fpsLAESPLclsiu&OCw#xvCN8E#xX4LEQmowyaJ-(6IdO@$%UYAV<&Wja|Y*Xe9D)AL* -$~4<52Q!Nn$oQ_87?%u;?p;hi=Dg`j&kza<2G>e~2M+sQ|K^yC^GeKoiFuc)$;E@VVi=nH7M{We;)-o -EnFv;NpNAAS-^2HZFmaz;vGR6$ldDPXg**pMki~*^S6Db^&j%ldMUAyNrW#V6|9r2XTFs)G$i&8muwP -Bonn{U!zpWZv`wa9Diji4oa9#KotaxUnOhAiok&K3S25^0J>5aDa;{Jb#f*bNuHypNm!X&a-fngBmKuWiFNiW(RLsa-KFN&FP ->_iyKhriYF`5z@(M&q$AGpSn1$jfTEID_1bErGoCP#rU4zTqQSeS%KQ4Z2_ -sDZ7)$`R34@{fF=%X9a%_zMW-Y7$iGXbt7vK1zCR7(ik+rzQlI0K{1?aBwCLxWOppP22V9vCki~0~lC -h9m(M_sByZ%pXQZLX+Mw_{SHD{wT^g{T&dsM-F>{;^U-UM&G%B0&!#4<9EN6Vt3$>#FxSM9(8xq?{;nW+A!iSaA&|6^UxH*8$xl%{=(=Y7U|v -FAVOqGO+)8OvGmKiNEJfh!iaP?sU(lVd4%x4_1!jwA-Ca2zhfLC$#;M$TtgH~De{Fpx{TP@ay^^o;QF -VKhI}0(M+s^(D*x_O^;8Eb%BxMAk&=_Z6jzJg$XQq!4+ -S0Odh*?&Yq-hAu4LvVQ{?%d}s}&d(HDW>WOQRGqq=vCdk>l&63Nbs>Vg4qm@;!1&z?qMbfN;_mN=sEP -NMIQIwc4ooGH3mi`qebwzqpe){Y$16m!${3g=nCWwVek;Y+g)Q@7YabGxU02u0`PBH_a{nZxxMdXmNK}lb5nLn$^#TxWrvxTi -Kh!t8r&{q?2lrln%o@#9Twg#_(Y*<0Ym_alUDj-~NP@95E*+|1))paTNUc9)3r8XB4V0r;1T=Jq6U&VT&Dr5<3Mq)oruF#SWM{Lx{f9EI0=Aynz7?ujc -9^2FvuOPdPC~_+JUF39BKDN`peFB*DSC@8+0jTmH=mq%x-S?CH}zefz)w>#S(;m^Y{5ciBdHEP{F6Iy3m -&@FI>D8`FMWKe8DRii5BtVmK8D0d$8bvkko{KDH|iNtCL@^JR${mGP8Y6YNE7S!Q(IE8!g<~&csWw0o -}YmwY(>!kOP4?$7UvTS=|FWn*EX>E7k&5U>7kpXu$<@5NX2hFdI-=x8F`kYvz=jLKU%$(y#8wM2_UNl -5{wZ}FN8?%Q{J6%6onZDMU=fbfGgu5i~1(7ccOc6%i>I2;vn0jl>y$r*}N?C?3X2nuvDZS_bFE*rQz9-c=PB#H_M|C|v2QwN(q7x;3mw_ -#Wcx^HfEkNY6BSC%($?ihbphKXkDR##|Ez!Dg6dIp_h;iN@?;LyO0lF&5FS{kj+0Hrm#GMKLo9;Zi@a|_FeXF%GeJ~Q6KNFEVmfap$6`CVg49Wj^h;AeTcw9=1i4>cDR--h-1PaM!`uaYJ1d>le&qdWr@^LYqK%n+!I{>y4x -Ti#dZ{;?r`5HxBebx?qPZ!=kQ+33Ew`K@MagYO3WiP&w|^gx|q{<}$wxBJ_~<2>nePdgSc1Xuvk?xv= -_ah>V?g${ydEf(Um{!ZT;yNeB12t8fEx&%`rN!~bOmJ{-zma?O3kxsG#pvwj_-{zs#MwsEKcT`3r^cw -akdzzcLVyqg~au_>1eSSof9iYm1#zQ3&Xp#co%pyD>0z-N>qPAPLVO4CW!>;ZyYNdjKNO0GrG$aZ?#e -uae1%Q$=btrDRPNswY{P_Fxqe1p&K^n2VOh-iqG775+XoiW?33g1hZjcsmB>&3I_P(XY7nnI629fQVp -vw%8dwE(~QZx(!y^zl#d+)r@LQXgp`wL$Nzc5!J_-mFjUsV1hdSXlLhF=|~8yYO=Td_z&@J9o2Lg2{0 -dMW_*99e}Us-F&C*;D&L(e_`^k`boEo3fj8K_uP}kJttV(t3r*OfY+}2(p=P}vjrF5}JvX$P2iO-3ZePEOU%d>x8rSJ@?rcxk%i*nXU -6?wvMbz|5!?OUTnxR~lNe$T5#cVe?*To3+6+JqL4SygNFuA(8hXxRi$BHd>di3FFkA3F2`&qCBdbQ-D -Xlgdwoq|rV46ZBrYctE3ERd)}GZIZvQc*s(TDIfz?dxdCLL@KTFtBbr&+ -5N-|Z#U--z?J=IiG*e>*;QF2&1aO_+aV3!F1x6eO8iNpYTyM>j3TlgrXFQRV@M(yPW>s>kD;zWdrMrQ -hD{od8P{=41_Pg0wS0~v`vXL-`?;C8e*z0vP+$N3!eCt%)PsThFI57@O(jETTU?PK^H?g{sSt5iXz)um32k2)o -O`S6DrkgWs31PFK!e9ks@-|lqr_N9j!s^o3#duF=W!iTH_&trT_m`n$Aj<{%rp-I%Bnj0f-9l==-__B -Ta_8U(^P-jS8(y-j4D<<{b8*1^N?k&+7aw?PBfh;IAiSf&0t=y8*R>&`iSkEo9ctR1VCK#SNZ)R?QbT -m(fhq2?p(TUnSREAE{G={b-D4BB-^KC}P!#Wkjs`197u9C*w7GbL_~$O2xNRUDGTr^(N2^X8uMOH@Mu -LR}(fHuo?V&qPNAr-^q9k(N8uY{QDh^2>+BJ`81=i**t)ZtTSNeo&on5exWKG9QT3h+!J=- -G%@+4tANf5R%LBAbxii}4$B?d|e!i7Wn2PnPWC3~EH+;(w#Ee)|W_&_*{%0c&FCe<+F74|O7ODaLTXc -9Ephwwnfc>P1BFvSy-$kIELRmYHf@AS4t+h1ozehW?iVypER-XQZcuFjw?wEn4=zq{8;ZoErxv6@jAN0z-w2 -fgNDEW>tUJ%xm45MXFQqibJ&DrsHSTsNVs3dx-8py*Y>NTzhI)G0~oC)vk38aK -P3Sh!P{pgjqoPy;2(>^2MIp8XJ4ES(Jz{S!uM8ySOgvb`PJ?K2{^sOVI^EQ=hQMFr%mP@&^*HWakRtm -Ndj;K|*s3P~*%%x7!;v(_Z9JHKyy)ULGM0nl)CR>LuJ$H{s}89Tbc!Jc4cm4Z*nhiVPk7yXK8L{FLiWjY;!Jfd97F5Zrer> -edkw9=>Q^Qif~*91q8T2E)HP8aD&=O9u!re$&s`P#btMwmM`$XcV@Y~DYEOJ6%tEY&h^afnb96xu7rk -#$Pws0tCAdYkz%1SgR00-fmtCKRBH`#VZXd7la$`hYOx4DqtZea-X0vr2N5JiX2srKyl-S;xInarXQ% -H@&M!`!!@`~^DNSvgB2ZXLEsRvRWN#rAq;g&6Xb>_qiO6;Ad175kWk}RpD7gl6TZ&={?(~C_cUWAYiQ -pVJm$xNFcv+E&2tE}QW&~-XjaRG2CRyr6^DeWrdi3AOs7N2|g!zg(pf$6;3>$ -m+$-b{S8H3E5L>|Earl2x7&q6;p#gpVg5;pp(UxV5(x!9^yt`KbsrYy3n6+!)k -n-;*WRmStdBi46N8&C{fig-9)6bA?O1AuJWf+%x1UQ2um0k7xUSNX_JOno!3hTyeResMc3xcwJFw+9X -upjW8=nAQMYrVsCE6xEpp`558vmVGzFzeXdc43m8OX!Qnp>TSgfF4#WD^XzkT^kp7wEI~HIYkE4Bk@N -1p!H3Lk}cO)F^>MHQZ)G6#JjB~j`6@*qzp{Z11`#l&sj%t%Tqjnu1xc75eMVOZ`a4br>?E -!HAWY%QH?JfD-wd>#?{23FJovOu8t4RJvx7F2mGQnaRQ9k>TJ$t?($F0veT%@>$sqDd2%Yb;DWh(RnU -0#Q&k$yAd=eU0j1`DN5?_k~~f)@Ow9=V9_;D}^^HmfQD#Xk-u0aG9jH;FLAvS`fEgArvkA0~J>xr(hp -puCcS*2G)pp?$l^HoX$MgK_{h5R}BEQQjB~CzZ}ECz@E`@QkGa`!NR}qi$46qZCu;?bBM7!J%2l?i%f -8elB%JIk@X~Y1(7c%dfz@ihS%Qo_ODpxX+~%v6ewslWb(4NdUuUyPlM<)U -7>+4u&14ez@n)6#Jrbbar3T+QQU`Z*;h*lFq5<9*+HM$BJ3;80kJ!m5oWOk}_29S098#_Rar<#P86VB -;!z|G&)V)VEZK}dB!-q|NPKMK@t__YNc>TYowNa9;w404`Q8&QTIW>gTMy<WyBa9@4O3%{HMscmxzC7n4#)(t&N?yaig@R*>DSHIES}F^zH -sBxN<}R5>HPHFAMZ~>z5>Jce*(`A!hSddpt_VP+uh574WgE7?r1lMYJgtNwu^s!46nNw>osM7>EUeas -YQWzh^?K*!PA-OW*)ig>;3@E&$QdH<@)Ov;ng{R-~x_107w47N;pi)VoJ+R#|Z&#*+DPpc&TJn2Cu>} -f*+3$d8)9r;m~btyEV%GMmX+%8Mva1o2oeKv&D>{T{*0k$Z8LX(5}KA5e_);-x;vuxm_GbOKiNn>(d- -d(cXpY0pGxwKK0~h4u(g}-ih-0Hm&e%2d;x&8UBQLgqpT~HSA>EI)xEm_7VFg`t`TWJw;u6@C6}JyjK -2Q3B?4H)FgCt&=qsD5_ALHK9|aZeu@_EE=JCbmaw6PiHVQtidl+&G8ros51EhlQ7qe5X)bPyYOMG~7$ -$%|@w0DId={XOVYtOk|B;^h89Z?rLhm0?O9KQH000080Q-4XQwuT+rSA^_07*Fj03ZMW0B~t=FJE?LZ -e(wAFK}UFYhh<;Zf7rcWpZqs$7&8N|Y* -J6e6AR!;4IY`14wW4a9qu7g3t@cEr{g(apg?5hZjmT4MSFLGtSUCTU)l6lda<>^9{s|*jqjJ -=d`EGIG6ats0zvk$;}Q4YkLjOfdFAkLNzO$e%~60hui|_=hxB&TJI&Q;T9mKU6+JgmH9;esb4X)=9WFK`@LRos%C<1rhlbr4QXS2v3qEEZ -Cj81IvQ#&JdgjL5*7ATbL@U13_T)8K7{;Tu3-0xP!XVVA;mC^Wl;=&H57jp>Et&omdT}c0EY()23MD< -WJ<&_O*HG6J1Ur#6m0L}^ypN4_uXDK%yYy{+rA5&HI?~lSShIC-f&^%<@30@2~|d(tnOG^sLnFh1bRJ -QW&^wbUdBsxl;>&gyOMr^g=x@gCD#(sDMch2h-HCyqPR$xF-d$zu_?7nUoX#2I{pc|;xIhERIA}OP3< -ulY0h|jA&ZOgxL>Gv#!3U@X)T*z7$P9jzDC~>r>v&pQmKr$ZVJtgLRliD{u&igQj{_Y)vp3kG`uu8CK -eXa4M#j}iG_eVDGtL1Y?;IgYg?sDgit7+gn(8g26>f2YI3Tz2GYAwaw=xagiTDqE+}-amlv02aRZT(( -hgyCHGz4&W-{CLqM?WhBK3L&ea0L(lJE2L)>klhuLmlhL+;EAxBEA59VXkSNhi~-@fbaJ(T|D0 -NnoT!^pEaYzpsa5{rAiOeEG1V`cz4f?XR|W{79#HVu3MkIH>$oV3?nIt>&)lti{E6T{KlTqjP3y-jPc -u|VtBP{AoFFtkn5j0Q2?S$dz5+SU`L+k&-Th6-W}nvRu5wmZlcF5`rBe(qmY|eqd06!|mS -ba-`uCp?9{wd>PDrjGB9z>{T9g5MjF(p-4byC8V;BRsj(3=PqNAC4MOrMBbaV~d1j_AvW;8yJV -s0aXs`uV3fXGeaUB1|%tAv@jiLS`WPN#~$clprpJ#!t}mRiv(+cf0h23{J+o_EjD_5LDsd`A0l|Hs#E -i>zRrHJe5ViSo>hxyrAmN)G*XRAvp48+e8u+NKv7*wE)9t2Q93|vV*xQ`vf{*gqp850gcI6E|7{;jGM7-;IaIc@w -r4&aymj)WU&@RcF8!JZvmG5$vVqXODTbx4#txD`>^^y0G7EBZ$k;C+`6<~C7xJ`a%pV9_-O(MR0l_Sw -oKP^3!X8DiLQ<1&bG@9EU>gS*s(*yDMiZCfmu;C`J?B6D=j-+2d;j_AGiLqpd~f%M(G&lX|GaCfs*PF -u1x?}fnb96Y05m@YhSoQJ7X_CvV;!y?nu0Dp9_FyR(K0pauC)<9j_=Tl+01Qwd#G75Hd=mk0B#W(dZJ->}l4T`NyBk*UyhVzs~$wndZu -b=bVl%Q)DWdRqK20_E@B5i^3i`xLpPIiw9VFpV})ZnHLOga&(5_w!zeLIxcHG2L)wB9Cdk}&iQG+U0rCC3h?J&Sf}5C<07G4j-NZM -VS=O!hu2WJl1No9(@|LblY3C_Qx3)CWm*Y)9RGRtd*`oB%q+c<={ZJ4FBq1koMhSkPa)!i*KodI&1UE7)?Cy{}9GRwMFcyu84T*u>Ne>3mX -8u;@g*TcNsklr{HV5Y%uX_WM--+7~r`-Y60>6-oW9lEC-sgC6uyGL~lIZd~X)1G+Sydcw<6WhLdQc6_ -rH|xw}=uZ{iXIEyg6spEFBYCw#yPb~0{=H`d@F$DanCo>t2Df&}&ZZ;vedl}fX^rGeXY|)f-PQ#(^sr -XumS#E -#RFt)?N`+Z)uZ*XPuTnsp55IDhYE?nCAUV&}AEFn7+{XaZOBt{a1Ot8g89=MmF(&*l=?)>^wy_xwIb6 -5xUXY<~0vdGWq~zBl@U^NIh5fbt;E^pIA6m$Rm{t}9S|l@o!^VDvRk2)b6Xi7ub}Z9j&p?*#OZG$dA9 -#i|XW^Xz?76KtF+N**-T{^qHk-Z@p6rw^Fw>E@}vt){A(emeQ)KGMqB0 -Jg5|@`o%GJ&<%-Dh5nu=F!{Q{-#M`CbgmQ$EkVw4N+42zIBga=quxEc`j^cUMf?rji6kuH?DPV;GDr4 -a)wp+cs}R7@RNE|>X^fB}@rv__53#8a?AycFUZi6;3k6l+_&_I^x(8{2C9YOIAFjP?*S{w`Yyx@N#!%MBt1Qw=n;2|C6PmK>Uhge{i6_wvDi)x* -8PEHC(0l+lFs9&b-z!s1&;D(kP2Bv*lb-(&aM?lNx!@qtk7n7+JH$x=DaQJ(JHBOZin-i4kQZ7YVrO) -R)*l~F2=}2YxlJPvvv0V!SpmT2cG0GD6?2#&-rMYEH#Au(UsdkZ*72LtvNm4d!vii#5j_DA7Ti9oKBcMjm@S+T)5lhMYsP=3 -SPa~*s;+ieCszN&7VyeMos6hU?ZV+y_<>k&Bb%cGtrH`39Q7o*S~3PLe9$SVzd?n8yuK2Hw0pLJua(Y -&0)zcRt-eeQs`#5wmS}IMrXR1ie`>T>oc;ayIX#p+!DzHro4sUX{{mG#Wo9c6W3@d@6 -aWAK2mt$eR#SqWLh5$_004pj0015U003}la4%nWWo~3|axZXYa5XVEFJE72ZfSI1UoLQYHO##V!Y~wu -;r(32$8ji#gv1{N!BOXIjv;DoLv2n>QpLMh2bXtyUS>XJpHiC(s^C0@`xW+Gdx=Q?GWf17m`aEp?7?D -Vw&+EMLd;C$-17^AMCY(aqm@}I-4NZfj8L!tIE+bh#T=L+%ERw)Tx+xVbwwXgO9KQH000080Q-4XQ~N -##QYZxg0D%nv02=@R0B~t=FJE?LZe(wAFK}gWH8D3YVs&Y3WG--dtyfKN<2Dez>sJsi7LpdWf*y(jMt -#_BlWo!MB8Q@vAP{J2Y;z-#T9UGB1o`hf9FqEQHrXDUizRWs_~y+cvsf&C?L|{F&N{6=LuFZSgxs^+> -s$8Ik3X}6QV*s`7K=q9+D>W9xZZbM8;I{h#ivH?_vjm6m5ER0&|A?Y$xf@56(EmW${ALvfNOaaTFDlq -4Q40JyAHI8X1CB`LBC@??|IXJ4raB`R;gw%v(R?uU4l(z8Elsb1`5k);cHoBD127 -F=dSQeGwiD3Qi1XBwRyEXE%VQc|OuA$##aeB7^#HO?pU7D3m{gW!gVQe?X28apLQ(Fo1!MRgFzVtjVW -`=IlP_b<9$UZiv?z8;x7=ofJB9G8o9MTiWLgfE`S7H=OI$RlasIqWP71HkYf_G*4s2^*wWny@~~VY -K{)0b=#}mA&|TYWd0}`Ldh0ir-UoaB17pd!bpwH8p%@Yw7iZ`N4v{yPbtHnDLgS&GBT=v -$oIU4-0m%zH-*{3hY|f60u1`B0GIl~-95Hup)R@w-0`Kgrh&DpjqQZHq=Pk4PJOUeBHZO684B_ -7jaL?H{#{5q>(^LM9kdIH3stVj0V$b_#LjH@<8@Zm2I22@4wqW_XEF0MK>Rgt=Hh&?*biVH2FY22v0<;+|1zc_CBrSJvyV9IK2Z7Syr0rI{cqnks^X2w^88sSj -DoMoGab`EOB7505{!q{?D5I3q1YBe?o^h3Kq7dKMx!-*<8W~9C>YdVc`Q27l|mYee9ZjPJkB3}4MnX! -V-2DTQER~QtwB}-!4C0gvctiV(J0$`Nmy}?sQ`szH|wjM>#0~vAtPp}Sd&oLdNjizq -ELVVwTTQ6Belz8Ta;6mw~Rdk)$(ivT}>Ih+;mUB19r18_18w!vOk@BeKzLu$;j#S)yQW67u`qE1v=P- -of0hQvl33stxQ*c_x7^zDg^gCP#Xd|KVFPz}CM!;i^%X>$50k2Q!J?}0A;yy08TSS(I&ns7RC~C^U%Z -rewB9eXN;wAHA+Ce_<$Buqi7O-D*lhe4%c&jVk>3#2#-_lB!2a|%=b;-k;haesIAJpvtO}AORb0-g;7 -)qb<856Wk}Q_VYoS-Gt&m)wo&K%1Av~|FP;!$R3=O5#J%ojzq8mJ%416!JubKjz|244c?TWC# -7d1oVH$)q^5ymTxL5XY7$bD0jL!6-Uqtzmo874MiYMeBkxOVtvX-7mY!rj_9rNcJ -RdCJn+{f!EKucQ$*Hs2umszVUAgSDMmUtM9hM4|NlDm3ki!d^wi`~jm|(5;XVw&QqZ25I!@(I-5r@z? -1DI%IzSCnu_*33Yb{x33~>8%NjO2k4pfp>o%AA@6aWAK2mt$eR#S7On+w7P006`n000{R0 -03}la4%nWWo~3|axZXYa5XVEFJowBV{0yOdF@!;Z`(EyfA?R(c_=7juCiDNdFwzZUr35$2rOmQJc -QahfbES#VNW99~{rlJ8ExCmXG=Z917wrxUp?wE+=kT{2@u97`R)3cX$fZyahnSsA^BcUm&HA(7s$WFh -9{wU&x7OG^BRJa$t#S=4;CT7ufSur{)uv!W3EvYxy!Mw^CjXXB@11~;f>pm{io61LYIry}he=~pK;ah -mHw8?Yi$3&oa&$Z)zctE$L!<;u!6)LXDC(!PB0{Mk<;*IanT<^brn$O_GPAQ$SU1~@SMh=U7tBaK#Dp -`z4V!^%>a#^y^=f**bXH+bvKWP%DVBG~C#xM%PwOp;}aZAF&NCX)#lE0Uw{GrwnMxwJ0v&YY7lwX>NK -e^$~6J3E^Ycy1-QXj)up-*ZK!3n&b%tH`zQ9_6 -cl}ll1%Pa13Q4Yny*@1Y0(+PddBnpS<4!YfIAAF2(l~Lp|DC;FZ@etb=2s}K+r34Oe5F;L}sYN;-#s?2id>x`o79z&gMIa)qlg=_uA-)xg2~Oib -m2hv?jeYyI%G5z#{P8e~!<_kvo{c2g?zraT8MA2y%Q*_M@n;PKP>J1aLesph^_n=RO`%ec7F;x_1zbB -a4q*5e1W|+{>j= -7UGp&{Q1Nsly5;Qib&-_sA_w-P&M`22v537j9VV~z+Lx8x&r%*4W_@LK@ok}I#uquwEn_lkZTy9Uaj$MUg}gsF>KWXOU#n`X20rV* -pOtLIR+p!_XNqR-SkUxp)A%1w-IE^6wqq3Bc3H^P%{KU7Qq`1Uq#@MfUx$qxT@>eLs)~tOJ8*it2-HR -MCL0RCp&cUzNkx9iwrA=v{}gb$bh{Z>S?|B5EtoC{eR|78gXDjewfM|nnci$a*5@}E=Kp9mEWns%8<(XOEEPmBW1gG^xmLBNT%ft!>=L{oq<^ -)d(Jt$xFTtjQ(!Vv6s`6e77HqMpy?BoReE>msRC&B$E9?FxbBP#*pc%zv6bV%;fTV@lyRQ1u --5){(l!9-yqVpIqFhWL5txPTFllqG92k9AWI{0p$rxX-kJkcsvL2zJ4euJX5hQhAsY^Y`q@CAQku|v1FS%fJM{nI6GJC -9Z9tGNb-|nI(`Fjw$JuY&=~~@p;Tv3d7Xr;|3W>s;i?sofu&F=ie6b)@IqEOOQCsUczzQ83$ahJnmV_ -v7ZMZ^#ugF*M?*lbp1|OZZVArbw{9-zIF3=H_i8r=n0or64rbd?0n#JeIZ1NXSO9KQH000080Q-4XQw -_0J(252C00IyI03HAU0B~t=FJE?LZe(wAFK}gWH8D3YV{dG4a%^vBE^v8$S7C44HW2;pUvY3SL|$AaO -+Re#mH}Skw8lIKu#*)i`Ck~<;D9;xMc}{RO;W -(3C>M|EmShaoh>d8C}E}#|8Q-Dl5iPcKtu-JFxBn-23u7@!M2Dbm4(W1gb)TYk#pcr2`?=OiQX1vE|^ -TX55_k5F?{h5pdKv~5|J21hwoY24w!WgudOWy=jWTvCT7mjSn1Vy2-%!Z#@D0ieDn^Tb>>g3IXBcnW0I6aUy2Ck)=Uw=7I -3LgZ@Ogay>F(hku|5xHv*Gl9JetGZ46g5{H{<*9-4x$$VL1H)e~qU%ec(tFZo{8SP5z^kf+)m8g*hjZ -d2@KA73YvCc_K2AU|e!lu@#4v`p&f^5tUpQ!q7n)bT37b6v8q~?}&616zU8&RN9glR|rtQnyu+{tlkb -ffKM1qP?_9x!hw(7;7Dtwy_KJaE0`+DJDpC-GguglTk3_WO0qT!61a|fVFkV5f?Jjw?sv*+81jD23?_ -$;(3{u;_2E>|*!HyNESj0vkJr%j?EJ4E4;>qBtI{Gx;^*XMk7g~Aq`RHQ8<%$HxgpQe4jM615dP7Iv_Az8@kV%(e3bIa=*B~o -7~Oj=pDbw228I2YhLbG4qzlH|4lfiz)ZYvPZ^DggV+O -N9|}xF$|=uoV?4G&vZOEBPVDPH9D6Ge$(kn9hYV7KM){sR?!IV7=R*52$jKQ;yv -=aW&2#H)?#0-7rlBmAgJ{u>Uh@Q8))Es*i>62Wdi}TWlA#LvR@XXc%qNNK@PFjz;?P!`m|YGnTtCG4o{3+<@|S -ndk`?^rH1_*MVf-qEFKv(ViY!D70RRBy1ONaW0001RX>c!Jc4cm4Z*nhiWpFhyH!ov -vZE#_9E^v9RQ(sTpFc5$Dr#P91rYK~rou&#=p0;XCTZ=@eJxo*NHWzB<_%g>O^xI=6A?ZS4NSmL|&fV -{y9nSgH&TdG<=*<|`l;y(8l~mI1IOj}eIT;I*9;@4e^kZG@3dYzGd=ffQ2nIzW^$oZkltx-#f4*MKL2 -CH6oSjTUa5N3uxM~|kjLFmkMR#ZlNjjD~r5~q1;bIEoGX`!QwKLT=!L)+M*lP)BbsK^cy$Rv?_*W^+O -);$J{3@m00OhEIx@AxfLO{JHuXH-t+Vo^H7=kDu?S}mF*io|0@|qysLuaiF@3>as(O;r9ucS3-w?^0O -ibN8mjt*6Uy(mWiBZ}24o%egaSEM*Fs-@%LazG8nES37rMntqEjn7uoF+hx@@IFEZlIRTQQCEnZ1#djm}98I -tvKX>s@cnBIxB{fbtAGX77B)vF0F+tgNr&0^0dJ|=opcU&)(A?@1gG*O`Kqsb-(R*DkzJA$1`qOata4 -W1_=23iyIw2EzT-i_Vx#0ee@Uo5vP)h>@6aWAK2mt$eR#Q9AxJJ(b003zO0015U003}la4%nWWo~3|a -xZXYa5XVEFJx(QbZ>8Lb1ras#Ztj;+b|5h`zy3u;$)t79}r+zkpTq;^w1rKT~Z~c8#cCNPz=)S>qkm< -oV2^o7gOTn<9npA>jSX~vA`T*7^7YA46oeccD!Ne`UYDV&vAcbY{wH2`AhL1%*dknI`P;c?3{PcKt&B -r;;fO%#h1d!H=E7w2pofPDHX`k*$4o393(<7iGjupJXVTQG1q2w5oxX-mZt{37$r3xX^rZ1O{o?~T}%fm^dN%;>vfO$%=r^;-PZ0Z2f_`rr7M)BkgyzZc94Bnua-$ -SaM{JB=#*JpuYM2507i2K*mMAnK(p&oNZd1`LN0xwCnY+`^TRXDs@x&! -x(f395%y-OA3LnmKS_vI__A#qW{!Er?v!a6m1%nG5Ni$ZhLGRAXy&<`|ZpS?;nQ6N$Dn)uEs}Ye`Eh$ -Tj!$6%un--Rd&@=X$P5xqNBbU8PmzNhikDTzIVJ=h*sR1ZsH~-S6Z%-eF^Bx+(6HZ+ZjRo7B5%&F9)V -7iMy>D?b#Ed*k&p(JRi;c^IY7`7ylHT%>8%8SIB`aqlfo7f{VXUO9KQH000080Q-4XQxxznc(?>=4vt+4u^vQ!zo1=ub;TApEADN0bKnm_oZUlAQk*T -WVtN_kExWz%*J~fKZP+cXx1O4S$vc+&r`Om*Y2nai%9LwJkm>9M(gRrQ-VW-YIYLy11zp56+dKQ#-qf -{zKEzOsAzbsY4PVDe_n9@GzTtJ*klQ6P;pt}C=B!HK*Ml67z`4c61_=S96`_DXsk -w(mm;QN!o+wwAn^CX)E?|M;RVe{dZhm2a)Rnje&a0f$l1hd;3lYtHMVFzcN^Isrlngk*zI!St+~b{yK_;!p(}rj@tSv=SPCzAi%dOVmV -&V3M&G&iX*O9djOL>RWFgCAd-L&NglHZP)Wk$5={~*+!ZMSQ?;iCWw-qP1_C>NT*J0V>ek${0{{c-dq4>FNfiOW{Vq_B7E3;9yF15a<3h{K9kC_KnMN2qpLtxa2ayN;E*u*A%RmHO!|Zt4%v7&!Trp -4h`S+c-}}|QcCN{Awt@GdP3~Rr*t>wh3V*f=9*B2CW~&GCi@Un6PsI7#+IKb^;`zkAT`yhd+9vL5zP! -73SD(ohNUywrEM2%s03CRGTLb7iAa`ScZXJKVfWg_7yL5vGo7}j;3XyLBdPdeWKXB)F%b8EscmCSjI6 -&pv$gaGVySnjVN9WdA1ruNZW5oFtUdU!KTQ2ps<_03yxwn4s-Or1FEWG8l1A|wN0b9DWtEE$I3(C%yG -xyde*R$K%XGhC=faU9DhUxvn(L=E74F2bV>#YnJSZwaC0v`r#(B2OkfO~i2*ktCr8#L67@4+T$Y>@CY -6v(ZdE*2g~|I}b1zTa(}me;j2TLR(+A-anvRpi6y3;AHhb6O4P{1!s^P+A9rcDu -)T(BD@^&onffLpC;&RZd#Zf(6K;HOA+pju5!=_hcWyn^rx92Et^fP(mqmpdf7J$|5W!BS690f?QT(V* --^Mg<^n9_8L|;vEINy!GKA;sg*{3e49K+LSFvfFCey%S<1fR{c_1B2H3_d($W{8R&WIrr5nN9Bm~TpFkTz>uwPqd6OnUuAM`n{Pe5^Lw@@WeQZ>qK!FFX@y386TP5Vm7UlF1)FQ -?M5qv<0Ht}1Ig{m((skAa2G}&kreng|ttum%#-=X%j4dHI>s|V~RP@fou6YNzQ?=@-rO7rj@gV`tAA7 -J^Khswlmnzz3aMi7ggN5A#EAYpF;p4>x+vQx)3o}NOlLi7-m;lp>mgrqjVX4sxlct61w==-74=PmxrQ -Kz(u^NHycGFftX7j$s~;%NhMcNz*R?{ZKacc!Z#_~yR$n7RNtFV>lb4!E({xtfSE5RQ=&uYA7c>&|A_Cd~4kbw -`@p`YFHGxWo>^XmurrMeok%$i9?}D`ZPoW?`K>!w}g(LmX0;ebq^C=0i6;vG%h%K?8(ApWgEbmGu(4C -!XJ0)IGbqXL`U>gpQjYXYtTH@HQ)yyb2NfJKUjDA^{D4TXTi-S)gOc2e9zFIZ@`CJ9pGisOd-d -Vid18kmtQQ9?>IDjmux|GP(*;qB#3!SI*1;?S#oGeeS&Rt3!q_};?k*{5LjV6CzDy~SPcY -qoZT)swj#M4or*|BE>vtq-Mwnq53l1@WskMeSe&-GOcuxH83iR{ -oUv@)Wm?`6<} -+z07~nh!gY?3!G5HFiXNSfQ>I@p+sMHJSfiIP@7Z#0~InV=!wUI}ti$yxA@4PO}dsLKFd+ZgXnc -35`cRl3X&au10edx+A~2|CV|u1 -SEGH_`QNw)smY0LN;W-WQ0>3sz`X|2~~dbo458jkuV>_4zH3)BRkBM_F{LHSHNYg$O-y!w|ToLUTd_M -O4DUJUee!>#+#wv)Wz>YZ?`C$ZHSB$vx@&7#iz6kmBst``J~ztUDZyW0m}%HYlkHCXhdsfp{kHLN^wA -BJjoFA(>;<9g@G_Rz9=83g=Tr7goTB`6+W8j5oNm_D}|sOLX@6aWAK2mt$eR#O&Lc!u -2!004C~0015U003}la4%nWWo~3|axZXYa5XVEFL!cbaByXEb1ras?Hg;4+cxsMe+6&z!SX>7+5l|;13 -p|{MHkn{BH3I%EEWn^qHT6%QWd3jjH3VjX7~^(N_Mho+FtHzU}Kpa4(B~Xal&`CY+BN88%9emHnVjjc -chS_W$)UG-w>Tc>r7EGyrqb)0L -^MY1YYM*7-)?J&DSC`L!e0|wtIoQ2hwyW2?Vh+LXw#1>Cnaw-{JD|x9a*;>d=5{DoMZ@bhNBU>8JO?` -RoUF*zjKF99loq#4l&=N7qr9Rw6`QAnnR9VRD_*+MZ>7Bbo+|~CbwjU(>T2ARl|R<3p)Hrf&6R9TyQ< -B-Sn{S5f;A4{=Z>=z(-*A%>dVKt=`$c)k6+Jbvwud~gKf0fO5P-krC!>Wz@!yjvM@ulr^4dsfJgadh7MsCbnQcxjei}OZV2 -q84Y7C0qV6dB;r_>j9C^vT;iPZX=xDX>BdhUgXn%`lVo6Tp4{920MKdVj(>n3q|5iZ~0M3%XAk1K<%BY0rDZB-3X1qJoQ2dO!xyNAO+bOV6B{!T0sC!bI4u?K9K}_rkcJ**01%eh8*< -cz(fDsb1~(0Q|x?C4*@qf@T22ZnX8hkcBEcW@`AQZ!6=2<7A3B`T7zhUnA~#$D1k1B@)mbi$ogQz1PEoYuV)MfvaT*cLd -gIEq61F*NbmW8zZBQ4;BR5sKN9oq!~ByEW|{1_m`l)D8fNU+dcf -=g=*iIQDYupGm1N*eZa#}rt+7sL+$4)I7aLNT!78bW)6#;}kXf?j~XG|U(_N~*^-0LBQXYF6+yhZ#up -6aXu5t~;jZOq|+LP!Ppyu=nH-I -%#;&F%iGngKbH<0ou$QFdy0m)F)?4$rgUGsNjEgN!i04o>dQTEu@JN$-Mygg*(l_Yy+v54LX3aT_RdM -9~_i4^klP65va9pUGw-C|22IJHni7*sbD!=&D;;R?21K1}qKw#$)_a1{3p*j#wv;w1}B)EPu$Z8RjY&pdHWmdKl*SkZ>T$%P@>NN@-cv)u#gGk}!M0d)ivU@mIO -o`d62a`EDV0AlBl)?JpM2)eTfazL;o82Poc0uuC>7f_hjk|QPkS4a`uiDU5gkDUDn -(DFgj(4Vm7$LV!>z!SIu3?j`j$z+KC#l6z3e(ky*QAT2C0c48$0p-;69%qlQG%v -4OYE~%j}V93>f&8FMI9L8NMd_ykNg51jv^@jGLEa@!UWMm!)!l&tZ<|x@~N(=>K0`{`G#pl3$QQ+2(E -E!r<7_+oV`t8Uw*#Np)ic&d?by5n<+L1C$w?Gt>WUv*e6DhFcC;@T@5lSlw>q3qC4qaVYdLZu?+uqj| -YbS`avV+_+%rb*QueWhm+oi*io`vD~z^kIEz#~ktGnh8^G%+zU+W;m=e_)0N1%`NpSWcjakbJZZoefJ -_&mk;|(jp8-W&40{JfeiZLmCHh;i!E2qk8y>i=-iyDXAA)*)<+3Cyz+@x&FwL)mUBtZUq;xhbP=|oH? -yeK%@>(i1q`+ou}YOV@7%L8v-0ds~%U~<{V>w1V*Ium_XeiNUIHU -2C_?{i1PumV3K-1`fE@SX!d`dvpqGJ4r@8aYxo^HHHcb>?eEp-Ny%OK>PTTjQckTy&TETuLkAmXRJ4o -=Stvl7+Idm53n^V|I>5;bcBR2PB|heqDG{H*m

IH(f%0Y9v@x=X>vC3*4^t8cch0MmY5^c8D=Yd)hhS-8o -to7SC>r}8P|oO1#IN)+rMY{M&=)Hg9ONBr;q -?`@Q|?Gd5h)t-{I@OsyiH8=hfDQ()&fuG5Zb;=l*$P=)uQ`Y=mhdSvy5e -I?eL^HqDYbKp^BDhcb&XMHAO0LV!PhU~MLEPg0tG4*vY6~agKcKwmE#tDyg&8$OwI9MTWcIQ& -d;gbIA!<0bHx#d&_Ap8=oR@#mLGxjyr%;3{-yrME0E(J^;s7o`ZoWwc&FjZ3@kn#ef@t`Y-lxos!C(5 -dHwvpV0t7@ntqT^9v-zyQ7|eDhei&4!tUk%(egei-bgHgXGb%?!Lvif1cXIxK`FW2(`KUr9XudEivE^ -6#kbjD%Dwr%7QB$)1Lk6fB<4;BMT&PyMY{ki(6?&=zVM33--7u|FHjgWH&OPM)Lna-O)~N(#0D^sky^ -54P(|Jx06P$b(4B52dGhMn`8i3V<~AY)nuI|gAge%btby22@(l-X#hLTMu -5tSjGIu#F=;9ZaYH;bBqDYuI)7)`g#Jq8_gUZTMSD^A -bvGuI)t)K9XZyMS6^I{6@ALmbBpMUo} -fBNLvzh3RJGEeF%>>^%`Budd_f?aO>`M+Os1m3y&Mt#2pA^1!e=qTcL48R3Tc4{+527_12hhJXnR{&}QNx4**MzBv5S3y;<& -;mKyze@`$P$FIa|+e6R(15ir?1QY-O00;p4c~(=R -9NGvZ0RR9q0ssIh0001RX>c!Jc4cm4Z*nhiWpFhyH!o>!UvP47V`X!5FJE72ZfSI1UoLQYjgd`mgfI+ -+_c?{dW0VRn=t-0{L+uOP8E~bytKAy;}r9M}$ -5wdL>><7e$a&+T?VEff#IvHZggWTwIO0WWhHd8EKh%5Opg!0i2U=n?xd^oALkx_eLSNjIO8m5U(&(zf -353wo_98x@?BAhdEqFg)k>#ZB12GW?-6u4xIg>;6^v&8C)l&euftcaT{7TyT(&gyy+CWPuWqKUCi8Uj -Pm+mAg|+oe`P_-D70|?;^KKfn60_T;U0+<&mpNT5OuSgmBpwF$1mkZUt4=*X6+eHKVv;L25%md!t+M) -#7w*!KK|nVFT@%!|CcS1`bexk0?Q~vm@-Gx{6+EsP)h>@6aWAK2mt$eR#UVSBHl;~006-&001li003} -la4%nWWo~3|axZXYa5XVEFKKRHaB^>BWpi^cUukY%aB^>BWpi^baCzNYdvDuD68~SHVxgcQnUk3&#i5 -1oRKQ8(i!X^2xDJXt#m7?O%G#LZ^0=gBHOP1G%)Yp!C|Qo(;BbOriRA3;Jbv@oT@HuC^Cc4m|MF{oMH -BcYF%k8wDEXWUK`Kt>ahj59Ny)5cX+mc4{EFrYS#o|Q!olFABJ`%9^GX~J4#*3hWidQWSx!k@UW!9W8 -0B$BM`X_Ps^n>uuo7q|`86wfp3%G_*Kx_>SxSKkk)QXI_kVr*WAyyP$(xta^P?B9C#NsRKv}NCg0s9j -6fkFpR#8RokIr9>i?~_};tN{DWn6(~D^@KD7omJWivR?0#CgKIL-IKc!!KV -3gEK1XRH^BXX9X_>N1b6;}k~Y;K6V>94tznk;|0N2+ImyR`R#Ht8S#sK2l#%#*;dw1ASA@pnr -4G{E{juM*70A`NYwmf)x0}3@9x_v8*eW+N-tYS(WjeqN4|cM?^wo!BUDiDQn3PFfuP$QH=;X`!_vo&N -G<(H>AsXMSf?+fruC2cfrYu0xOo!(>kGKRkDgksE;2=4ke8fb1fB7tioa)4j#e4S3m>;AQ{Uq={6mEl|1enwEm`JYxBRPrtt)Kaa@|3T}_=7xevltWXgcm=cYvg66&=@mAi5(0tBO(y>=pi-VuM0FAb22H^*Jhe@3kYJ!EsePG3C41?TvO_xYFJL;F3feKnvxhLnY){R*g5TE`{qy#!KA5L5B)aD+t$oPj>zJjOiu}pVoE8-zMo$Gnd=@i-SrU -vwAu;9%a}zU|Ar%vhb|Z8IaAQ>zK_*H;Q5#u~`1mBW56IXJzqWKIIph?U?3T5R^ib|<$Qf12Lw(vlHLIp3`HO-sRp;6Ru -1_^Wq4|85-^H@;d+=yoazpWI3Qo}OaXKW+48mt4tkU`STcwUrr!EXF%kGdBD8IPMwZKOyx29d^`-`M1 -sfwyHh0KvA|LKy8Ik)s4VkAteD4Rp;cjLI%V;sd$`D0nKHONr%pJtC5oPg;Sh&LX^|mGAX*2cA+N+~t -mKz(j8#1m!)*e3<~IE?p>!lvTdlOISc@!KQ~$U&dWNDwWZqCFIoWYjwN}Nec}?q;gsWrET`f=tv8grm -PqSp6?xMeV{pNbO34~Y&8}NhW2<%VuxXfQujKEWOpB@BqU-&ewh`B -e&uUB!*~3*hP`5GC_+NCM2r6$u}%(Xo610chxZ2bEvP$DprjO!V>~jHXd6317tmH)H1ej*c)jgbOefI -NHjdW(CY}JJOY@EtEy~DsbtWO$Jr+nT9DSzJ7Ay%jo~2`PJEpyatw`>2^kq&=3=OPnwnS!WVUuoL#R| -!EU=+797I(@_7p0#&S3!0(={nLBvtS~H0o(B)0?NFo+hk9V0fXC%8g0Tx0@YQ!ZYHH$@Ie5co_?68BI -DO!}b(I$DW2k7#9Vy-5_=CX^6E^!2?lA|64%%-Stmr!opAAzmetgdJ`^p(-HUz*O)d4@A?SZcR*jb|Rjz=qlXn -vIQI-@wdJr6F(7D1&s8#8tcjWXsG_*t#tj;x1DtFVK4NRb)D_4X;>_+%~WB2Go?4;=&#?i(!fxNK(p1 -kfFGY`!Ge@3@VrT&&7Q^ue5dtLl4rT^GWRK7B>YZL*s>xLT$ -lu`u1D5zTe{9{vuxNQ)5Aa=4=%ltazZopsa)sFwVS!5l(Ih!24{Fi<#Nj=kRC(r@Q%|W>a5zkdlV5(3&s((wqM0$;Xcue>u?K9~&W_ -c-9^k+hutvA#Qhh{NX9@QGLPdJW;xU>O+%_Z$V*iRqL%U8-BJvnczZjuD0L -IK(&(7e$We@W0Mon^6((TyR^y7uK5?4)k -_vaU%&TB~QSzKPBBbcc&Xj3UkvoExDI@kU|BdZqPH>~8IDk|6rVPgiXL^pttw80}@g;conf_|Y|IKt$GCstd)Yq)!lLm1%& -Ex{}g2fbiU@Z7OX9LnIz&s{v6vE0*6OfGR6WM~ -{J_%jM*NoR?NW4x_^L}M%st!b*jyv2Uncm!-+yLOyHGx~roGzlR*bRo=eqKRsSBts{3wL+Cy|l*D*;w -uuclRoEqC8pH2*CZZb`+?6?13`s?$wa(Y)3FecbO|`K3=Q|tDv#~s}df@SZ!?Yi$dze?Z#7~NHFqJ%r -$oja)S|zVJ8j7Z1y8*VnCU~*?`xZXT84Oyp!4)5gRy_>5(?OjOOMbN{d$}WO>_GcarS -C;L=LhByuoUhpS*|J<|x4rj9`%c7vE$Z)xe1}~2?_UGCTXq!%b_4&vd$v-&KY24z9qC3d>nvM0ZGO+O -?kUG}%u7HHuc2wC*AQU}%@~AQLKuPOFl_(9s}H$7f_PU@h|DeQk2$WODq5nGYC%<#Zfq(i5~PpWL!P! -biBcg;{@-gIvZKUGCMp$v4OF!M+9_vG@m%bfgC~2u;`Lb*mXRGS1l{Dd&8b_D1_!U+y?Y&<9=)CX0vd -mo?7UrZ=&6F|-PU6mx}ICN?9lHqLzlGw?yksfk8IuQIoN|oUo-@>7+Py>cir3K$#Ps?UI*mw=B*`(X1 -GmnU=1`pr8E&cU%AtVV($A6hU@gH${Y^0`}A3JMrQ}hS6_0{xVBHI5BXKjS1S1&lJCTjeAlpO#L-KOI -2C>{wa+gGjlA;pJh>NQ-vN^A4@Y1Cbn_CQXO5)tBI0uFXRmHx>m91CQG8p-zODy1vBmka8H9$-)TO@A -Z;belA2&iyJ@w*}chQQz&7*W&Ow+y@a&FRroZ-BzX%Cf+Hc3RAl-4t=KKb4{qdBzK?+Wdj){s&xKzeH -PevmWqmK?EF0@pM5RodO&-Zr1~mwqW}*MyoRvY^xxH*o-E%Xw=EM?3G!8Ac=02Uq9KQ7;%lG~xc@_!( -wQhP4b?MiIVp<3$nXgedZqGAcm~{s2%*0|XQR000O8`*~JVz>+DQ#B%@u|E2)|B>(^baA|NaUv_0~WN -&gWaBF8@a%FRGb#h~6b1z?CX>MtBUtcb8dDOk@dK*WQAo`zAQB95y02wSvc6+B+Ub;susk^mfOI%5|r --$YOMW8@d6Rd(!fGF7GM?2qnr1J_Vu9=ZpSs-Qi>^FyBw?$TEMcyJK;~q!ZteIE!<&$i?Ssef2;PCM9 -;Ix=s7nfzWF570io|W0t)0YRwiJyamr@m3uW<|DaE~{Cw%of$M1jzNKsOoZ_-Bz2cs)qL~20hN2wQqg -UZZGE5x}0sA^)B07mFu!u -?91R(4Us`oc)s76wq&&=$LbdE^HNy;FqMoR@8NiK_=++&1fL9(>)bZ)AbnUArl7^!s*vke8R2{AQdL^ -*r0w_zocW7ciBVn`~CVW-MEQOFOfyZYo-#Vn-bvPL97pK|hk3NItD(2-HrVneF -wcFLIS#M}^)9Gconaqn#F&s^&2RG&Bs+s46TnCFaK$uJx+s$@eP9|A(L!T8FZL{2N$_f8{a3IfNlKN} -oeybY&ZByNp`up1bw%M&p*BIu3(=dNZ7xLh?Sl6&au6?^H+`GlL2CQtB?&a$Ga=WRPZb*a!&Dy;N1X# -ch>EE+vxdc>2B=JqEcC)N5^zZVfsFr|H>$Y@Y%JsVTQ!HhRv+jAZG_B6cLBGJe}%yuY}UIc2O0dxTYPm;ewdZ3P4y+N%s0Jx0kt8t9o@OUUhmf1a?e87aB$bK?Rw_M*=pTf6c@`K0EH8o{pIP4B-}s4p)U{OQ3Dd!xG7 -CL!C&LNU3%Bld4`c@Ce_1G2(S!7ZseMH`^M|2<^x4&{2#pkRia!6R+D$Mmj$nQ4e@tPyhCrV*k6V1^`` -b;he(F#oJ7`Z~lapf(v};2RLtj-0&a}=#9B=7vM2#Z^|dky?W -EnT%i>0#&z5CTZ&wrdKK@_=DB5`z!hlgs;BHwLbYV^C_{U!m9Z11J9!DPIg){*yTkj^T24>zJ3(hJ92 -d_CizMU5(k$rs(b7`4y;5cFBi3`z}#B~7{mlGfs^JV&#GEHum4+yuBwfX(;AAQd(!BDVO{uD$;E(ZVo -7Wui`E1;dkvJwMa!++at$=p(r$#FpBc88VtNF*!E9j+PiT7;ayR6*Cd>*L@6uqq?QU?PBq?_~^*0Mp4 -T;hL(m4iU+}b6-(gp@`jfFYEWkvYJctdjS5ryewur10DzjvQ041f|mQ|Uu5_bIXnD%2(OWH0Q3Fe`c~ -#52WmLVx2rief;aTSfYev>YPQLd&R&N@3%8aC(}U?Ih@y# -glRvm-3{94e+PYD-epK|YPJZuLsAYE))O9OeoAJU3&WG?V4h10ek -A}vLYY7{&RQk;A;6XlXR=c5FQ0j>q!=i)a28@!OL35EK@mb68;ZNBsZWMIcT>QQ4>onZBpmX`GulvN} -5<+5+H5_hOVumAsEQ_0qd67LR)|c%QJ^t7mejqGWIiKj=KJ@4Q4G$ND+16eCgBJM457o`~#&NwM`|3$ -KpihxrA)5_&4FAos9}itbI=HviC3fA?m2p$m1XBR6l#SFOK?BC@2X|~Hb-amR{A|LetgwzLc*x} -x2>0SipaK4?v%Zpt!nk@U%c0(d?b_+zSs5eZ|W>@75FQ$->M+yhUp)H7Bl#!r2|;hyR1R7?zTB5>2Nx$O-qnm6TSvP_$I(4J7UxJO1FtNWez(5{BN^Hy~}dv3BJNY -k^mTj9FE08dXJYi@U2ZN#P=TKL}}`y?9<@)0xo -7tEpSl8k^(jnGkPYR;h&1Ont45n3K=kU%P>47)+8g=1YgWR6coW^AUHP|!>`+V($3b^YJ-2#kzF_MD_ -}MBZwquVW&o%XvAmDAGamkk|FEe?$|1XV|ZHpvxlG6!6EY&EF_I3!HMGI4wI`;XgA75v0ZK^Gmz>^Km5~m1#-*|Ww9m6 -J~WPuxV@6mYG9^_-F(k8c*_%bBb|Ly)Ih4-z@G=3kIkHPg@3-leLQ6ha4YFV6$XKjmjr;$1AZo_`dL# -ifD95O+Yv}9e{MiD)n4jbQltu6`;7PZf&Yxa@ERUzGE*o|T7qh=5J0H;R6L`OD<_%eU_k*q>g$d -iv&X?$5WczkT!Uh5Pf_Utc`?*Vo^^a}S=s`0mBmuTSM~?fLLjDAi#`g69H`CfrKl5fB~my1c}3(xZpV -rn%m(PysDNd-)AW{iw%1HMH-g9trypm2=uy_u~}S!#{(GUS0-&-mEq|3!}sbhRiaQSKH;%wgM>j&*yy -Z?W2FMR;LjVo+c1jmKK@d$)k?I7`$aA_XLFuPqaP&73vC;O50U~+I#-4FLepq=OtnWEq~`g>br9O@B+ -HHE!OjhBSUqEOc1WCo1&sq_w;>HEekk9&qKm`aPH*Xi1rK}ZSu_S(~FjXba)s>g*vEr93?f@&hMsB=ZE=C=J5}yLsF4`ogvIAiTKckOwWX?x!xDj0(M#GOJ^QUOtfO~PiJFf7gE -tkM@DdRR17%g*j$P*$NE8C54BrM`v$G8DAzNzSt&;4f}c5u}3R0U88mbo8{h>yQaP{F@s3H9)2nmk^J -zaanSy8z%BzXZ6y1X?l%^Dl62R^?jM?YIjFW#10q5}!Rf@B19!-}&|9^8?c!v3~EC>r8W`>&0gH-~n0 -OfN>moIJmwOf~Z)KNoGkfPWfPpr{%cUW@`u>+^!>;d+&J^5Z0o^>y&no-Hi-w$7G;6<%R^OtTvqSPntUZ#z3e! -x2{4Gj%~qwVZcqO<`R3{WeEkMR|DS)!fBt3QrWsp#MhD8iLgUX54F3v$e|BIPSor(#LBPSnlSc=Zg@p -&lk3|`mV!(#xcQSg~;qR=Tj=#;OahoaoYFwA@YgYJ74df|rVDN}^U$BPw2deXVuV7&d=M6EP-Zb;=vI -KrczMM>_%rWBWObmrwW}KP`Y35k{M2hSILwk_1_H%4BAY^EmPCI<~bV_p};wxhb=zL!;(R_kz^Qxu00wg8d)7WxQ-Lqu04mq;;!5~dvL!>SC-3X}~ -0t$3rGR70qneiX(nfdHol$E5?`qy$7DYT4j)%Nv~;CX9U8;F*2fd%BtV;B%K)b9nVev)w}AZD%L2n-X -OJ{Hg;f#|gDR+q<$l#{b_S!|)e{Mb)bS*jDK21OqL1Tl0=i$5S$!}X71457m}#q1Py-Il;RP(XDCu^t -l6Gg706Z%HnH_;q!$F4nt;&zg1lwp_ojK$Mbq!Z_sg%^&~+orqsU&Wf)gH5!GTNz1FErBzpl9TEzQ`6 -+=-eB#E*%xk@&HQ{e__oeih{2DqPoRYGBBa;}Qo;d)T?l;~E^TAmRQZ8V^Q`*cNZcC8shARA!as@TKq -Y$mAF);xFwiN5x)lhsXLbJ}#gVW}=T%SH0Jjn*D)eLW5dNP6SM^Cu+O5gl~MiGUAo;VE7ke^r`ewR^3 -^j3&$`t(?gu(N=>YI!~W?ce?_d&V|{<&L2V&2P~n4IJ2Xy4Q|_K7}ZyRcy(q%W~eb%fJ<2Jzq0lSM~M --T?eY^4ky2V6;NoZ;Ja`NbI$tCaHE -i|~W(r(j*i+3qum$TfM -{QBHEQf1d+u;xMPe(uG^w04ye=vfr;1Tcrnwtb)M?j9~07Yu51B;+Zvz8q2l-hSF7uVd&|NhWFD^Qa& -FCA~8?A3hg?QyU<88KGyT0!{Ne>@Py46{_5^|r*#EvwM(SK-0OI~LEkbmqoLHco(CEOSpqUeU=96?J4 -$jcEW|7`JuxVF$FJY(rWAW6?R`Ajpj88JXajYj<+u-g~?~%F3dJArx&HvrXh^DB4D6za=V(ww^*QBmf -iN?lC>O9nzumCw>q(#5-WVHT&I37Oix -8ZNd*f?|%6CD5*E;3}3h(M{j^O@c7xk2UnM9q0%0pMZpXdwE|qozM|(biri;J36h1>HYTUuY#0(Ub;G -!9GIZBF=yt?v2WR#lBDXg|7~_neke$NcL0>m$n+Y{_Q}@x7&1+lo6Un9q!c@R1p|4w!1knR(fjU;$+< -Y++zc6mYUXgWsHc)fP;9R~A{I9|m_zC=K6|{a@D4*qKzGjGHfP>8UQGBDTOTU_%kWp|D47Y9>duz;-XU?&_a@{uIX2cFDG88&_<19pI*_kOLsdbZS7TJ_OVGR>u`INX38oP0 -_*WHnkq;VndjLMG3ppkQ%vBN$gjYn7hD}y1!M?6*@x8vOwTe!TrAvC$)aKoVx!0sqf8+wn`#W8sA5R3 -0J(ocE)X~4>Pe(W%5EYX^>%Cck8f8w;&S4B%+{uH-m#xCQ3-jsYJ25QkLlLYgtU!5eGvGe)w=t)1w0Gbk}=FsRu^b^@|VXjADx1Z22q -g99?{uRXO+HXXxGxI9#lEY#NcX5^J)Dozr8?f9~!g{kvQXt$HDXh#v=fwjCVq`0x#H*z0WUKALow+Fh -t5!UmC{fLBCWg0H8ZrBm|J>1HV8=&|Ry2u1-| -$z&B2llB5$GR0VkE>0akyuMF-1<0VEsBI!J)6`zAgfy;fdw1AsVqC>b`E3dV~?)A4fgp5PNSZV#2Yq}@GsZA#6AS8y~F>x1F*;+nDdVU6&50pu)RpdfJw}W0i)SG;s -4MiV#qT?7@*AucHYtxhL?7aJCH^DF&ANb;4UeKGwi<#JH1fmt^v@20Ng%X<|hmO^Mi$|LIEezM=hT+% -j9WWP;a7h1EM+MqM4j}i-%{s)J0&Tp**|H56+5LXH#vp#a2)MCb`LsvREly&#w2JMHDqKR?QU37(>=X -5dZ6}jho7@RP)t@6uazKIyM&7sI0{G5WBFcZvv23SFrhnJR(VT#WdX^$~-P;EP?G@N`+6|Q8sg6 -k@2A3WZ=55O%VAwdaaVn%)vj#+1HQccnp4_#w8>{>3jtB-!666f%pI%}g4p6bYE#Put+c9?=POe06BU -Gudi=`Zg74+_WVDV@8PL=fKtUwL^wEpXcz$2sQVtt4Ttn+#OosLoa`Wo%a(?txzDiS6zk_ ->wEN}|`hv@B*n}HRs70(sl|h52m;SmbO9|an{eFC@VEDHS!lYA;qvRmWF|PD|$0#EsOt1WnPMia0HNw -F-yXVx7WDNKm-QY|+NRv0ulWSHtAG>C%xbhriD@$klu$Uq$)fiI%HX({-Mfqez(T(&_ZVUJ*=CjaLfV -UJ;4OEYXYzwI|W3OpGLu>1~6)0F0Wjm33g1<%qmyiWm9~Ed>Nd}f%Q}wM5zo(IG2^%DuT4Bk@8AVAUt -zBBUBsV$rEipXCG#TDILS_w%G-kWXEiMpc*MOc{H!26_qCp*&bVB})JT|;VXjp(9Lt!esvVV;#lr%ob -N{0?0G^>yoH#sKm$1#+cdJ2y?<9b7xq=pHW&{Wuq6`lo?58oiZhfDulo -F)}(!wB>cILHHq5wT9g<{U{&HreU>HsvA)#m4ba|gVbVnd2^tSk$5q;$lMqdDZBRQp-?E>#qlqaD_D2 -SVEkZ`dJiXK8j~W%YGl}iNoC||c9=|P(V*qhR%0JyHRkmeWhvGKhnZOhZ-lo}GVxORq(6xxq=(55`G? -ZdZaRNm~9($aUq|+2RfU4da1FH@TRRi75&Tt^-Nl!`qz>Tm>I2`B_oI%TT@&iw`;GHoS05uOdZo{kyb -92I!4_KLUy*WXi1rHo~o1HM*(_zVlp^>zDj?96y*~}`FCXjjFkl;E#INR3lP9Nj_?*u==31Aj6A^`D~ -`A_cFm~H|*E78imuqbCRY%^#>o_EO1_iaI*3=so9%8hj4I6NZoeW=aYIFJk -pjtgMfKnBD}K -_$7#4Bp^)f0Ik&`cDaEjTHMAa7YZ3dj1~%|5OJ5lB&D8_qqr{ -zAfmM085bJ-?6S_*o@3;LCM;u2knAlEvv9jK9S3vh5RiVxVXVG& -L`4pa3#4pfCfkk;Xk(qeQKd2yjWN>_K~gIODg4jO=R5tz0rbKTz2hK$GUeX^^!0C@+xRG9-7Bv&2B#* -c+42p@eX$^@gr!dO_JjOV-Ef-PDE(d4Kt+0*nm_n1o4HVI0R7tnf{-%P>tt#+I*}9cqBTG&*|x;v_qS -7xeofY%a64oGi)5of8J6|mA5b$uq768hQUD7C?Egd>MlPzA7yz4Y?tGNz -wWwYM;Oi$5$Y*e_&d~8?DHEiJSPXj!mjTqWzqq8SZjvt>#pc-vX-Q-(hqu=*0!M)eY{vgDYz#wL -s`*f%teFq%be#l3dB-ESsO5X+NRA+_*4>gj__f0jw0~}tsi|v{mofC;n%<|p=$l}IFE{>HGQa`kV-nP -=)-XQ&%AQ-a!&|!|xCLG|<+ZCO&+tzT?T}Ils?J_=zXUXq-LpAS*U@pCYCuc}m^;p^P-Q?$_m`*05ps -!8#g7{N&30Zk2O;q|6=lFA!y>3=5C;d3YtvSrvCR-F86f8`Jpe|{VSLlrOW4IW&{pXzz?$XKUL86!d{ -3s-^aQkfAD;IhbFb$0sWC$4x{DHQUh`}I%P_>&x7KPGBe1_S1zPvc|<1hx<8K}!#kQ}T?a~H%ib@X-- -aNN(hzwYMRZHQW*lOYi&DXJb;T&FfwS4aSG|#K~& -8y#zWDQWF-^`PBr-&X|0|C%tmIi(o&A46n{I3G#;wfOz4KX=Um=GvfA(4+RE%a$3|UPr9sugE5w5Tj- -<^B|Jz^k-!_;E&b1{bvFuq6rmz=3j*}hWp&XeJty!7Eh%<5@ZmTdcMKX{0`c9+Y$CM4aDc%x5?06!sw -w6Jo%kr!<=geVpK3wGaj|}TL5I@Dce(0uUzL6zdR#{L_po`rhy9=%kjWUh)NpOss18|RSYcQtXd@?thPWUqKKL(iEnl* -0e4dtoZbX|Le@LuziF6{z%@fvD=jKc(1kuy*|b&`4>bbluM5l-hAg=kPHBu%E(zrqJD0eMJ6Fw}R~qZ -?$*0q7u`Ez_nv=n)#Z1Y>hk+2xJL%OT8_W(wOr3zun&sT5FqXtIf<|okrzFSiCdl_p`|mVI0&1SyGXN -8oeQ>E^vd@k;Tc9UTvf(sXvnd)D@EB2e$aI9eWldU9>#lAiw!xvEC#Gr&Op-vaOFF=asbfIl#=E;|g5 -{o5$5)+^sNGU>c_%Blk8XTDE1M1&Md-!IIuw=Yxc51 -Wvvi&ha=y!U&j{YegWuR7s~(zl@&w9#BQ>T7jfmaFWt*(da)qU}1d=)GO`VyoNiL|i}qgtv6EY=;HWx -3yb}ni`|$8Quh>v%;cM=wpn!JbRPc)kwmNW*^ffnrjcg8crvse8w9r=Fth~tYJ{nkCDvrAdmsW;5BvM -FE-IYy(3s?o+Ce95qswMG2(GJ6WQ>GtEO$M3t%W~a^M>|6Q8-rsUG84To`v6{}fr>>bc(&r_U!YJzg# -L-epeB*b}=jX$`yq(^z)gbjjIyyIc^z8AUvNNrZV_F3a~t9WQ3FwsxY~a!dXN>atA!&&0f_AA|3)lH+ -%$&yS4!L|vnp8m7JWJu*ZWfs%Cww#V1Ei*5p`-ZaA>=%Vn=`hGXT`55YNvq#{xLl^!%zg_u6*cs}T$_QH&P55vX^JN6MU- -UCV3YlM2=N%r<(i|M4Zk9Kt-%3Pg2IUjfYMz|)>I4}*!vo0VvS^(wVhIvuL;QtrWRI_i*E~rD1O*{>!XLBZ5_Jw69vs`0LpLuwAoui*NvEP>Eke5@+cNh-H^ri;j3=tpVIY -IQvG<#NFKJ>x(vA80tBj) -S;Z2xSZzmlPVB5&Mh`M#gd2@wUWdbtIv-b+Kx%nuOiAq2Od{Nq!dTkko|AlfR*I6{6YHq&X*jC9uLnz -M`s%Cz>WW#ICnG;6A^cc-$U=o_6A(oHNPP40`Hgle}l7TSSj6;WtLz7CAB*#S{7cvKB3^Hw#;{HpwcZ -bjhX?$KhNkgzbvBC=&35`*At(6c{HG*q^-8F?Oc-7;0ETAF8{YRW%il%i)SMp-q-~cui?EADDpS2+Ni -{5R5=1V+m_9;LqQhE&0EgB4H?^n>8N-b$N(>^D_7mho!Mc6q2M}vvu -Hr%v^Cg=vDRri1&k-k;g;)cU;~Ll^l&$I8Xs;9!Qrq5C$QWck?i3B)ml^x(i3q4ahdh6Fca^4Y-pNIg -{Z_RP+qZAuv}kyqSa!)t$iqz<)q|gt;aF=Wb{u-<>&oe+>2WVW1YSBndGAw$p{M3YN+cQ)B${AyiUyG -whm@WT|~J1I{7bldtfL)$NYq*b=W! -%(zU|4J2(%+L*r@?V@VS&SHs25d?at5P-Ui?R>#@~eMugCIE!l=A#*=2lVHZcsAnI)fSGl)mU4t@_}+zCxM{}W5^k3E)5wX2ASJS{;^Tf# -9N&brjL^{u7Dy>J%{s?C^swNV?PLH`xSHLp29#(O=fXiVX5IU|r;KQ5%=KM2hu-jmgv!;{xxpr+) -oMWNNmMVOAKmRArfaw}G63Iaz*9%qep5&RYcyTp>LSr}?tb=@y)3IYyg&NcM_a+Kt2N?**?}friBzYH%? -&Y0QE&z5n&nIkJ^uP*PRqd>f)}UEqHZ5^V?35D;g23;S2 -q%o7vS%sbgB4((8AiZECIa7+xpxBJ^jJmdJS{t;)mBLtz!YdPhBeu|xg~@8L)nky#gX1OPqgHHCvYkz -PazF@&2b%lDIu&4WGYhV`>JpwHcImlQX^*q0hMi_o=ZTXkci-PQOU&E2Z_`@;~>T3^G?3S;9UTSg9M&>xC_qi$BPEh=U7za8MqYMz@fwAw> -5A=2R#9D!#q2j+PoNYvxm|FSo>sccU`Jo#(D2kY2uzh&IO|ZR-ly{K!v2sC|$7l^t*#N$pq)vVo2|=Z -{Wj_hjM^2S&rYUe#`j#_y~f=z}}x9gld!IDxNx(wfCx>d3;*{Y2F_Dla$oZb`aFp-K; -M$vo^;b`!Cr4}TE$cp8>#v%8TH{|m*Sm8ad-+Pi{MtFEGcqSd>iaH$BuoD7)8C&# -`9En5Wc0sv1uy~gZ(V`z4CCbR|A^V*NPp98H7W70o-Umq_^k(Ahx~Cz+|YTtjS`&oUn;I -n%hxtA(-`ie{2rk4O>3`k#Oq<}(=6jpvz!CHpNRq*e&ad^qcanE4>N`WT!%{>;^ifeB6MyHd60;A;rMBgrzI(81U3L95|daLU2h!+-=Af3DsEBh_5IHin3)wGn1pKsIUlpqs&8%y*6*gP+ywi-vlmdd%>YA+h#2onxDgcKDU&;V_Rn(V~J -t7+K1&wl}>Y~%Xz5DCyR}-vB{OswozrF~-S?>CMv92^>0cYQ1{+b6?v1liqgq67f5k~ovsyi4ZIczBM -vk~u#xRj_69ekE6%NWHyoRsF*}G90CMp_3!@Swdpb_tWo?pRCC+PwXyxS}V4aCzWqC$is}{K_$nDEX~+f{0pMP3|H(8kF -+v0h!qy{%nr9T(Dy?g&*kNywXC^rQ%~%W^hHr{E;|;7nacg*t7SW{Tx*w<{3^tAb~Eht<4f@#m-49w1=XP4yoc#?N;1Sn&A33~F{Z`xN8clDyM; -p!X!sB|kDR8ORf)#g6}xqzi`=%>Dw23}Y%qcxMgPXkUlXY5s4$qZQV$}sC>2{+gmv{3rq3;Qs8G(c?+ -d9E1>(Aw?9m35UDwU6AGaaE9bdG!rO6ynW=Eg@^A|K$3ZR&m8+5|#^#}m$)^w+XdNc5~gAwIT?cfHJr -PH-~Hn_#$$o1enQt2IGs>AQ$U4!WwR^^(q#ETccoa-f}0m8Aeu?%RBY-a?D(|eaUD;sFb5rZ%~WG?7d -ztHePTtLjc+Qe}9sBzLMSZyq+nNTIWM>kk(mJU6lE@NYkc2;<9*Nxi!Ul@bB$=^<1e*gO2Uo%?s@bO> -XoQ8{Z(_VHQk7!#yq1;e*VwAP^Ax%#LFSbiuLruR#c$gh_L6P57;<-;#l@xjN>=|qpQh)T~MdT6OOMr -6dhzWuiCfd;B)2zkV5)fhzF3Gixa#X%{BGE%eS+=#!@s}Yi@ECnJ*5C7EBfP*hvXOKI!U`dRF&m_d5R -x9?bH#UUmvGQD@Fb1XQ~}0D@2ce>T&iw+1?)?S3O_A9;aGrqQOMNeH_f_O??wTyhG4`rt$Q0}DR;b`&nO3jWN{m#kai-5kRi%c{S}K|boI48@wyR -(1&paa3}UkRO>wP-T%1-COPd9@cl;6JG#n~<(p7OJYLxv)_Ne*di!Vk%g&sBk3_k$`_;7Xw4Tm1{=`- -WL*FD8ftr6eMZzcOYG1<(*6Oy~lb}Q>|#Z5yDFh?z!9eznpyMVhy;5}}t1yziTcoO~aU>kAm=<*$hK3 -6!J*yT8gOe3eh1RFR-2Z-VKC<|D8!8y8ghtp|HzNXV80UI-I#6O#@$vfAfB^s29ZiRTEvt5Dbj*er^Z -0jn;yW8Et(_4PjJ@-<(+^jzud+nv_LMw5}c$(Xl^+l;!vc;Y;IJMTpVG5jSojh7R_1`A;3nAle<Li{1dso|vOW_W-%cxluT`0qmA*OU|sLM$Zg;s-1 -WrJ1+vHtf=4Nabi74_nK$*nM^&J_E`4G3-mINkX?vHlCG`E3moL~-kBrQFIePsvT*nNbH=4`XB|@4T`yS1G3ZIm$^5QHwp8D{?m^qYx*)A-!74 -wz@hBW=K~`fDY`tHK{%fBxL8Ku%q5G4U{+_Y8TWWvTg}x?U*Ob2>~*!z1}Jq6O8)2858oRB~u>jh4dZ -m?<`xa~%$^TQ3d@als%`K1~%0P5k*fB|znM5M8klPa3;q?+m2AX2Q_r6o;Lr%@E*NBULMls0 -iPMyfQ;uMg~-5j!7`1r-;WqX(h!|B9~da;i@fd}yr2cC2^o@Iru+Z=}?%15*_z!<1N%E0t4Uu%tJ(g% -?7Ev%^gjNPGWChLo8LzjodB{8aLCZNEPf@~NB0|NxsTh1bYKtN(~n(h@lU2Uv-!7N)hJgn(R&xd2 -l>PF4nrt;X)k_&i?n)<9{!Xe|mKM-}CcN2V?votaMHDQ@3RKH^PVePgVU=`DFkk&^H~yjN$fPRzLC8( -~Z4hL)%7eMkpa`EYo+`ZcH{ -L8JG?n)~9+WWWkyEmkH@u;e{V{&dcxe|~S|I_7nl3?%e--Wc -HJ1bU9`ELBGz#ha4;EhS)Qot~i6C;I#!-8X;kj|iDJeqJ+?GR0L(K-1J-(y5GzGIW{>5J3si${M5NG1 -dLQaiQTo(N3k`f`zEK67^7f2cIpI2fTl8*g{OtAB90ZH)I>nLBGl(k>qdz425qMjA0yD?ec`7!!9eT&E)^7fW --$xSSs;tw+?g$$0 -RBGQ-KFgCLQuRxS50S*ri0UHcPqtM9N=X!K!0q`K`X(9Dq%*f&|d%6uAe(u|8)#QKLZOZoL>!BthxNi -mXJ>&3hHy<~7CNjAVi{QQfk3E1%V1Rf03pM{I($k}!o-ml{V#WoDtGbdd=?<6G|kX9n`S^)J+k%U$ajrvls9<$$JL>tw>0E6j{DiY -7iGIpiek9wnKTmvw$v|4<_j1OM%X;{USW^?hXoioPwEQMfPN7M^*&>fHCN5bdTZgQlMNVaqs9h*hR1< -V?|Ni{x(})nT2nZ41qc4r(IdG!C}3g}Y@lq^;~sum{V`ZpRDd0e{SD8DM>j<~c59)jBpnKAo?c%}W(n%6a01iWOi -b)^w5*Ip|%jS=-uFc@&WXZ%%(S?RdgKivJ2DiXFiJD~xHOVYX<~>%YgCWV{!JiWen_<%@(L{ia%k;F6 -b7;5f%)co2N02jStMdmCZz+W2aBe8cu}nL&4*0{71cpQ9fy -s&-xc<__F+VX*MO|ID#sFN=@sLv(%mr+C=00MnMMf^vtj78b<+qPo1=9BE;yIhTIRRsL;ZvlxfMEempsyA^9a}Bz|EhJqJjWL<#*F9t3%)^hvhX@;jdX`TwFVRJgYVo@B=nilIx -(!C$nVI*?O|8rwUKud2pzb7P9=3toVld3}|PV#EWY -LZEl=v(+CIuVO*bR^_`;0jcqs@1f1W2a)+vTN8WJA@uK5^2{2h{FE91sFlgaS68gk@z!lm1YQ5POOXI -7VPD(l`h;Rb`1u!0xJ)$NxAZGc{8m_weDxmk;bWM3glB5@)Pn$@$KZ`}K4IggEi963lI{*FSwI*CO%pGS*G;ogcdfV)7jLjncojO0xj%9_ -^&^o>g_Y!j+RxeX#-X=C^$vEoCQ?yCXN~Et{d%t)b3PPeeiB$P}rP&Yww~W8vFMB!&+ALv7_-$q -^R`J|d@d>E1z8Kt#IFkbbK^Gju4^g$x!%P!qdZh{`79a^7V);>N<#4vNr8$IhvWMPDHh4=z|1<3T*Yp -N;aQqA%E$wp$LWAEBZ7jHOyz*v7NzK*R$?B(NbuqaqFtt^QzOk&yAzDs+U1SDG(R>eloXt21d2||bzX -0Yk!1A^Cy$+-tHD{v()`Y?lv}y##oC8hurs&Yv>+Q^p{a7UXifUyqi<^sik$HT0V)@|x_#!AU7<}a6u -$u3_L?swR1nWcveqEG3Mgj**=9lu{tJOC}T`h1CQY8U?s#bWws6lqIt(IDe%dJ=vZK>su_H~b@<4@Qs -tc})IH`TIOt9k>v7geWoadcmksuSzmuyxH`WK?esqpUXQiVstKiT43B7}oijsv)NWW&)8lqc8^K{udG -^MDHwkGPt0?7MCWMf77=TH*_MNec8i9XBBj<^ -!%0=ByrfuC_;P@L~N&5 -C3E}vD;Ity)g}m?0c+Tw2^nect%Tf0YZ>1OG@Llpf;UM#9o=8>FhjhT+=2arY@zEW0-vz+AlUMGH~k} -Bh0{GTE=Y?u!qg-0*~WNsJqG9fC3OlYG<)v!eh -n4iS$dp=JXdr)-2v?%jC)=IWxGHJKx&~W4qvKp_{)sTPnNh8vC>`EOb9@TUg$-wO9geNoS)=iFey4E# ->*+KE}cJTfLIsYD&i-drS@ccRAWQ*-%Q%g0A}og+kLC1WHjPxdF6l%k8{5UbLYwST5)BlXH>2Vl4T`?uF|!-i&_%Zfpfz?A+GKA?zuL#ga-uS#o!+M2P&4@-mE_u@W}3`)P}4 -)w;tNaTJfM=tJimd*b3Ih#YZDl}9gAdvV#!uA?ZIG5WFt{T3Amh2)bs;~*%$IrjrMzD*k6EY>)1dp>{eC#qyi1X}m#>+$4GtSLq~oaAM1X&9>t$76AG%y0o~R9yYs)_aS1wniPNRn;=hA3pilx# -l*_%Adm6fIuO1zaML_{c}v7vOia9)WMiIy3- -&3$Z--QitDh~`HlK-bIM~eEAnvX6G)7@xVSr3`-_R62e2uSXI6Ir1UL7v&5oo94;jTfCC`Sy$!k8fyq$H1Er_wt}Hy@Gvc=6 -^OeF7U5?amP-vZFPEg;cahNmV7*IzMR~=e97X-JXVPJodC!ph_V`y%()eT3$oHWK(sGi|3X0f0UaYIK -o&yILrg+95MeBVjzS0Cjj0+BvL>#T0Vm^USR@d>y7v|I|pYCImKhhV7>j)k}369@<%_tXx-??3ODN2#0bKeiy~%#0-2Sf;lI4X8n^T$edVkb{-Db@M^u@VwP5s3qYWM-x(0wyUh$SZSbi3yF?t}jG}j}hG2^i@oMA}5}ZBdOZJXA1;BNICAY_ -sc<%#vh|=FlG-K&=L9rIR>9g~tprX9LFURg#XFVhXv^!q_{sNt)m|Z@Yx^5hSS_M+tvTie6c)Uh74K? -zFpdx+5msf;l04EY=I%a0PJvkH;?_r9GGFaN-pZH)>vRmNV1q!Nmhx%1F`GCAIo16Mxlwba54DdL%I0j!v+3-KIjd@rD%}oYZG%9E|{dELP{E!z^`2;d|e{`}6T1LJc-M>-HpG4_bEegZ5zMKP+liDCH^4%~7q${U$*x9pNJV`{kw0GS)*PkF -w|J_1df`RRkJS%}e92XGkXdw!K2n1++e41HZ25R5xLFoLCp6&_*fx9L!XKjI-`Hn4c;3JtWQ%xa(oZ8 -PR<|I(u{;z<|$fi7D!ny0isuPh4;QsGRtdXIAms8h!rCdA#h=Y6|g9uIBN35aU-n{BOCLQS$dYS4v?< -YGL=-_*9PeO+UvVMsE!AU=y(QL}z5m9Ag^3>I$P#l5x7CoZ*g|LM>Q~b?QJ&b)at5$d7iSfBMe&Z1C97LP7%rda%M)$!QvbLf@(%M2N*qEzNv -JcIJ2;1Cb)puJ?<01?XR@xQ(Ba%U`u(^7o~d!Pxmg1c_U~wxQ#lr8jcK^Rk|^>&GC!y;=?euJQJ&1iF -#1xrt8^W*+6e_1{p+amr23IY=Gnt<+YeBx->r%t=VX`XM;ph_#|W%|&T%i*PS0pD`oU;d!CKOpR-*?z -t_^hV)#QXn}yOvdhJ@X4tX^Tu(STEs}n>7j)DOU)-(5H5i2AP!Q4~5aL7&;x&^9%8*&9hOuTmi^$~~sdr}_F`1WL0a^O|ZKFe5`{RGW7(JZ3Mylu(VP9;(v6jcvVvQlE1r|(i;5 -gJ&b=j-w0>7D{*fNQZoRX&_t%fc;Y74=7P=9XL2TNkgt -t%&AG=uDfBxZnfQoVx7;nY?px*(K?@ZTey#Q}O6F<{a>l3NV~-0%IGXXIFv)wk1HAr2;92fg6ux(i6w -Ae9_Ll^n#0U(6l~q>~vPbjIC*;?WD;@$X?dg*`Y -AI@#g7sB~0`z4jnb0zkZv_3x1TnysVowTN6#4-i5F9LDt8ygiDxWs#YlXW%pqD>#Fn)1r>rPT~r+5Sf -YzsP1JQrzvVApy?*!nE1;12i&9i(bJiu?7Q5_8lr3iMq -M+e|JG{l5w;D{F?*R!UI51lax3gCZEjjG3hTV&O9E~gBCTf(}3>0z!O6WRP*OvfonZLYQy$tm%A -}hg1CS_1EUBdv6)1L|6z^PYKW{}{!+<(>SvCsJ8a&=C`9>J!UyPHdflcc4l12i_;bQ+{gHl0Rfu#tgz -5SnY{|BRU?F{sc;v#h8PW7S$CJ5iwCMG=`<2^$&NUKLVTF$_M&9~Z<^nD20DFt(189b94RhHzP4x#iM -*CPh6loKm$*{Ii`Ygq3!Ps2ZY0kc!JSMf#!qS78RYT0jbkkSbDc6~}6t+S%IS_GJ0EO&J1D8~8q&-^6 -n-8Y-qn0IAni{;EOB(hMDlK-nDHl%L&})y2I0u&?U}H*QC{HquNgXb3%qJXZ -bf;6Mqt3aCtEL8QNx0L<<= -rR2GE7JMT%+N!9qJP?0x^zm?L*mKM`O4j#v4Nazk&H8TQN0pKKG{1T{9EwugcMNxN1Jn10s~cmO5+aD -6p%9m0I>tHq|Lvfm+#PfveSWwl)GaPxU-{ueZn5X5i%*KCQPkfG^Pj&!?Pw4c8bHHIIlP=@x)M{9FDV -22kMvEF@|=JM{El9#+M#a$aEt(AU3xG+kBW}FkuL{K!8W<7ra&%?v5==0wnOCBu=^YUze0D0~`5#X?q -5(idHf92s!9LY1Wmx-UujlKeBg$*TK-|9aN#xgj`6X*l;mQ(QOgcN0W$%b+cv-lyBA4CUbGM_9|F!KG -8nmmAZhOC3LW&YPN0^CN~nvRlTqYWiX#6S^lfqO>JKiX;ZL~5=IwGH6_rpx*UmEMNMJb3pyT+nQ5Qf`P&w%W>Q~zApUvbi5eqdml4wxV9u*Z%Oz^gwmX23E*ZB+ua7P*{wEiK~@h{ZmJzTdOEWTE4SDmVp0x1SD_LHDUqnD7*nFMc8Z;5t9G!bhReh;cFQB65*Cp-r9Jt4^f -kzBHGW!>0wq0SgrB@3k78FkQMyl4z`l0lN*Yp^;n30d5N?omYfipMknav%bac2{Clfge?;YrwPE)HMH -(hb^p0oLUL^Ng|YI7MfmR5gx(vB0v!>bT1=`uJ`}pU1sG`fojh=zK~RV?Cd0aQ(1dB^Jk;Yj`?Q%|^D3cE+Bcj@E{$83#0ep3 -Qr@&25w+ns04%?eX^!`Z6aV_h~~0=B35w6PLWM;ghF+wt9AO_tn>PXSO`AH(Qw%z4tn9b_O4wM4>Rz;n_qW-*wUXg_O=cp(Yp!h3SgPPC7LyR(^KoFPVb6sIYyw!FM{2@F0 -n-SWCi&!n$DVH(l0n1)-O2)}-cX?Zp5_M*VG;$`!o(50IjJ7T=Z;B@{v03vzlA)e8HJ4olf$Q}3+n4{ -AefH?lAJEL6JtK%?)Y8mu=h^V{k*d2|5x;IwZSVb%+C2NV)%x8!h2nw&RfnVO79=RV(ny9P%h|HJskk -a`o;^jemirYeJ)vBl5P -=olp={xLF&&*~qB*5Pt*8LBW1(;3Qj!6osM0u|ICnf@Y%tyjYCD$K=7mUy#1r<~}Dsby8M9OI|Y`R?O -j{oRqYD9+9y+yP!fi^zB0#a2vEkvfxUZv^Fi};3$=|gAJ!`f~lY}yFo_@PINxO61RKc9~^x1`uVqCza -T=8Yl*$};SaZ;4u8nuzoQ5LbU4a6RSRJ2&7WSseUbeK{_*lJuU@}-@$Bi_7vlq+p$pwZecLIZ -|Hk3wYc?>gOEEJe1_HS#=9u!dLM0fpB4k%%?qOg2o>h~I&J@p8domnJDSQ4EZMarI5@bR&;+-2B7*{jtL<^EG94MarU5CUjngw@ZkCuf9q8S(>Gr7cB!!Z7{(~f)Xq<%+Vm9BVkDMPP~|D;7ysoEg -e$nG>=FS!E!ME1o04yGO8Jg@k+~F7D(5+{v8YAKbzGEvu?r~I0ZG>Wa=DAEGvUwa^`$2IZttqrL>S;J -HhtEYTadB}Q0IwgR(Ugw_o;(6+?eKQbIa(X2!+DU<&~jyn`EY6;eN7H5vuovYT(Kd@~1b}nG}suGCp} -@0*YGd6RgLJkFXx^IO8RH6s5AXtD(k?Z9UVvl=CfI_To!7HV3(z<2U -3IR9U}2VVxmyAWIMz6O`LhJ;x9zl}nA`+xoh0ZtEBL$xw=CX~!d1BdZ7;q7Hc(0v+pL%8fWZ(6_{+`1 -5NwoQ&XjSSm^vqeBh#tNw&z7*n2oUvf|+)f0j%bjlx2Gt7W<$*Noa0&mW?37|=8IF$bAEesVW-Lgtph -H#69d!TXIYt)w9R#;zk5%_{S=jtr-l!}2!_r`FF<|K@pl640Fsv5*j&2qx+ZzSIp;Q?niMMq!A#Ub%L*>84P}CADFx;1IOd -#pZ;+ay*o*?Fu^KfDVAiSEhBRKXyojxHtIGqN^cRJ;M+H-ip!Xw$)1)I}E9gCSPNrFX8VRm^4tI=^wj -;d5iiAm2MWH;NE(%yiWGN7y_vJA{C>d*!mC%HLHP{gn{BWE@G!-WWbk7jHTGgQtyu%4Nm9Gm7r5d9w8Dn5FHz4s;zHW%@F~go-3ZukS -5GQEL^U_*2id;FTI=nzY{4RywEuMtznmciM3Av58`ZZ3crcS1h7coD00q8f%CD9OSGDSz@~~hi+8`Vq -`xC*k|$vb%rJlj5BUjckdxyM6`qK3V#PFhSsR=>Fi${ZJ<&3DBRg5T2b-)rz8Y)^I4+MNgpGv>72uj$ -@QCtVfr!{U1EnVYEtZ8CmR@Q9|>`isbHlA;v~W7zQrHZuvhiAyvJ^v8>)4b7r6p970ZH#yUpV};eY%5 -{bdq0EyTy+-M)jPOM6Do&+R!763{*U-JEg&z`Ly708=)(nSU`^pos&M$!V~LAc-!xJnhKWQpbk|friH -IdbzAFcu+a~A*WPzn52O~KdbcOgBD5{|)9vY!YUE$$iFb=~8wOgI9#(}z!;DEwJAr>5j#u} -@I4IwEqmzxSh2qfP{U_!9|kp@<2J$qQg$`A&g7AvYkNx&*@*7a$#hbt=i_fCXTlcHNHp}_Vi!>GNH^gHnU_x7unG>j2%LWDC>g8`OhHT96bs^$b_}A -3rjvZ{oCZ5r~mWyo3Sfbk@(ugMxm-11sxAS}kDYwWN%0rcrTSLr3&3H<^FYz=r4>s? -$Z$#(;|0vJ#=9L>EKm=xwF1PX;(#iHM?2$eAy+3d>^|W$B72+Bqm;7m%H$3k{t=o|0vamr!Yv_2B{{B -H&KJ2R3y&6*NT8Ihgv597b*C3=`B}um-i%DmQS<)!L9V~08;sJkWTtRU4Go4zS;xn;|BHBo{M&8pi4w7m~y%#sImS -jX}TPA7^lseSgJ8pd|Bw;Yc^-?5eJ2gvv!GqtW-F{l(P~WmOag2mk)}k337k=VA$m3oArjqp@OREiUf -J%Snd9mA^Lb)n7b{rdmAW~!N@|dAY6Xz#US}RfMxT3$&Y{-2Z+g2`mpGv}JZyR -rZxh)6FWp;@g1WBnuwpIfNmytzMv#Od8CX>)IEUMEOo3yP|s>0;nEtGuj$cNs7jF!*w`riz#RtkRxKi -i}EOE&yjp#L(WsF#Zp2PH}cG_c8zhnIT|laSp_N?LjLw`NQ9jB*1v^Em(p5?WC_@tgvZI4UlzX7-Zvs -#UY?5_8vYTxkc6V8Q_rfrXW?vq$-(94?`d%WYv{*wtyyrBrXK%QbCI<_zkAk0X!Xir+Sv`s-*a2z^;dYWIz8B02{*4mtJED6k8=Z=DH!9`I(Ks3Ac@H*yC4k#V5R8O -jtso?p-}9jdTGOpQnUjDvc0nfhw{x#ZqX0NjdPITvc1qof%aZ3QA#MPXk^5wQBMp -M3qkS~oR$h29kFYf2AM&qddlkvBxW3&6N<$wG;OwQh_6Nbia(42`!E+0sRy$Z8LmUc~ocm?VF2!XHL^ -O0EF&8*;&=_oMXZq-o@#DWsIH(T%gHmhHr~mtSyAy2rX=^Odw7@;}`}$k$|6qUsWh74B)?AzpSk3DW? -tLR9R|t{f@d=#Qa$Y_z8KGgwO#Y-iRF!}g?t9d_8k>G_ReCc^^|(TVv@*UqCxw}CoyXh}XsI{vfkn=5 -0Z6+j2x(7@EC{EkyB+^^sx8DAUrlmeSSKP1bH_S -3+nOWkD%QK3O~ORoaT~3GMD>xr(b(?q>=*MSptY{xuO7{?4HjH=%V1_SsdQy663NgbrhM$*d!%)J>K3 -8f8sw%ZX{Cp9mIT&=_l1p!R{;*3H$r*CYt43cy4BNtSDp?xPgkinTRRy8 -j{M%|nbI>I`KDJD^`xAd8+pZ3-ywQdug`ct+NXD`_lY)crGVc_}J -{&;n|iM5`B1n7_T)hj*Xc~8v90})kJSno(TRCqW$v{m0BB4V6m-HV{}%{61G`QJt0lpwBu)hn#{br -vm=sNS$Xe6k3X^k%Bm}UOlp#Pe5nY{Tc>8kgmcaN-JM_Gbo~Dc=n|(;9X7!jues8fe(V-?`q&p*?oC< -@uxq?dNl54`!(;J9XtRRTCB}g-dd5X{}w2+>C}i%(_*TXn+pAxga1ShFf -;e3zV@^b>$Y7yHPi1^Tg2-ET!6U%QhE@+OX*qM?9U{ZL*5q5Kq`YmnLCkS908R{J)o6$Xo2*;!EiG2Fux4NfLiv -Ew5F4?Q2d#9C#&y(!TOJglRXqB2FUhIK--NXp`Z&}jkOC)%<Xg{P>KyLTv8kW#vG*=H4t~jgFx-!IM+o)0n56{e-_Js`du}?4im7>pccP>XRXPDGN -7?U0rkr^ha{(B8?cX=$7iXsiQ`9vQ>zKUEI6Z1nxiheMiEM_fcX}>4U -K{38g!qbp4f9T9lw&!A|pyaa(E;=Cu&s601PXzRb{}j(uFs$#T$c$@i&yS0$}7HF9;jCzz8L_XF4r%= -Vi!$@J+-@TW^T(u;!vDw^hSdX#|FA*ToYE(^ixM@}s$_G7O2HIzDW(orY -M;1)G|2E_T^>L9QRDT6Jv75yw~+c@0UdoNVmZcDAMkjUx66$i3U(ACdy^fywdn_Lb*X*t{-8abajy2% -o$UF_vXLbNQ}7-kbIg5w-5?g*p7>b@RhR;eC~xP8PbT$ICaYxGSZ_MDE%Ay7Y5fQye?O6*MfdLzv>bZ -7~PA!1vkP@@#Dt6j>Ysoh?*n`*NquTH9GvIB}%)R(XsKq+pU^_+6}#1&UwrHW}|Rd4|ePGs?%% -^;u@stYC15{w_A-1V(>kKnX0q0XJ3(fRO;7=AWT>N6Nede=S&De>PQ(vtSMqZ!Yut{(GjUb2P-|8=BL -eYmhRUI~|x%TjiE`LbR#R?i$Lody0qLTAG2!7i&Z=NOq&I!u6zd1hN+pwU1LjQ(mz(#)({?aXdu5_etRRFgw{-rT}s0fD-~wX%D_^9@$O&c3*2UvTpMXg -4JbVp1aMQZn|0t3!m(u3zYlff4{lVyF*yRQI_jXOs2P-{+o+mz!(5KXlT!OC6UW`1L+mti!#3Tr4&Bv -9aUK7hi>?-YuhGWE(U4&5L(WpFe%~lqt`P|MlX3#R(FJXFm+b=a|f9_`^JZKzVS6XX776=cB{kv#)^! -$Pzg9UOI2ZJR=c2iz9n!&-XfuD)9Km*{4)BwGlgB7W@2%!yeI9@usMjtbkqTG#3omroYj0sxXrmS2X; --HSBTI`#*6o{0$NE#+x2JFXqk5`=;{LiGP&d$z%vGPgmig}d1bbiZpzhk -K#u|ZK`*~*p_=3<6F8p~ve--JhSg$xH`!fB83WDiKIbao!^*N^Rf|HY9p4T-~IOHOUhS#Mn={D)+O*g -C0lk~D<5xXtnx@>i}7u*07@JJ8?>6YF0RTrU2w*AW2lL&YcwqKH7ae=2LV^hP6(_>lPx$3*LDFo39Zg -!4j#72~*mYnPPNf`R* --*7SQCn;t~Tlt#XPm+NMcinC{2_#@ap;_n -PP(J|ddjLx_M`_V~N~&usIc8cC&D2h$w02P#6zf>dyRlG>ee`ILc2qzRkXU_5Ag-Z@+o*>fMXy88FFzef|7PqDEa8jnVJ+vaIYPWj^yBQZmHE;9~BrXYP`r%> -a)|&(o5V$3>bHQL0yS=Mj0?)-KzO@>fB^>)zBfu4$Y&rt1mAf!*PYy%FB@73JPTJ}#IbImzV6JqrQBK -h?*EQ^li?j&cw}>B{wa2s}#AO`Nx562`VoykJN-o?&W%RAP-zY7AcE^)m6)4J~}ph@x3=+KlWZN|5m^N|3Q9?L#uVNH+^=t-HF42K2j)J%n$_GP#H|%Mvp&7*;7osidsXde1$6G+hRwNRulnELCTcFhXYEuw3v -^VZWaa_NnGrwm}9gwyE$#RG8x+T)H6G40hKlqZpYbQHU8OvqZs;PHrq-{@uUXP;d!o$FY9V1MV=n_&+ -1}O`2P9iyTm-5P(B?4Djs?P+#km-`$Nc+rBZ@wDm&i33G-w_y7@YZ?8K8uJf5_ei{ucvSvOSp$8kN)a -(+yjpG=^I92nrJ01_*bv`V;}(sNA6Cl0bf?l*a@^9TwKbIBg` -j;Vg`ytk;N3HpPCs|C0N$h1K*5LQ8xqr+&)KqQLl!I(aWry~uaBdLPc58(!o>;LLmWU~GXpX>gn8Xu1C+zivzo)(&ACF0#pqqA80Aesbdw~+)bSiB?93<(3M1d77z1d~*hu~(K4`FzCCQr^K -pDHeE$snaS1*wSz9B7u?U^ztfX6gw!RzQTZ;|6OA(yKi4f)bTMt#=$s${${o3v~_<{R5AWpz)|FGyZN -s*~9S&b5__w+S{eTlPHd(A<@)jFi|KnTd!H{OMcvj`~-<<#;kmJ7Uvb}$a070aT95#!=hq)G@O06a%z4==+`*nT5T -bZrC32hi$rxTDbY9_<@TS^IrA2;h_U14E9A|xOi5e2nsD*-^DCJq!WkN7wP7^GNvYCMc(aUIknH -l|_aiPd(4-f|ZpFs8bciNKVPvbRVUXMApg3^H65kc`7)m$wWR4g%G0|SBvED9A0Hq&G9Soo?>m@LYY-?>6lK(=ra^#+h=Ec}UkWzJHvZOL -Ua4Nj;+u!5@Rm7qZU;z|>)BF%J4Uq3y4X>$V5vvap?xHRHZJX`~Uj7n%#t982lY{&6u6Vo-<6WY9T9(Ea~=goD>S{Qe}H*p8$HPZ27Assl*zome5Lrl -x#8&9+_E$$A+R#(%}FvimLb_;c$6m?h2r~9qU-I7+-d1K@)bUz8p=^Vz=YF*Gf1Qr}_;;zvNgq@oF*jZq^lWj$B585=RLMvuV7_**i+w3#UO=U#&xXS|Bwql@W>lg`O -r>g6T?Z?FQJwij*ZG5@Hwb7|@Qdj<@BA=GvKYT9KHyKn;=vIA5@l^U4{GviKxvJ*Xg$P3gEznwmzaU1 -6-sdgO59DsQY4Wc(e7qNxy8z4=XQqE*!4&>wSUv75SeXha?GNZK+MiH<`;&-EwaAUsGUa4HuC0OA|ou ->U7Bs>p-B)J17@!>5e#NRUr*rHTF|(XB_22=2g>tss<_;oI=&g{b1hZ=1QY-O00;p4c~(q<0RR9p0ssIf0001RX>c!Jc4cm4Z* -nhiYiD0_Wpi(Ja${w4FK~G?F=KCSaA9;VaCucxO>e?5487-9c;Z4O)~@S@5P~C`Y{oGc3`DEw$`8j@6dlw=&Ry1T7f_0aITUKfpTn(OlTAt7v7vKWYSt(_32W72~Xee-5I%oRw)&<;r#j?)XR=OjIX=PmjOU+Yx}BBi-|xshlG*2U{|kQTwyerb4G4%?@z)$ix}=f>( -HXkwIoM-Z@T!CBb4T6Aec6~z`dc+e-3d5w)!LG27mhh;Jtq)RfPg0^N5=T7|*2OSWMsMx6OzCx1JgjD$A1 -ONa#G5`Q10001RX>c!Jc4cm4Z*nhiY+-a}Z*py9X>xNfUtei%X>?y-E^vA6no)1tHV}Z{^(zSV#RgOb -*xrmffSYt%u`MoI2g48qDlO4AF-a6iDyjedjv^^jmhCQSQ*(g)Lh*Du9?9=c@=0MB2Dg&tR8k_)igA< -?Nq9j^TCNeUs+^`+QdYhe6-nuerYNIa#OMpTN=z;;)>LllWt_6&qRO!ZGlkOXbS|xROml&7nFY -1LYZ3<`xIl}Fafx)3)1?(KVUNUC1S`%8RAIRR4Wo-bKv$oT+e-OtwjvR_Euk9(bk$Xy1PFePxrBU?q!gemtmQu_ED+8SdW9;E*9D%SlA)A65FThxVqDSIKeGhaumrQWS -4IJJl})RZBu0=Vk<+2&iX}-91Q}VOL=c(S1x;W@lQhRdjK=8oWizR}y`k>)WMGO+#B12q@?jLtZmvJh --(M%!v&AeNqv;fd7a5i~R6BA=@#B2Hu!^t;k`$q~t9}K`>Kshno<75>HL!&Ly%{fs!RGsS-Mqib2% -rX&gJCro`NmdAuJ^ywZRhWqePE6(#=?pJvJs~%}Zk$cyg_R#p7t9R}*wqb52T`ZxK!n!5qJ7KA5?K{fKI@fQ@7^OH7g?}X%P?l-I?kyXL9+%P`Tv -8*&AeIA7-MDKpVuqpRp_ev98r;KxkAp@=_XvtU(XBsupwfOSO{7h5>FsNv} -x1+Jqy!;&RS+mo#_H_ThS=73FHrt6e7Htapx;>-0XRTeM#+vCp!u6m;OgVa+`A?kh7!0LH4r%mERb3B~s3q}mMYmz}7zhVyy3dIB~w -e_4sQ|Zgwr@sS}>4|NcJ2IW#DR+PS6Y5F3PTmLq0#Hi>1QY-O00;p4c~(c!Jc4cm4Z*nhiY+-a}Z*py9X>xNfUteuuX>MO%E^v9plwV83Fcih#^C>QQSqtsZ*FgnQd{c+M3L!4 -(7HIyEq{mP(eljAKC`#nc$H#%+q2dZmg-*b}sYHPR`U2d7P__o#%z!v|5@NW)sPv>uP+iv(x+G5Maz -coZHE6C(mP2_1Pu9p)vUBH{;acL7>Er&^Ir<~>HtwwN3wKsKZuXRw(uuy_3~;6V*+Pd2g*P?(0;9B^g~xauHdkhcs9{z -B7F;;njcTN-^`5Mw($(`|3izle*CX^FU5$jfxkCW{1q^5Xz#BccoWV@OjM13hi(XCP~k;jth+;u(Y{% -Kp~x3dO9KQH000080Q-4XQ|<{Z(mn(L0GbZ~03!eZ0B~t=FJE?LZe(wAFK}#ObY^dIZDeV3b1z|TWO8 -q5WG--d#aGXBqc#-2^H;nf7d&+#NoV%pNj#f6PN#?6WH#GFXS)Lm2wNMFs3fv+r+<8(guo`YiPO!tK7 -c^)_xC;V%*skg4MKUWSxMTi)Jl1|6eZ*}Pqh$*0=HAhI!;Ntq+TNsl8Uu^HwDqTkmV(l>f+~_=Xq&Cl -!6PMNx`z<$^K~K0seg7xA!Yi6ymD_y`-?HSw?tDA+b)DR8lxwYF(*G6p_YUs5D9M>0`Pid_luhlo5$e -Pu`sTbUIDq5Z;k{s-RXBL~e{)Ckd%4PD->^xnMF3#v~Cwi7s@K(*)3Aqx?XnVuBx_>?Eg2*yU&!Z!0M -(D)q`fWi&Sd$~YsM#Aqx~w8%&B;}n#ZO?jO9L{eQ#J^>>NC`u6*xdP2-23pvv8B=4R;Ua`2iHu-mUPW -i-%Cc#6R$;}+g4(>IoE20>XBSoV-sYLKNSI&a4oo~@jHRGFGq>2N##oTpWf;T`jyM-ZMrAM>gKsVSqk -SnWrs+4Ntd>M#(swJHuo{ChfD#2suaAsdDldq& -V?Y8d2QKidS22#2#gsFk1%4!w1WhKm&w=T2=Meri)RDEfGmDDoC7f>(xs9(A!7%N%i*vFOP^T|cXjb7 -D!FXU+$O?3doOl9<*&F`;h_o&#*XpG(bczFDbQM&%jn#^1Sr?}8(Q$Oy>JJ}j9sk#Xww*ATm#n#F)|laXi8*gC*^Kx`gEcLZzINa0P|in{?4u5a5UwO -p+uK7uP8yR>;}Fj`REB!Fw@7VuPs2S%V;ec`NDovIx#?W=Z(CAAjK*ot)ZFc6NGv>~%Ub!8LuQc!;rInc)fN$Y5liZt!Et?Yrjr;!cU8*7ODw3>qA81g4dS -xxMwEJTfTfgyHy}Gmcf@T|r!h3nA_qUo`c?$<|aGhkpzECYv-xl+Q6}GiO&O8tFHL4b1g#OWQxPA4X9 -S;-wt`yD}q?)&-KkHQ#3(2I|~|CDG~9(rwh2S(gn%vBXqW!F1ra{yrUl-cq=el-zf3O(*t^O2UwE*SB -*ig$(=|;ih|SxptALshdm9vA>DwF#c~JV0$}ZeYQT^4oC0{lD<8z82<11$}sj#w)Z;b|D*k!KQJk{$! -oVIJaaZ=HaOJ#M(yaE`J-VEDZ2jIFcV(>`e8o%c>Uq``0{%8+q?PukGC&xr(Oi;+#md+xqp0)BRyTDN -*t4-h-0+!sR7=B>W{t8-ak-F0|XQR000O8`*~JVYCo`j`vd?0Iuif@9{>OVaA|NaUv_0~WN&gWaBN|8 -W^ZzBWNC79FJW+LE^v9RSMP7zHW2;pzv7U7u?JUfv7zgPG+5eVz<^>4&X;lJSClG$S{P<~(b7M72Y{_k|Qje*a%?q(^aaTC?3%0 -3*+_b;{U0I7p2PcD4Dbpn%{C>AK`K~lCX;*u_&IR)7h$<(#e -+SG!m}rnNm4Ll;HqFYQ@Z>tw831a_Dt{X!qZi)`M_SXD_@^0-st7Q@}a2lz`#z5`=5s;gr!6rtuG17{)8+do)g_}$t5?x5z6f@sA`)Gvm$ -QU$KM^ZMnoF07z?eGH&beHSJyU+X%o22F>$(&v-r%yM8L`F3B>CYp4}xMG@>y;@&6hgF!Zq64s2trqC -#`_N7r2Mn`)SSO8xh1ouAAdv%h^L=P1mIH>_05;T#N+t@~qOR=-Ud3>ob8r)t(eH&w7+rC+x7Q2ccl+ -_dP{Z>qju8!EEB;Fb7LHigmfCEs(#Exd&4t+s4%gU-2`h?b4#EPi(ot$FX1lrE8AGuUsR -kDd|Ed~F)}WnW+pSiv;8rUFAr-bRIG%q*vv8l7br>ClUP`zx -rHtBs2VN8sev}u;HvxG7XeZYXPdrZ97;QparYYKrj&4~YY9T8oPr{QqKV+ojE+y|KEPJhMd}RC=}dc9 -tMih5Q~Dx0$LhrI)v5qxauu|%ylcZX!S0Xx+4G -L)1fgR*v89?v_N7G~X!$dN7~E7*pT2%PB_L;VCC=8?j8R@C&!>>riB$LMQG?Z0F=2|Y7)PfhrV9>Ou8 -3C-a31%3Aq7N@5{G9<{9fVXesyIHqRZx#a|?Kk$DBhO$u#Laea?IG=BV`uS^g93K;;5P8hDsn#ZO$q7 -wzPZB`{RuO#%kNnJ71y@$vj=Zlt#Z^9OS=Ch-oFdFFAv^c>;uQ}pl_|y-XESA+@+o3sK~JQU`EJzlkkMmzR)&=_}Z=;?G0c+jJ4cI@s^HbY+;% -P084i30T&bX=lmkb1cp -1q(6Z^b*LD(Spgcm6U=wi8^*s;Um{n3J140Kuu0#cdN(QVQTyko1p{hAk`tjo|2!fPTtzkjaFRRhs+w -mj-`w7F)Kl`_kF@>oSilGKO0H#E!+yo#bDr#6=fZk|>6@e$gx%j0$rf673>lDG$N*<-;2$@Km3TtD_M -D6=Ll*ULsiJ}bG$la7yfrXM(R1s${imFXSRa&w&Mmy!v6XUp`H7(GMMKfW$Nz+j7->f{Rr4ogrRb^Rz -vV<`5+P}?TMg2QMN>lM^5))5wL4R81jYbf;7nWFlHLVaHWap8!NIT!jo~5KiP7|PY=4VURsI(IpdLHrEXph8Y;)#kr~O0ZBn1N&Z -C7T>vgUmU`P^)kwFxqPtFAbk{LWl78{HPumO}8xmV6)|q?6t|Yd0#9h@$Xdq(uvD0%Lc7KEjgo)c@m%89$$w3~H{d|>h -YYwn=r@y|3z()Q4i)IIm&H4CCAUhpZFVA4uUOk-eyC!PJ*il`6Ps#Cb`Hy<>UK~kX>u`i-C5*ip*)M? -R*|??=K|^fef7|@wZ{)rJ@4TM1s@6aWAK2mt$eR#V_P9{#%q008+K001BW003}la4%nWWo~3|axZXfVRUA1a&2 -U3a&s?rZfSTfaCyyH+iu%95PkPo5K4f`fFtd`dfQ;rBy9t1;vz|b1-ypDSd`6;ED9u@3ySvJcZSrBzN -B*21)7J%7H5WYIdjBe_@WR}6QO$Cep1h>mrAi9Q<0~9R#2&!B<13%dG^^nvr+}s^NinB0-xclUC_@3& -u7*1QK7lWY1Xrg0WEl~l2M%sxj5reoxDHo^>~U-1V#BgP?}1u9=V?TUdHp~lh+>-azF}6XA3$cxgd9v -=F>SmgU_NM(>a^o4~WRKXQBluGDa06dd|=*W|tuV0zbwbp($9Tr9K1ZhpteQZf8O{<5C;dcA2zl_Fnq{^YO3-nW)L&oh5VxseKu>VP~ll8P -)7j~Iy&i2pwONZvzi376!iU{msTu63b?VW{96Px+K$-c^uF#uBFPwI~81XW^YyIzln$%Mp0Qln&;4ks -ZfTKyD#xxIidwt<1jSfl0{|tS{)XsTPtmMX*GKE@R4?VNY}cwNM07^hl_*C4mIfW -ro7plBoggBl4NziCxXvbDB=KI*n_DR~?RHH^%MKMN@eCiQa-Xvm6;5~^tg#(c%H)L>g_*{bb!o<1a<0~LvwKeH*^?Q25k-q3uXa{gTf&|IXXMJJ&8 -VzF3&e9Y;??I^&K>DPy!=>UiCf4m^T2GeysxZk_&A#fm$Rl{C*R(YtzWEr-J4QM(2BOSS_PAKLj%wMB -qEi6Z{il3=ybDWf(r1>~AZJ;+p;cvT)O3;9o-So=sU4@geHh;*fCF~HDOH%!M}prPBxNT+u5 -+JJx1-?&{nG>(^}!KBzro#`f%}aZ#3I7w_l>QQT-f_-AhsS&9hBzfIxmJo@II(J|!CwwhMQbP(ila4| -%o>z7l(072EFoxUJtQj2Am23zfQdJIZ!w_V1xh`pN01^Zj*uZEerZjzR6c*G)lSeAGJ^#3=1{&_#`|9 -DH+oa=X8xG}eB~Q5OUIIi<QNhk}4bq|}^n{Zishyl#iL^K6z& ->o4ZGK083EVC(h7;@Zn|o0u+f{#83q36)|uV4qduTM+5^0TD(3$jEvKydI?@OAtur-8DM>a(RArdxKo -<0@Sqv?0+S4=4NnpqmAciX9v>;733XTB@P^u?rvLl+|VF(g1i#uttvVeUM#1yw@14cvsCL+$`E8jd=e -h|JRwKRepjBl{&IDDae0n8-3L*%%)Nfxse}1iUCVvj^77up99|DSrYfJE0R@6~2XK3Sa!-T-8R1E%^v -fK0fN0nPiu1s7j5}`5i_Osv${5GDa>?Sjom(e7V=O{r5mm<1h^J9)%(xvmR|=gD#a%-#Ps1xgSK#|K0 -&mXw?V+}d{-9~BX3}+&QL+A(+4X>2tek5xtbxgpO-J?I2FH@XWxcww-!-4t$LaBJYm+Lv>DMZF8rKZXaB`Y!sqWVG5Jb7zj0j?7qtzbZ7OJYxa5VT@E4(W^JlkNlp8HzL)KcpC#J@1xpMSXI3 -7vW3ajcc{5Q?*}VGHqeUeSxGWbtcJv4XjlHmP5SMd4D+9lcg{aO{1(%j>uzYIe2XbOhCm`2U91A*BuP -k0?-}4{iWw(!MiX;-5zcP#7GWxXqanKsm?U|=LWw97zy?W#oSD!3)Eh&dC;#MM?Z##i39{ujWCDtA=+ -@kKppmf$YTm^4vqTfVk9(`^?x>By-lU}MH{P3u)ep!g;M@#{QW$&r1E4~{E6YH4-6E7mG6-|ZeDAP4& -2D`0di@s;O^(;SD56;wMR+%4;f$#92E9q|KTt~p1QY-O00;p4c~(<{`37n|0000`0000Z0001RX>c!J -c4cm4Z*nhiY+-a}Z*py9X>xNfc4cyNX>V>WaCuWwQc?&@Eh^5;&r`_EOUp0HO)LSim6VjYxZ>l>AX4% -13bqPLMtUZC21-bxAPrzC4I>=|6CDKuO)daXO9KQH000080Q-4XQ-&KjFir;m01z1f03!eZ0B~t=FJE -?LZe(wAFK}#ObY^dIZDeV3b1!#kZe(wFb1rasy;*y2+cpsY-=BhTH%u-(HF4Tu^_Fa@ldi>_$KrI|5C -<|NQ#Kb_)JQ6cSM0m*j-+JCZ*RqdppnFT-|vn`mQPY4H3{`JWva&Qn^3h#iV2CbB-BF0inxVXWfTU`84G@(Pd0^B;@3TOLhFFQ>)d&m?}j+@?0Jk|{NkE -s+XlX`02ASD<|8DJsJKJYMrs+~8Z{K%MzwGyq)AR&H^!r^A(zxIMq6n{j#xxBE#7l%GE(Q%E9buT){Gtxp-O7$2d3FYIHpFnc(!5c9hJn|%nL_B2DGa4H+Yix+ -EwgAj#$uLN%)XGEdMy*I*brI>CMrRxI*CfxI#9<(KpmE09MTfY7^;@v)TqJBU-yn`o6fED|7TVK1@GVKB$av1#~cw&$M8Mf?=Rn@JyW?NMEs-jk7Dxs|JrlzEot!XH{PP^Z~lwt(BZN)7wv~pE#S -qiS6Iu~bW7w1Jm?OGcv+8oqb#7A3(!V<6Ta0>Svs>qb#;^28q;%4{IG6 -ptFMQ;(o}EAN|6jh`@aL-5XE8x$`_#nEdRz7dFNDhnfxkz-IkY|8DX6LLieZV=ZBD6B{Et`3kh0+PQsh6@>zr~OhfX!Q_^W0fTKFzXL -o%GwJonaF`EO4S)NPs!~Q8c}y!d!;Fga=?wdla_C^c;o$@pIE-6(x)2%@%K8c;9PxJQ6~Ja0RKSd~@R=bd&sN16*h)6yfX?a8u*HJUnSB -d{>oK?jzm!BYj_+uAPc#e13I7ug0#uVbrnFDqe>E{sRheWMg-qe8FTZohMS?B~A3@qEgIPS$%-l~HwV -{yMegNm{5zPTg&}UNbpHF`eIv=jCPS39cEaN7T^`40IBaG1fv-|?9@2<)E -oeVOX=wIzr*gt(rVKUDX!*T&kX8%6OV}1adKigjIXq2bKkYTqIp^jxasttSL;TR*>yEK<%+zAbGJ<&+Hflx2d^Em;= -MWB-u35n%O{SiZLoaNQBhq^H%JjDo$7obBO9Z^NwhyNh?mPsC6NWB=gFUnl{W}NiK6=3En*)(?snsM# -Ms@PGiNxhvxuyzMS5867!~0-v6f>oh->u)63$rGc(rr5r0EA|F!b>2u3Rl*vA%tCI#!+ctGvvH|xwmh -Jn5U4Xn!Gn;2H2alRi-{GqjD*?hAZFXAWqd~h-&eF65fP9DgGBbG?$6q@?Zkwf&hXUU@N?_-N-dn*f4 -Hu{{T=+0|XQR000O8`*~JV65G%(0tWy9t`qFa%FRKFJE72ZfSI1Uo -LQY)mLqAn@AA;?q4xVR2iqPFR66*;neg>9g-k9CI_Tdb(Ii-wY?V%K6ahh|Nfp`UJTf7b03dLF)TCBy -zI=g!zqok)i&qzg(M>y(EIa?_jJRue9kjLUl`tmPh8N4=i>I$d>Qg&6lKegz0=+)-Lrhjh2U9AGNZL% -Ly7=6q-7TK8GgewrkD$v^T)MhBo_b^*-XBKIip#YLtqP>)jD5gg3$|?3&N`U&DN4;j1e>zEN72fn&)9 -ESv;gwo;~xJ#lQGi;Y>) -0;=Zk%1UD1i@;rWEj>6I2TAN*U#r7PPTPfYvHBXcq#Xu0Opw=EA)Uv2-ETtd{j~0n!e}2s*BjXhl#IZxOSa8nmC?vn;tw1 -CQRQ{%E%ua_J+{2;GV0zHmAy=v2Qz5B@e^CYbr0M*3HDev8H27sXAKTcU%ZQc%{OSOxO05P3OTclH_P -mpiHd-Qzu^K0-k6eEEh?*Gd1pYdm~1@-Yn0S6a=%iT<0tZh%Z-JE>XMBT-A)KIi5t%>yFJDEBC&jAR)=)ymEYEu~-~QesT>5k!k|z_rB=i;3|@E|XD?X4IH&45B@gaG`VM`b&dG#-;at&!lGUowf<;e(lv`-} -IWn#m7a)ZoTIT{3QZDQDn9Z=a%B3JM1WRThC?*6TFZ=x=ot-^_kLIHEQ+o!X5v;KdfX>^?LWa2vq`;d -xi^iZW6F$%P6$7jW>y;{tLcImok^QN_W(9)0Eb7fzhMRS8k~Yz1t~9?fVDnPz9#0z1puFe0Pw(KGM -ZtzttM-C)Uemb`boJ^0muZmnGpschkCCzrv}rlhHNwW09&|i;YJr*@6Ci4gXXOs6x{)-JBN!AG)NupE -*oJaSS)(SZyrE=?Eij4kBRBJX;GqV)#uP$3S%{v-B1xG5*fT=(6*>!iBbD419cK^IF8Ne-98-|9jr2x -7zgUxU^m#&62fP7>zzuvjSF*kmPGNq44QP=3hg(JET|9?&0JeKKbfcOaD8u))QWJl~lG%8W*9CyAEVG -|ER5OA4ugeXv*@lt1%B&*HWksJ$Zv~D-45aMqwpP{lZp%1W-jJQmT5a?Op1KMkn+}JJNPL;6|_;EfL!hkjCdJfR$8mErge -ns4_hVE-Uc<6ZPYx*<&kb|L2mYBwwxvLfpx8`bKS@zOS+iN*#X>w<-K8LcPcla8ui(N*fb_=RAC^uD^ -}dWT)Fhv1uy%5P=7w$=h-~RP-s-&5v9g~G5$=Av4p{k5>NXFb_^sxdCx -HvyQKA7l<5$b;*b!2!H4^I(x`Us#_UzIt7mx%W$P_;=%cw49NvO7GL -cFwzompnBthRt65rG_~gOP#(W(LmRzeRHw&1*z< -v3ytDb0|#$VT}k>rI_?19@1~y`=Kn`l{{m1;0|XQR000O8`*~JV?nCll#smNWehUBq8vpFa%FRKFJfVGE^v9ZRY8y2L=?XJS3GsKO0sf7yIfF3E4+!5wUpSwcDAajRpgDw$$(><8G -Cml^}qqa777TET5&8raOB95|FXY?H#2segcKGbBJIA+%=^Cg&71dL4xoM1hFKcYWd0C}JZgU+b~Nn(x -@opzbiT-;Icvf3{RhV|ASs*El*1Xpli*Xz;loSy^`rSRpmArGE+1$7l6CMrbP}BA%KVlVdP>3a~32y33bR=dPze -PB43c>KLWu;dP$IIw!Ti1=5$7+02wu_rB+8-Mgj&jfgp|rk`msL*YZzI@SMK?#BfcpJQ%yqnbk9UaL@ -2?eGd$~C!LImf9kl9(83@!9- -KZ>j7+zCiW|m>HMIPvGI-zeZPW8QNCCD3=9y{x;GGJZ4P7TD3@zhiXhPp{k;$;1f;Tq1mNC>(v}<;K9 -T}=`i0C@DXMQ|EHuQm>F){{1hAW}vQrzm)@I42kx_CElGW5kA`mQsAq2meJF!4+bVfcmsWByRErz6fp -d9F}x*g{a(w;WseV_@GkgbLdn46T7_cTE9xq`&XD=s598fDVRhc_OAW@l}!>Ns27CsW@fgMJ`Z@)L^= -pgpnb6``ZuE(X@!AOPEfhA}<)3PJu47l;uz)6x)VOW115d#9Y(L1HR%Vs8jilSvqHg^KyHZ#wp{~pak5NrPtynig0yoe%tjqAo?lO`d#ZiVk1HkSd~hIF_H*fe9Lbb{ygZVi17YL -_gtd^L?Imbvg%o7M6_Eu}zQ+i5~Hxf7BIiYyFr(&QwLN}UyygwvQLbE%moxpe8MOB{h*WYq=NIHh9^+ -kmvUIkh!pi3Gu%RNmF{P%hVAcy08MS5QLa<&L&Vb%^GV4?Ffo#A~ZJ| -oljDlG@q5ab%m+lP8ZU36=&m6k?L!@-H$pyudkp|$*@Y}q$z^8*0t7M1USx0WGy>94_MB{Z&b5!MU%!xXJm_j6OPo)0#l?zfs$XA_NJlUi^QDlLr4JneX -q=S%6zNLOJ%-M=4)kMC?kmfp<|PE7%XPhB5J3#r>VDFbElhY%BYY@6wO~_Tl)+40PJFUK^UYFNZbsci -f<>Oc!Jc4cm4Z*nhia&KpHWpi^cV{dG4a&sqRJLxJl4EW8Pc>{hvhpXSNAY*e{6}QmH{OYTEVi@NzVD*aAV -j-cESG_j4gjR&N2Iu@Ff+3vI-=FMvyL6saIWCVyzsnNi~N# -6_V4mkp$!d$pp*gYr+ZSj3Z!$JaxlsCJ4Mzxd42suB?%e69SmAAe^KtD0osyGVemo*$bVMIr1eEe+VQ -gdm%`aZRJ!<(v0W^bk%y->Sn2~Ny33vkd&p$q(t`vY$_bp5f;bEl7V$&{KC -mBL&yAmT@sS(*P89W!0{j4+C&!N=nkbsRz8)O#T>HUuU=)PNWc$w$Jvsx|_PW?~=nUd6GHfEu7zBRg{ -5eB5#K6#=+GEv5s8iRsO4Y=UqX4Uhs4bd#vC=RKgDH4Hq}qmAi!f|M;E_`R~xnsv4I4U*J_&5j=IE&T^U9dwpQZ<8F+zu(wq{!ksEvFv5ngw>EdvHP=!qnEhqr(@ -`GmMY@F&`PR+%A};3ikfOpcu6bL^3&?)Hs~MnR-{NIebK;-RT)Xjw$ -Y3;AuzO1&z$7=$(M|GL>Kbn-e(nV-gFZPxhjc{ty|443XUEA_}kETG{>CVZBc!zHc~sK+r4gdW>0OkY -TiK~=zPA0%Qdb?QPW!|FD%R2rjQcZsGyaQ3HFYwU1XM)-pSYS?Ov6_-~P?b$vWoeuXi_h7jqLXRIaYD -S3r=%OrB8m9_KA1wM2jDm~QogW1qJJg0l;8rkIdrt3EwcpZ-_%U*4%tKUAN-Q=k63`Y${6$o~I@r+dQ -nhp}T(7A~X`uNQivE~)I8m!@@CNoOip`>`J`9+}f898(Kjk4eb=F&&rlp(NekRuaAz23c(QyFRn_m0WaX@QfmeD%cam?D?EJK;wsYV)QXlvIyj@ekEWhh5Hj?rFe8 -QLI^BR1ZN(~9(q-5V5o==-GB&{~7F4DGKcL9P2*Mr*z&VQX);jIA}Fgzn5+jSnsRbc5R2*Qox-us-58 -W`7d7Jq>W+o5SEFcz0YNx;-sUW4EWtFHlPZ1QY-O00;p4c~(0{{R`1^@sb0001RX>c!Jc4cm -4Z*nhia&KpHWpi^cV{dhCbY*fbaCyB{O^@3)5WVYH46-O}2U?@aDF~XPT$#2B$dW6{8w5dMXf4WSBa; -F}?RJm-N&@5%6ljzGmtT@0Wv`nSC~_#OgGU^`_vX!5e?}-Xve;-d`^L<)BGvPC@>DoEWKnCI)QtH6eSiki{_Xh7c6G^Ghc -kl@`Q3eMhQq#pqDhsfzO@HUwmCqh#9$vDNNH0l}Pdo_>xA9#37o_Xq1PjaC}2XlqQ^hzd|*{z=SNw%- -P61^{{kZ%}LHvteRnOd==-ehiR5BAWpfn4J%Hu1j&gbMy%H$ -_l-6dIwkSgh;=QkFh$+=xbbnDsY)u`3SnvV)`+$Zf?!h@ZHou1!;jSrkC4&h0PrbA1zl4XRt#HFSBI> -7_Q)=_0-k`|7$28k`Q;s|mdZf}gYgeqs^RkkECf7bUU4i{DSjNi7~N5P_Qs%xS`8h4^ts7W->Biy|Nm -yw2)s%ZUPxbPmD^(T^6xgAUD1jb3k?S_2x0K?{ZD=Pxevee;nzO=)`wSpc-@CL1FZ4yEvvgSwNSnLK6 -5fIadVH29--&(AjF%+9?%EZaQ~<^8vSPjJ=u9KUwqjtmr@Mn7>MjES0JQ}G03QGV0B~t=FJE?LZe(wAFK}{iXL4n8b1!pnX>M+1axQRrjZ@2x -+cp&4>nkoY2y7P`x6LXDnxY(;wh73RE6NiDlYybJD4VS;3ZyiX0s0L=AB!xD)*sMKA3)Qz|B^51C1ra -i-K1LZh~zovo_j5y!0AV)uu{3K)=!{qiqpsT#Pd!dQ1z{r>rDgw)c_uS^64X(2&LCj88{bslYK1>e0J -TvezD$WvK+3|_H*w9)pMb@(io{KXcV+Y_*kXB^RD+fTtE!+dv@%pkgDmxVnY4&Zmo>Nu$gb42K%>>Mok}%wC0qbkwZ4mbxTE -g@jH>f{GYLXm@8F1>s`EqKVV7**s)e!#I -g&5XRibh6Sfsf~3TXaef$>`>3NBFR`gfWCH~$izCa&!f8Tmiil1^Hla~Ktu%K0G)|DX;Cq1&Bw^gvj2 -CG)q}Z9FOGGOyV-o1cxvq&UgI&4>9z-LaQw-mqpvOS}d0!x3$s@w3WoajaZhl5jrbP#~U85S -yjjiuRPCNm6)$*0t%F~Cmq(kKQq}+P`L0ub-@&&X{BX}F#Bd -+>dG)7LzIxNaUmd)CZ};Y&>s-DR<%ex|e>>@}>vs1`2R}Oa=^qC_cd+X!@7`jOe*sWS0|XQR000O8`* -~JVxf(FQYzF`U`4a#DAOHXWaA|NaUv_0~WN&gWa%FLKWpi|MFJE72ZfSI1UoLQYwODO$+%^*au3s@#K -CJd2%ieX{D;BYVTaIz9PGn{!iultOS{&qx# -EJeMTQETpwzOyS)^o)q|+nQ*uPfarbvbmKwYHxRc>O$59l=adgFhZMim -@UfG(OL8lrbR(NT-;Er*@D -la2&$Z)pOaWMQW;YIgs`mWtY6C(+$5u=F^!%bA3r=iWKDNYCe>mz?m04Tm}zwm2)SLHo}7fe`N(P3=} -(Q43&mC|Xhs#Q7cCSTS@l&`$Qq^?%Xgz%9z|J}5sZ2N?)qk>!GDG8VOdeSCoR}uR8x|v{7rp6-FO-tXi -QS>=&n@{K_IU9Lv9fA#H*--5vrfGAfj-?8;1&)WHFihth`^CME17O^Rx-+5XuurJH^BFNU!Bp1QOWN| -ABmpPPh5$+bs$0%*CSw4BjBqF4%orB5GLy)6!g~;o+?&cr#+9Xt2F}9B81z+zi<-poP3*2TnLFfs(8H -%#F);`XgUkQvu6@(?CmF`6WHW6cxmD754T>p1+_bE#eR`0B_tyIh23Jg515r!%U{`yUVS&2Ji%v^L$@ -lU;+Kch)Don(`go_CMm4sWCrWeuNfty-a$b9!JIQXpdsfqjR03=DC%>uwvs4;#EgKJ -cs$v(u0Nf%yy3a<=RS66`S0qi^tV($ji8lGBusiKb(VNpe5~Y*6w|M|9#&;+03>aD06U7>7Ck& -$#T|u5}HI2<$sOh|JU*{UkD{+sGip_i_{yn$tx{#Yy~c*;37s22c}hr8=OLRMcnGkTv45ON6=4glgRw -+ZQy;buxU}NpLWFunAo!*=hE5x7DZfF83Y~0eCB+Ext_xJkx_jgXxd{7HVtqqH|v?bvKX>Zo@mqwykkUvK@j0|FiGM2VAtHD<$tr+o -}sjHt4b99@EVy_DX&ghuip{%}oy+^Ym@KK<~c-Kt>X;*}USRyo_*L2(&rFW!h`IzI+qx0UMkZ!Agk5x -`cXRn&=VmlpPnCK?|ml*43ydbzK0vM_Xsl9eBbs^f?Ai;mr~qXC)VyJQ~H(TA}Q>4V=}^Slf;c-@g@T -y&ljqK)2w1UR=mv$WYJ;L1KHgoz4vb%~j2+tTndJbbc5bxZ5|E-@m)P4slw`erY$c4Vt(lhF}VE^$g& -2eUB6Qi2Ct(KNooppf}7Xd+nnAfTnK~2g+gsW5%ci+bPAbUtH(7m^h_Z(2I{Z?0b!D@JKsYDKQVKG;0 -ZG3JTdzzmkdi7hV{Pr_4Qj;`=Ih8@{s`+8;XMEj_j<9_geHq_1Ac7gvTy8}J52&j!gRB=&+xlU~XeDR -_=(5NJLNUS@mqNsr_H2(mwV^qEM5q~dp?f(Iodu|U9c43~!j*$p^Dbb7#yFcr6nB+pB(3njYq-2&8d4 -?W*glIE?bN=MIHOp?=f{iGhDl0GOta`^q+RGb8ho{Q#wpxDoOJi+#b~be@H+i_O4sfokZpe3=t;YmM@Q2 -n8YJ(&T}tT&bCIVGIP)h>@6aWAK2 -mt$eR#VF+@nr@9006lG001KZ003}la4%nWWo~3|axZdaadl;LbaO9XUv_13b7^mGUtcb8d1a4JO9L?w -#qawm26~8>O=E;s1VQmq@KU7S3cE2oZ9;dGHGjlPKfFoPTBL->%)I$Ak4%8p^dg}D=bBa%INSgn>Lhx -A<4b*;wyDiE5hT5~&T46?Maj;!s+uO~&|}lUBM^t55qd5s -eS=aO9KQH000080Q-4XQ;4ZKyA%Qd07wJ?04D$d0B~t=FJE?LZe(wAFLGsZb!BsOb1z?MZggdGZeeU+ -b#!TLb1rasWm3Uz+b|5h>nn)t&}2w_=xH#}!?1Nj4+Dy=JG+Wa*V=5!qo}67?Au34w$o-h#1ZxQNWRB -+aCm=!+BL>Ll@Pc+e25XHHk*wi{1ec#FDhdh$?CoeYpdKU>Dk!IGwnfslu`}0z^<~I%`?UaQDK`udqA6Ixw+E5Hs)$qDv%>}z6#ochKvMv{Dn2|f$&LF) -1&v`gm)S-#yF73pyl64=+Ux{!U!UwXe`!JR?}3#LuaI%k6L^9 -_~X;v9R!=2V%LkZQi4v#W3fz=zNQtOjPQANqmq06iy&4b%ztlf% -yOZ8t~8vS)%f@YB;a`rS>6LFRFk-Xc6+1#^-pHUWSFcxFKGk!JH%GSxgvKB>V4eL&mt|iA8xo9~`V2C -NOu$Pxa>?9BC-vxXNp5cfKV4X1fX&uA;#GP!#H9Sh{WA>(`XnqMt=lVBhiMyB@6aWAK2mt$eR#TigE<}tJ001mh001BW0 -03}la4%nWWo~3|axZdaadl;LbaO9ZWMOc0WpZ;aaCz-KYjfK;lHc_!aP|jDb22x{eAq43C{>Q*xpA#y -=WJ)UYO^T@5+Ms~iq!I9MKd+`+poLvAV7kOoMi55=T23LB@$>f`rVBtkJ-t0Cv1@?GP|F$x>}z639k$ -WM@L74Lj1E9WmQHz;hRbn<>0^CpTXd6B}=x>lR6auC#$&3N>;4|s|uc#o4lx)nGTnE#cniIR+j=UG?nx3qfMTtYo -76YQ7}E{EnIq|E=|~`$Up63oF2oJemI4Iy=0{@Kz_QrdwZj=_0Os+nVK0JUX)`0BJS`zxfXd1#4F9$T -V5|dvlgIP6*~js27mNV5T6!eLnJm6&eFUT3DW1hDDvV-Qx(HBs!yJd%LZ@r<7ta4`G4}n%Y4D{!e4k=>)C301cpSUodxvGMZN~ -zH#Y%1snmqcIarda028JMqAFM*qu1v@p1!-hi_cEuSMRP~U!KQrPw#%3F!2dY)S`-imYa=8Vl*q|WWW -NSdr`$AQ&v*hiWSfuEE?Jmv4QQz8E-Fi-{6RJY%^vmTxXR;yaLua#V@54Sn6&hI;tJfvN>F&S^l9{A!W#9Qu -#4=Gr!u)kvT^&9M!&nc+yhXLgh7TpNI0{B7-T^%6pevwg8{5KC1AbciB7AK^9Wr@qZ`*}f4(Jbw5J#- -VzCRlPn%G1fqC80qwjce57P17cMn_qHlErI6)yzIlsHPIKPF9&%BZG-Nl>p>vwl><0=O?JQyreUY6_?h}f -xe+@pN{50C-93Gl{u;NI~*yR$xC4<%P^$P&jglT{p#K&Y1v31B+_)LDSUemRcpgmD7|gghYt6Q<)47+ -n>DuV=LF39FPi&Jg_xdlZFS?q+vIEm~5v5QQ0p>IEdGUk+k`{Vgn4g47DIEh5pe3R47#P=f0M)WsMkb -7NpZOko~h8&szZNi`*kPCvGx{?NMc@*|rI6a&~Zz-)n3o1U^!PCCoeG5hOEK%`Y*5T))?;HB55Xe7%AAjd_=_S%>=y(XUjC_z$kPsY+8ur^y5f}bY^* -g(?u^+F`dbMUsp?HCoIO;#jcB$&>)hQqir*!GU -ZwdACAC@=$K5GlUTB@|NUFE+x1&p?9vFQecU74g8r47(&6U2R4VRXQymlX9LQH&bwsAd!i7_ -(s7O7<9Hx1i&~$_CS`fG5G^4H)4U?0SbzB86CKewr3bq8hhrL9^a_VYnfIjz%CqZY#Ql;@fHg2w~ERr ->GvWN1y4^DYhm4D$rpm=*h~k)lakylkc@Jqg^G9?*D8fSZgGv0%_)3Wdq9;!5_imOvrxn+0aJV2$}}Z --by@QiSOgqjfNB9T5N_qzqFaR$`$2yJO;a#H>;$Pk0b1lB-=*HQkrmLK*h$cok<04!946*VLnGo$|sMLY6-&iNFhqR68Ltv9OeN -+X}rBZ(MZ~tfNeTrTqAj!oZJ4-jF04_+nvEkw9i(rg+kolz9r&oi?6NVdcu1VIPeoJ@kqR1o2%7QQCB -B-1f_tFAbuG{?45eLL*xW(dq&bN;81F15B|aat)3nl(X;MsU>7QbykgrtE$>e|L}()_|LJiNc1<3K@N -*Dj$x7aLkAEH9m39*@*cm%ZLMrBYULURf@(d2NHzS>6S++~8b51?ngP)6(%tc{n$B7wY9BDda@L7|9j -_smE?hR7u@>m52O2jQc@tEMGI}<^jQ*juCi1Iry(k|y4Kr~J94rcuFtA9h4q)gF@?zci`N -3{AvXA7+(7yz+-DKtZ~&WzSSLnD=<%^W7RP$0~lLfojm{={2jqEvU*z#F@g2|09GflKS=6$M4->=#s+ -DdyVvMT;1l -Q>y9<+ooXeOM>$LRdY(W)U1vnAiMF+}#FaO4@c?1c+>Ma?WT;p8>>WcFNTRSXL*&6NM-*?3#%jBCQHaA>@QO0oYW}$(!ncv5D?=wC5NM_QPIR_3~pkn -f>}$&jI!=LOlnZjt5?m4I)}(5S0}|_;DA6Qv4C(A7546z(O&1%FeN3#va6On->rR7WXwq;&fV(AV$(M -oRC6JGs|j6FkIEfO~7E_7Ac4-?U3$b&C*krkhwnCQXgQXL-yIVIx|;xN5B`FUd}=qGdoaATk1;xQxOM -}N>!+7X;`Dbo*j#UY$hVX>oko9gfvstY8sVv%5%25aW5Z1VhG0XKu>8OQ=7N%R6ape;JtJRmI8JMtAl -bRb)mI^mhGs+RAB+&ypn2j8;d$qn_~wDxhf^KU|0w4iHYx~8o}<4d{4hVI02r49Hsddrw)_K~id2VH -8e+1l)I7sw82;jyh2N&yiHerzBHT$YGBnQ0g_W8pK-LLbZ$NCMXYjg5lZu4VH!e^WDYG`Riej2xil=O -k}Zd&k8z^aR>$MdZ;(B>z&$DV^}6MyF0UIJ6G3N=fl9j1~U@p&#do@yj13V8X}M^u};ggg&^vh}D;ap~sL%a6t}Q%qqKCnxAp19epL)CAFlAzs -z(Hwi~VO$kqKNHBSxI_9i|KJwKh=&n$1{CKBmcS_h9I_c{$An464n3?Upa4X%%*aDNz`Qia_MSVt5$B -;%0(dAMdvo2%B4W#&X1)IThtwQk9b=Y=q&J>l=QMD6qPuNi19O{a)i^v=PAehNGFbX=6O`}#|nI<_2G -6zn>l~~)<(yC>T|+W`-8Dh(I{zV639#uLD)Y5 -!mt9V1Ce~tu_E43-W?sAK$mQkO(>PTb?3PBTj1 -g{N9qAG)WlO_xcW$hd%C@zKDk(ZnwBw^yURJ%~gx>=?I(iFfD_jS}z^*HNNysiR@?>kCVvHb?&jlBje -!BgFngaw|ODj_h(ZcS^?k4xuf~X!H*fq+KEfDFkj>a}MBlx$^d>ylE)C$EJ+twf$<+S8me1wq`Sba2| -(LQ|qI4nz|d(`c86vVxKF3^vD+wS1N&o_6ymL;lsiAut=o$6--jnW5>4b-!`C`4AJJKcao084xHVhY* -FS%d>78ZE6`FKy~sx-y|4;Aq^Sa659!a4n%r4Uo*jp&Xs(^jD-Kb?nnVK~+wyuM$7`KNX@Z4wAvou*_nR~!%OEI -1HPG6Vn^qB5bHB+x}jp2LW2rg7T8w>*v5-E?ElTZ$#2m1YrW2C%eN@AP-2C~d -_^jtDWjuPYcR{Mr@_|vke~g@a) -ITzDOOl+`K?eBJles>plkyYkfqiL#zzRe7*=t2t}5jFd@ktdM^MV=l3bCB($h)K;m>rVqF-Fu3}<3Pt -i+XxtK4Nnpq{06xF3c38+n4?|_wPY@a%w$R+h)-P1x|1+irtM0%A^HvAh96g`dVcSTPzT^;yEGmr0$ -TO<`TkR1GuDelrh7kZ~RSk+j;$v`=%O<9|zPdU;yK8(cdCY%dS#%l?&>U%ADP!5}Q0I&LG9YAlP>`A` -iPOpUw4-;|=9-zlDbuatcuLLjFuAIzv42Z9QE!>OWY2(#K%mwbdGFY4UeR+g2K{F4`4 -FB_U9xX0=#RNac%Ac?KPD_nZY^ILy=m|D~0HV%ZxQtofkrp(I@-M$e>@7VR#6}*T{DfO8KsZ*sq{B#M;q -^lpX?i672BvIDOkylSUrUp2h7nkPqWy_uwgA+)zCaK!_5UmH0GD!Fb5=`2CCN2X#8# -oalB3|Tl8?ahj1FC_HCc3Yr&wyDMf(-Iq+tV4x6h-;m(|#)uq5l83<(6?5;f*o^lS?6C>s_ZA@)hft*IFb5y -nb>Ahz9-l`qUQi7;<&_AeND4;-{_`022=D>o+j{d~Z&lz+U^{&1h3oR63t?nQJaJwQIR3nveGPLU_@U -=71us%*iu=T}FX59ryqyXP@3m^5Qw%%sfOSHFe9KzuN;72|t)5z2UQz9A{|!(}0|XQR000O8`*~JV0; -;kfX$AlQ0vP}R8vpt2ABj_c4*m?H -V4$Ri@0~Q6^B`ufs?XVMNGQq-N1pB2(mbfzjg9sP}^#gb(IieQKV#~Na+fqBooF&aX1b<4;One9g4R`rO -+Y=Z#@dX4k$;Vz|~C?F{s%um{1(z`1|oRjt6vcc0N5mJDvDHj>h|k{_6Z}5*D0a)EE2HgCo8q4{>}rJ -{eEPF@d>;g-#g3#jBIYplEG$--ZZMrKJdptPh7WahG~MfJ{;V)EsO@OdAF;TU)xY%7x2W;*TtxSMsCR -KtUx@HBfHCX!%ct6_4~j@V;Fb<}$ZiWZK2csV&cC1YBOOuiK}();< -Em@fXQt0jsQ^}AxuCXS}lb2#$L8aFFRIrE_LM5#-Ow~ceq4&1YsLf<^W<4kkR??WBO-9qHpjW+CTdq* -aYX&a08_tp}}>mPC4_gP+LR+1|VSopdG^jwK_*pM8@n|)+65e3YUQg+ARqUXIXuW(KV&IuvtbMAb7?~ -(y@6IEC$WpC`d#4c^wC`*%vZ{BPB0FugBmA?sGI|QpGYBo?W-F8dq7?P`wPnyOll`&ax2$+_z0eC|#v -4yJE8(SZZ!h0Uv5_N>fNRkmgFNUb0j1r-w{(5Xv&t1oO8#Z5P1;|KyZlXBqlr -Xd`24QQQRZjh06pFVL+`zY^qwg+lS}j%IwAdoB_Whq=E13vw63q6|pQ+t2Y``sW}~5Cw-w*hN2k4*u` -y>O5@^3jbapf#E6^1i2A|EwStjv0V8f2P#Yy{Lpa2_pe%CHisPxSu;~*H>Ma~Ia^7kOTK#rc`3PB_i? -Za9(lVqC0kgx=p~xueBClRrh&fi+SO!Is8M-sjAcsG4 -p;Y#IybmNE@<4Oad^#6E_s9E>?F5t~BZa}WFg -xqGS#yftHMiu5E*)kI%SYI}_SWJlN6Td%`$BRC5XNH2@3ej}o05Xa%n)Q_#WMDxKDtR?6`YLhdZ7~%C -N@Z^7P^XX$~dUw4lU8Avp(2_ndm!Ci^Lxj3FUnfHVd5C%_dJsDH{~tMO1Dm1x##G=2(n48oHfdVpAX8A@CG2g?u~;ImF^V%JNafp-$gc^Gn -7A`J45l~SICF!Adhk%SOOpIJ2c;L=>E3JPoOTa7HXY1HSkwZ1ApD2p6a%E?o-Ug7r}g1f~oG(%Po-zn -=Ic;Cj4snMJS(>P*?(wkV=XfT6}Q$@c%XVD8`TfG>kqxw_EvNP)h>@6aWAK2mt$eR#UsQi7Rsp007@7 -000~S003}la4%nWWo~3|axZdaadl;LbaO9Zb#!PhaCzk#`)}Je`gi{oJO_oO)>a;CyCSGDWKDK0+NMR -C;d>%HsEalk^@LJ{*#XnDSzNOe#GaKEpS?UccY(N%p -m3N~@GsdeQr5`04d77hI90m{vIh{6bS+D54jPATTR5pHR(3K^C-_=1eLw6OvqbiiUoVFH0dc5z0SF=A -SfQ*3S#77C9f+OAE;;3hZC3s4U>C>h)$)EXkNE_Vh7vi{qz{Spf<%x0+7GvZUHCS28cDRBS+$mIH$e4 -@lfYi!08Q2WK~0(uvO8>r9L63zZHwOT0w3_<1=E!T$5Zk%B9Rk7B38}RxESH~NdF -Z$sQ_z3hzL?v8W6^OJ`{Nb!LwXKyby>LElMJ0grkqCcDRb(D)SxwK*){Rf?jh`NdjM{)pAV6KyJlI!P -wMLmkb2HW)2;QjoE~P5~O6JglMIbL7?TgAR{v6I_*1H_zcOL>WV6A-?;ae3m|Fm77YF+mu3L3jvJlJhG@07<39WYCJ|IS2>>EyXn`0JMHc58yw8%4ehy5UUE3OVeV) -0%tU#>fC`Jp9(2H#Pw_At#eTfEdudW9REU)ToiLAQ4&>^7?M^NpRlCgre$R$?{fxHVE$h-lbMW&@fSA -H#i$*h7nV~9L0hz!|DsTSgLJhjmV?5PSS)>-R0tOb -R4B$1TW%Eg*!=7L@i@$9>+|YD(-ng8<^yPV2U{~XR&p8Q}ur~VP#-UVWtqJh>YC~91YdnI1TDElOQd@ -hF5n3(_*p-;AB~>SUww&&_t4QAbu|ND^H6}&!iMGj!bh|K<9#ge0Sx+9Ftk7nw01;c3b6(8z_){ -yPI*dnT&cH#CD~7^=M1CzAfZ>a7&FqM^j_%hydX4=#2}Lj1F43`?A^cbI9}V$jwt>B=KpG4)=Gn_Cl8tq9u|SO$DUV9|Sp)qS)z9ESfSd2;t+F;4ZgJ= -J_lBM`(suC{|U3k8neo<3>)oV9S+IsuZJJbK75GU!eaC-r%#Mw;+sy6ItIZ!dD9&v!_N)qdsB~*<_ssWzyG-M?#@zDU=Dnt -m)uzW<2FXUqFT`Jry?*vyswU4&|NwXkDff;tS2mdS5<-kHz@0+-Kp=B!=ugm!oqj -e7x;gJzFyj<`n<+NG+PRY8j$I6gA(^2sN)B!PacpMmy)Dav>MVIi#TZmRO1_D->#8+; -H;O1Pvh5p4UliUrS -Y)wdoS{TT?LGb)Oo#5Yi2Qi`2O44E~mhrQ|1rt9G#jSd-S}FhO`0(lHBy`*T+HW^7+Mx)m1FPe}iIH? -;L*ww(&3jea41RrTNd#rYQG=jMk{|?tmKoykI3M;+W`J-{kh!pwsxs#=Rt>syydgL?gL~f#f86j}E1* -sQ7dT$8f#YT*YvCk2bo2a^I|AAkJ$nIU&WDD)(CO}CxohKL{_WOQSJ!Y{!)p!LM7haMnL1~$L|~E=KQ -)JDc#Jh_jz30V$-XN+?%A+%&rnz0Q$+sJHE@I`5{0tBcT_ZIcdS$cZ`W1?+X=dDNh2v%)qz{Cx!A88X -X>paYKWRj{U#)2JJMDv*19zb3mxLGWk`SwYok7f1lo8qdZ2tnHszfb(g61^Ci7vDmY& -8AxMA}(GssMt&@gm>4B+Sj6oWtjV=8)w!Z7Va6~q5YWK*iS;Gg04lIR_Z(zck3iE8PiW}$;jFE-br8? -fj!j*Hd^`53)0JZx1$( -rIQNF$SkLGHG;)ge?5P9arWZm!&$P6pF3q*yK#?!Q>vS-a!t-uQ0Q5#8EwV<^>o0Q}P0BjG2L#s-ZyQp_|XUr -=W|DCvUZIq+>t%MzjG8i4oN$gu5gsxg_obF$f9r`k1Y$F%?y=KAP3Jk2&I%R%XHi?aEYI%${t6_jv#4 -P6C-5r3lKCKz?aOB13X{f;WSZNGHq&COQ#%i7hl?PuNHjBY(TmbQLsmgj-C>}JABO-?lQ&T*xgxpwT9 -{8#qgdxD7$wY_~w0;4A0m7DaP+BWQKznFFNA177G@V6}W-re*8YI@1g{8Gf;lnk)3;C2O(I%L{?$90> -Uu2Iw9GH@L!f@6*r9U|vz73O}Y?w9~`$PtJjyqAsZDL8Ul_hza|z8T3+Rmr!d&z<@6e|CNPFHlPZ1QY --O00;p4c~(<`vZBu}0RRBe0RR9U0001RX>c!Jc4cm4Z*nhkWpQ<7b98erV`Xx5b1rasRgkex#4r#&uE{9o9CG2JsPmAzwn03k6%PN1E}xy}a0$r28Ywp5zVvkeevx61(di> -gZWcw^Z9R#d2TqNi@vl3rCd}Jazp5q0;!URr{GGPaes#?f&P?nIR_4*^3gaHIPyj_vWZwB3TLQ?*5i3WrCQ@&V5&D<4b -dD46xm$(ZHMcnj7j9ZBBKp|um#jgu42pN|MEeD(#rPmMQM$Z;ValM?k#N33=*OG7^aFOc&gXOy1%%D$ -2KsMQ}5cYV}+jx~k@tQFUhwKyEGwzhLD)~;_2#_5$MZ=@?FBB_bvMm7?!YUTMOxUk_iV-(WDX>`p3f=QJK73-zu6g)Tt;lWs>;(e^8S; -1^9k24zk(|0YXMGr0`pttp^#djSVpxi^{HXgo~L6Xw0O+RoZ8C1u(LIUf6$nx?0;?{n_Eu$aY!{ -nk<%z{ipLI9*}7f^d>$*$I46f+&YT+(64E<=>{9k{l+O=6YqY5S1w0o}GybnlY_dJSNd5SSiK2tAE1U -?!jH4Y0My~R!G0x$l!Wor&)w*m<~IG-g`2e{DWD~H`HZ;5p0m)(A+vu3 -g%5Di??wX|5i6NQJ0?(3Qtw-0nz-ToVp4N(g%B^I8pU;kX3aVhy)Qq33K7&?Q5;G?D2N9Qn>f~|v)UZ -~vpbgR8>thFby4tld28p<^Y?&w*ImC(kZq&*dOXho6x+N3BG5$7p&%%GB*w?iZ=ZqOcp(e0-Wx8z(tA -!qj2WAAwRM`S+Cm8^Z)pP|ei_y3Xe!_hJPJ`r!YjW5#yRz_^r*xhs76#Boja!k2HxQwrh-)8VjoIT9o -^nc3O2}!eNbNT{jf5p}*`vFi(0|XQR000O8`*~JVy(bqsz!Lxf{zm`+9{>OVaA|NaUv_0~WN&gWa%FL -KWpi|MFJo_SYiVV3E^v9>Ty2lz#*zN6U(q*<0jZ6KPMj}y3WPh`NzQxm+KbI>3}Z8pIAo88I~1vOlcN -<3=eMU``jwa-aNj ->}f;4_OcFJ -Js&Qy;O1`G*K>cCGYy0$D3Xmt#`)1U>W7M7cY|32CW<)ewS4}5Xlh^BTN?7%}0iGV^T6YTM -VtNtO6`R@AxHFl+JfmIax%w;yED&6x;70ALT~%N!Fad0R?iyihkih -l50-v6;4vLNGAxa4pc2y(A;a -+y20kAmJwa6RP1TDLT*uq9NFsCGvWxLC(MvL|iA6fMhc~jDF9v^Vjs%l$-2DqLqGyxo-Xs_C)K4#2&U -CG8Y=zZJh3N`$$RY0_^VGM2qlErx%c>tZwfom-uD-h~dc58UJHV@PJhSo$<+AE2Jao|pXny7ZKs=Sjp -^ABxLMB(8rsL@dza##eBV_po!4a(~dgjIrI>h_S?O=ni2O{_uPt8TqcK`rkVmSLkF_`&DdT8Wiy8?*% -Lhdw|46gPLry@bcs7*azmg9r0W7r>kJYr*y70O}Pi0VL6@#uJQ3+_#|B(<7#*ZDd!sMNEWud<`s~qeo -=iob#9jcpq>XsveGgA-)53u_RdkNL6H_#k;rPh>Eome4-NfdD$c#>M%X~x(($0!YGe>00W!0vpbZTUB -CsV364yHRSdacVFa{a(JGFjtV)``>QEcN)yYS&RyG!}QdoDV`hu6owhh|F2Ii=tcwpCuE;Ajh^gXZgW -+7z5!Df|Euu0~Q6zyMmN`->;za6X%B@52*U{?t|OsI!(%<20`P&}#8&jqowun)o8-v>WbzSHg}*oQhU -rbykXe7gfr;o5Ej?Wy#-Zyu$p?kWrlCzS76^?~N!=h~Q^vD_x#$?+h3ioSLrdGK(9rofB`<~5vzd`jN -e-Rj%6nV51)9eLbnrl8HxoCjjb%QBw*K^_<4F|UI;!I!zHgE9ew5VhP^5a7XA63#$pOu=Ud@uDq%hjd -Y)Z}Z{-P3YM60(@jCfSU|`qvI#9VEDIf2eG%7xO=js0GwWMd~?oR-?x2T62K`-&{hH=xct6RvaA#tiR -EQ}G2n5kQz&lWxR)c39EkBf_PYTap`P*fh2iClQycI9|6s(CsyjMz&(Kk#HB{&MDT1Hqbcfwf()|<<9 -w8)l%cmjIeb?;~-h6$1{pPjQ8gR-KO3GkYtLCnSUlu22S$4S3yZ;tn=DGsJS%W5nUur?Y&{-KThjg7% -5x&GmhzlT17d0H5ION9wa|KANg&{!as+l7H; -RfM*V?@1?V%_Z`&PlU<*=oVA{yq$QkBXiV4UqQUnE!EKuO80_c$ogW9$0G!H46g)RkXtY{C77^VUS|H -fl@kxRik|6AH*r!Bz%yNw2|q~N9Vu2ZR{un@`G0`9egMsoxl1N6d8*|_&;&*|q2lIFNKczX%ny3%Y#V -BCAKy--&NY;Z&ep@8CrsgwoFX0|;5KoFrpZh^{EI0!VHu++XsNP+;kbr=_BTlfos}R61aa=T+Iwx9 -W4FhGobfd$@$W=B~FKp0`Ic5WW -~FBU<#@;ObraN!t#zv=Ii!6>r`}Ex~up7pR)wrOiBxL`1C^P*FeL-_SEoG-(B(NRA6{4q$X9yCt~yA1 -hjBoE89XhD@`U;UHczE(C2Bd8gHz$3VtV%EcRdoQNa-kup -1Co-Zbroj6aZyC@9r!6!brNZ&&6lbv>b{hA$DFuxwbfDa|{(z*igNg_7|BFef+1V=$euR( -$!m3C)~Jdi*D;4;ai^S^fYY_u}8-E#zE{hmv8z>7er3eteFh7l`WSc7Ey=nW{O{$a(8%aZjG#MdQSeC -RbX$KD~5GC5(01~kAHsjyC$i7)EfOM^;xCVM%w>VZgM2s4$k+xBBj3(y3Fg2$>PebikJLZZ6bX5(vq; -Pqv;WkuR-zWM%5&_o-s+5EApW$@I<;$2f$gglkp_BHHhpcj7mJM*>omd0UgJ~rv))@!JaUwFz(fUUtqo2Aj*mp?8ld*#p -=rz)*D>23!`RFYB1%_n+i3sC*9yn;r36rUzR_S@A-=eED!dd7j!G{r`G*rbPY1xt74fnS-A3iSQw6?_JL6xG3=0Oor$wT?QnEj_$H%|L1h_3_=7x-Dld`VxlTs;m<&b~YRWqZU$A4i -F7mYY*v%4idE*C{A>s$DSWAhT3|5SUh$Wnb-~ob*PVNvUPwG~TEV28L3H%%>lL}y#=lw~(CwUfNDBTp -hj!j4G47)>CPqS(E3}@xSvfqt)ypM+FQ#E~>w5#GOQD+h(dXYhW4Z?~T#mOp7jV|G7rJMG -d28_I;9+D|jxlcssaj -v;D-Z*NHc~jMOF80Un9)JnsYVtTHeC_i}>1XKDc{xuilaWQBOW3|Pae6|G?>dI$eENVKliZMN0{NykV -3Io&RJaD)>@xo6D}Yb^4Q@f)27e66xN|U<&8(xrqbO%O55z##jZYT>4Lc*V*lW`NA6McVVRZoRsZ4f54QU96i9lo=`JFCwuM{0Z>WQ6qWs=d(`%E~xobl|xpgR*A90Z-rXnV+w$1#h~x#w9#m`jD0fJtj7Du$C2# -V7ykHE1-3p;POPVt3ekZnk|?DT)e2*Y^s-^kA)20Ek={)(k;cn=p?hVz`=2ylPmJi(E^8Bf^%pXtex@ -HGrx!brBOAFxiXG6@Okm;yLAqc_>{yA>HvI{6NTV&@UuntdG`szIya$3{;Z?k=lZ_jTBbajvmSUgsk< -XtfvHLGu<#Fjs#W8`YAP(wd`bYOv2ala-$=B{;`?7H@Hc>d^Vfu(!(4Jhtqq0zEUxdibXb+=XU-9sXy -lZlU1XNZr{)uX6S4dBGFy0I*t3y=a-Q)%6w~@TugO<=;xbaXo9uH-#HH8})mw#xK-JwWb2GWY78x0$WkWnS4- -5)djj}gNBT;1pii5elXcmkV2|1(uFJBvo#2|=%|T%G;8syskyOp*-OoY(|D^i}A)3lnj;Hc1<^;I%>X -ZPh8hHc;HRbP7YKx0cYKr4$ox)Lx{bi>MG3yi4oQ*TuoZJF4+Q#`j6+z!H3hIchdR6~B@rbkJ!y&Kym -O)S|Ze1PKFKP7SS?DD}=4briH!UnsR(`<;Xs -3HHhwuvIQtOl9<5;N+v+Y#iY`&?_jj$kh-oSY6nZ1GdKBa-XW)45b)Ap%b@I4uf4!45`X2^c$IW&$P* -*4lJEX^gv#Us0Z4xBTi%W3OYl&=eg-**PY&G~4*Ea&_pR00?!`0PX?ovPb89xc#h?^9b!DWsn``NmMX -duNe-7x9k~N^ZN>oCner~(-sfmtv^RI+qe5PaTg?O=r;jSvF9KO3wd2|k6)POh*gVgX`XP?2Wz)=#Ax -&Y98yy_3SWk>Z)Ig9tMKg+lJcp#Atg`yBWHir#BH32C%gbxe1Pf3mv-iZPK15w^(vm6F3_3Z^HJvj_K -$XtyTlc@fbb+&{@%_T>gjHiP#DclX7EhcT;^M&WNeye?%kT%VB;|P7#(CAV&vp)N1M&j{l5fhhUwP}@ -x4sIMU^tIF*Ne!{>CpDc4j2);e2$3%p21*O%PlI?z9LJBRk2=ZUr*65!AXe -inpmYc^TOas2_Hg1GDaLA9z*Il>A7vD^mpTa(O#pl2I^?${y{E0XIHZSQ~&59~jMw7%9{bH%lt=XEcU -ZwXjSP7`+6xZbux(B*T+5I+Dwt?B_9Hs32jv73_44J#JXS6&-@s(Y{`(&4YyvnySxj9qY_Y`3I6kS4w{{c -1n6+Icznvcr*e?g<3s^$+g%hEvo0{{%}=$3DJZY2T2@l?!X9k680I%El!yM{D&Uo)ahE_1-p<<;fw)# -VT3^6Ta0>*eL2#s9v(PG)1S?c4Cg^>!B@|ByH!S=^IIMr&Bab9=!=fh0E1pcX`4Wo4;a_j9utc0tFpB -E1~-o%_zludN1-&|q&n(wfC$D2kQ&>l+BZ^SBJ(M05>=;VnTnk)RPwd=>jhbdjsbLE6Fi4hkM;CO3h* -G~RN^2z&Zi4#xY3VxS0PmpuqKCStj@+X!}pX{8Bb){1$%0068pe)EB<0bclh;X9aW3*KiuV1Bs5xZ4u9JZ8hN*WcA7CHtoGGqsHN!Jos7Vp<)I=?aYVeU3OMopicC0&wNvXj&-Vybcm*|R(dy3-JA46oXm#6nQ~1GK8lXH -fMQ2eHw{50vA{>57_5T4-O9KQH000080Q-4XQ)i?&ghvDb0J01K03rYY0B~t=FJE?LZe(wAFLGsZb!B -sOb1!9hV`Xr3X>V?GE^v93R!wi)I1s(-R}9ia?7-2bmqma*v}uZ_Xxbw7?jCJg99!H-q?V-Ocn|&Uou -Ne8vK<-m#S%61-hAIv;!B2=S4O+!*YPh&X1Q`u8d(=eS -lSDv9rzr$ua4lvzwijRi|A%Y{&uzG~4Rxs)yX(qJs!6L-ZFFj0J>^$Qy7fyI@JCq4@Wa -GW+ortfOPh8(6Q&(t5hh?4wuW{OS?XX|nXx-~)>XiIC{qa4F)DhHf#y$XB0ft$PqZC>d!Me#ELear&2 -x9y!ZR~9vi*8y6Tpm%#&OVU3J&EXdE#`MnqDf5rOrEVYdAki+8rEI -L;YS}FO5<68?K9f8C{czRk)tlkIwQ2b5(yGVCsxQP776?~{Jk7UCqr#0L8`dpY3mEU1u47$r9*V0D2( -^SElN}Ca7J)uy#e^^@f!!MXfOsRJM2rgX2G$o2PaU9Ct~Wfx;I?LJWx!JD9Mdh_$_Bbe*UHeF=%$E#G>N*P*sCq9h@~l76M#F#K -=HjX8=9wQCHc^#)76<0bF-m-2eeZRUucj{O7d$Yd2ry+YkV_XP&Q6#mtP;f5w%_-)Pqxqw|T9fP~Pe0 -+H_8P_xA)60#9ewKmfKFu&l#Q;WIIEHb#en@5$&B4lz&qm!n-Ep9LvK*cp-maCc+K8r;5ydyAqZMpu* -Myhb(KR;OCk0)|lw^3qX-t5-qLa?4f$JnicX$^EsmU04qK#xb&t%&67=u;EU%wJKeB&_`1|3Qre{oR`j}5TG%ZVsMkI}XjJ1t%p~ztvF$6t^ --lffGEd;La|3D6=}U?xoXvbb;7dhHOIEaN9N$^2(Cg-S_zh4?0|XQR000O8`*~JVVHPL3jsySzgbx4! -8~^|SaA|NaUv_0~WN&gWa%FLKWpi|MFKA_Ka4v9pwO8A2<2Dd|*H=tk1hN5F4_g!q1jtJl=^{bbF_PV -VDGEWhXq$;lsw9;}UF6?8yh+r}bUTrQ -H?Y!>YVKSG-BDpd)Rra+HIs1%ebTxDBDIg28<{3!8<7b!Bebd8C512UiiFUl8o5qy44i(7 -UKB++*+Ghr3UVZvB09wLg~h;}xF?tk3^K^+R>2EZ2T%tB>slQ+gRUB6EC&~bBr*tV!JlbPV8fjv%Z|j -z=^suO_--21ZnY8u7m6B0#dHoQ@C(L_yk=T<38?kKX}?R}CqDtzT#EveL(?}H-(qb$zJ%C`#!HAD1HE -b#<2OhA^MOk6DGx7PJW1GKnuVtHHrDmJz68pk%!H!bs>sArTQ3FQWSgQkU^yp}#mc|{9uv5=0Ql`jaA -x6fOXl1m*fScUd+Sqq;8l^MZA5W_SKK1;N*TXxIeG`9BM(_gyfe2P-L7pRwj|8~m5Gn6Pl+&qSB+d!8 -d8JAu->7&f#y}~SDQRTcz;4i%(y3ruhmE~Za_Qx9Q61?Cgv}O3z%aqLNjIElnu&uJUM2MTr3-`GhY)} -l>LWTTfrCY9(NORf)pf^`1VW+_zw5Hqbw@|@0|8foIG;M=D!U{kE<-tV9|m4{d6R6|9z$ad=DSER#OB -%VhfNbEw%^k|FP=p?rFDwVNZ;|u-G0WTbhr*X(PSHE>Ef};JAFd{NV%sTc!;{;s`vY_`Tz6L -ZcOuzibO?Z*5dP4enk%DL^RIQC$tOrJ4m_k?Y7B~qw?b^#T9@9RL6TaA|NaUv_0~WN&gWa%FLKWpi|MFKBOXYjZAed2NwFZ-PJ& -h41?-CVBvka&AaGH1W`+Nl997#=r^#Y{XrP$BrMhH`Lo0e{df57viJ5{&D8Mk}^^ZDV6>%#O7$o0B!q -qw;Hl0c~yN^;%ONh}08f?8loGieTig7&yApbK=>ujKx6{i_EB+Sbs_ZHatH8~SP$Zn(AD?;U$_obsxQ -V{bNocF*0qhuv+j4bGXT+t -hrJ3+28c4!)IBiw)M4c)CU8UhS)1M+q&sgo^`q_Hrjk|Z`!@ReRa&=bj34NRee2EU9GaKy2ur@EsIsr -Dd|)KkS67B-izy^yC^D{MWeGEW}MxqRkmxj`mwGrciT5jQ#b1RLRYHovMSG-T$ROHgDZdqLeEV7%kld -eKyG)d3zgBcH=8q^=Q>yKZ@P=RO4W(h>Z0qm?fl7;a~N@Vmagi}lWn_xVlernXj}O3&+=2)?|RqCllM*C)vLOcuRqqS41v7I0qNOPF -jLe9?#HU#ZSj-N-)(h6?YT{Q0UK+_&-FTk^)Be4G-$hKx9WC{wyj=etv;?=UA0A5T~*R0&!%)&QCx`Krtt -)3%$u^>bSbf$|C>x;m4VJb+M~1b^b<(iaSvMS8zgg)zLoKH~6%IGYZ4WgOd(U$p#*`yA8nMhcd#SPu~ -43o56Ek6acfSvT}h`Vkg|yiz-7bE&$Ik)=cfHf(Wl@7g@V13hD3l;$@x#RksVI+bY{=MjUPcBNM1>rt -mn`9nxXJOKWM)i=0So0h_vo>BD;-6j{dNQr|3K4U4j9VKtVfqCe5CNue4TUW5U^ -$ZUG7F+9TrU3D)OVgiFQ*<{|OAecD*7>V?m6c_7R>E9<%C=iX2|eb`hjRlMYRX+j^P+=MT5v-kXGVT}TDUCaWcakyU+}9v8dNOFh;?~@N`Fv3U#RDr=84R%N>J3oB3YLF^-M38*xNQ*?u#I`q0PUrFAKtt^{_y72>Eh({<>|3H5~L -AOkUm#BpDF)uUp;^NmD6Zl@2Y5{XHKIUWK$Ds^xSFGXfO($Z+<>qy!zqe&wp8*9RJH3c>dK>bpZc+_VwQJ>6;J!yKlaFwzs#pD -zmmVJl`DdpNcOJQb4vO_}T#uC;-8X(CL#4o*ww -H9XOLeOr~S+sHZbXqd_=SYBEXxRs)Xv<^2%}zF2KE2>sk5c_DHY&TqD96BMXEs%gs*&+uLYlM%IITj- -U>vxGg;e)zu@Wr@lLo|W|u&W;Ff7XT?NTObS2LEs=~3I&Y?Z%kzKaF1{l&0<3u*(CzWW?hCUrSeSG0$LXbHDIzU-^kW6-#1crwhq@b&1d%pVL7 -GrMX(58mSZu5fNwZM-W?<_h;4$Nagjbi7rM0&_@4o8ez_nKF$g6S5;F(_qn;B*Lb!1)zCf&Ta*!uI>P -KJ1qBB;D{tq0`2JSrheILf%gP~JX114= -Q3iYup0}@F*VP2{-3(72Ob6yyv1Fp@wsRcd{mpZ*6cn~Oukj!@HEhNhtHlw0&c%{$oaL`iuB-%+dDu9K|zkr~mXK_H; -18gt*3xxL&AtNCy}ac0nB-sb}%+T7xmTMs@gV@S4tj58(N20A{?B7?4mp4Li$8`k_eG40>hY -HQGEida99OCef?#BOQO_SZk(77gta|CdMi8>k?LX7HiK7bUPB-0@rm@}O5`1wXbP*gj8A2jt-^?CCe9 -=eXV1TlDY$#VZ_&8*#E_6WB~WX;j~BG6IR=_b2$JaLH#ss)Owqjl#uA0GK;?HnppEv%>`2r$u6UtiKg -8=5>rP@(zx1NzY-wkIk@jXsCd1gsmKZDbnGl)eH;fC+fwt48n1Rt2@j^a9C-tIrNM -N}*Vk35;5kJI1qtPuCpw*#b9KT05c~l+Ji0b<^v5?l20nH17V8SoaRni=;fEgi$&8g#jDLvHh<-_+&i -Gzq{{}J`|u10ijQG3p8&-eT!w-AB{-!Fk|FN)O#GPxEh7r=DnkZZ0m^g;0Fq@63aR_5%b<1fa$<2C~$ -X>L3}05bPp3iFziPG8g&#dOx@akL8y^q|8ph9%C8UJmH^RkTfjVp)^|iCkwsMC4fW-hjgJPuFFD1X;Y -Xo7C}ItPeAAW4AdlY?jMU3Fx<^wt_AAdAUT48s&Ya$SAW>r)x07dfEi0t+r*h^3jO?(>Xg;@l@V9Ta} -=+GFr8vdP7B%$-V9ye7m1BM(eBv+$KD_^*#RU55=L(}Ez~QRA1lEBXA0Cw%S*ueX+v_9|6rXMy^@ -<+XP5m>XeCKz{$wYwLPQoc|JER>HF`$H=b}4%5&bpBapT>T -534Q~XpR#a)-oIg3&o%KZuh*bE2Rcpf2uRa05&)*yF)AMwY997XR7R<~&*2d@l2o)lV -CCDeEl<+qqL-|a441Ekdv`Br@26tFK%AOdoh*kNFM}a<0=RIQw^NM3}N^iE^4ed@#yFjiB9;FuwqJ@3 ->?0P8>CYu$WW6xNw+?c<4t?7|%Zdlk6ShU!|eD}J@!#y7hwvT-Ey704p7>YyMK{zVu4M8zU+rAfc(8- -HiI@+#pL6LnW4MUB|9%VREIk4EZ;97f -67b&<`><3HWkwDu`0t57Sz7Sl8HVLjM6fDiV*Nqv-%dli*jjBd#Cj}HHyhedy)KDvQg-ofb{$1WA3!6 -lPnn1^<>8>TMs+}c$$+tvdCt*U6yfMY9918W#e!hh?zyfovjQ|$3Jzy?Yq>L|P=eH6)Vne_PD{2-x`K -dhvTBDR;qO#l<5b(4yu+Y((1ju(`oi#wP{O+y; -T4{NFsy!^z7-=J0-uvZ}vs)hh050T6(zIrmfHMJ+GMoRNmEOqk^UCfXmis(8_i$Z1zq|`k@Zw-J+Kp;ld0ajHZ7lnyKMk2)d7w>Z9!r -ePMr2{m)^|xrWn72d)Ea3qNdY%y$C0Qlrg)el_Ag$gU5|;*!mnV88`FTiBkmpr{c1->8lnDCB{nsdCm -LaKxhNshb=``24)MqClH5x_tK(iy@&UUSMMMf@mAe#h^4MUd2c&!AW9bRqM9lmPf!(FOSsv|xbY3&>H -Eh^<(Q{Ro$)+>~3fp+R*9noah2k-*t;-I>{KdrMm+sVs?^zz`&Q6$D&&k}uLPir|KM|$OU^zII?CX@h -7-s`~+#R=>r(@r0AQ@!J`_XWt4z`l7yszZ8|H!GD^gi5C9B#O&CZ&zolx3#(+E(LsjSWLH`u?F!GoMxB|MnWMNs-@ocfgE}j5Jz!&JWl`nj4F{1W2lWZS0j^e&pl{vYS<6l=Qd -x>>Rqk@uX!@wLNvTA;|7k@xYUfXQq#v`vVZT|U=Oszju16Auzc^>Yvu)eXB)i8NR;ttkbe -I^=v-7E_9JpXDYi%C26{98gOXq)Vl{l@(c8gW0FbN!+ICEnzkb%IGzIdj|~rMqBma?JM>CcQ3w$jVv8 -K3$QbpPtd=E3qzY9ISDNf%P5j*N*Y{?N=iajOf+sZbk_9e%Eix#6LFVa0&KzRD+-RaGy}}TbSQrqr6J -U@LN-&(BoOM1dd@gI6I75UJKv!7dDNE^@YY-UVXiT(0l>o&CTkJ<=riDx1D$5gdF#jT$gxh-^LuN8dZ -dy^0)dF*m#6>G{5l7|&3!VP;FPNOy1B@*fUL7A(_IkD=wVG8rV4C%HOQALP^}8wLSPej+S9OC~*E9(S?wJOrUDUfKRc`_JlC9MNH!Ok;a34$VoEBrGhL>WFt80Hy6kB;IT)nYJ_x$A4X;3+vvC01Z( -OQ`=1O54vtsb06h}K0IjvR;+nl+FU%?RiCNK%d_8CxCr0&8+W1h)V*%N|h7VH{-n^}bgj4ips>Q$?MfVyL5jh5^ufFl_rV$1UGl{YggA&~yh3Dr1)XdWhu-2)Y@ZjzyH$`btvjy~~KvYfCWnciMifL;tRRyc6B+JV(laV~owc@d+bVK>K+-HNZ997IIDU ->u>L(rBR}H+taisW=02LYFB3DbE+mi2u;7Tc2BAH|saVAOc0qbV}U@|<|kvazaV@3ho9j~O&sAfn ->eKO%XdzdSXQHY0yvt-aJ*z7tAkRvwBT?>rUGlXPRCgz9&?@H5&6#gInNUbL*N1amfgn`^g5A(sEVpa -<5*`djDAjp#yUqH~CO+#4w>L*P2Jfw_IB3@4WY}0$9H5n$UPeVm^SeAE>6-u62oKoE=My%jB+UbtkFS -JMRQOfH6#pHji-FWFp1d1nTzZ_m)UmsHHr>&vP1Hl`FAsHBx675bvto!O^o|{1rX;8#Wr>-;dshBb!i -uy?nT}TYxl<~ME6BppbFAx|g<=)H(hXW-G5wKtR;B|5Y@=ym2aKPFqVIrn`D*sR#g&Qd&M=j6IEIuB#mqiw#yN#0gy3 -P@b*hpgh5zHkVa~`MaL&`=(Yp;tlc$p*eNtSXsnSOGk0z7*Sw`eKi&;iw=Ww<+peu5U!PfuMp_>&CJ4 -toPQU~7Nw4LYV$^^_1>ljIq#LmHeFwO-OZWn28dm{YmCNbZqPINn*!)=~3#vLibOh|aF7+}0gaGx{!v -+{5ShZ|3LvFedJXfD(Ct{F-WXn+AnC0#I+Ujgb1+mXqT+i?$OMpl%0~COnJh}P5>>b?-ZXl`ogU76LA5nZ!APw* -cz^#7hb&UL*wy9q-{IMt(05jrR*HLg>~YF|9DxI$G=RoNbd2>}-%tI*dhE7sHft^M0s&#d~DG2q0-DCpvFK)LlAz=zo&_@aryb~L} -+t3Xo4_puJMe%#eeC_yOCxU=rIGg(f(W=OKR;a^k~*=!6$}$36&>=5zvCt^HhEP^lPM+XHTC78MO}HV -q;)H;IQZ;0_wGwF>H7LhcK-g`o5)06tWF@?v_h?#Et1>(f`?SUP-%h89%^b2R^xxmP@O3TH7BwTT0?F -Mw7ea;SF!jHv5=qI7q+h(F}tnbfZpwg&aKR{OGBNfR8Ord%0|~Ynr8ax=BZ0F4<8@d2I0ZOm4TUywXw -^+^MNJ1Rxa4!?XZms%S#<1wY7FOv-jisp0*#r#>IZu4DWCw!mC(RFX|J+ui2zqMp{vQAXOfF^_`(O -l|pt&FA+;)Z&#jaQL` -pVS2+KK`$+(BbIfO)%S;oaLK*EnVym?IYh7aPp>B{mC3pu5(17cQXOG=` -Wl#t?*JYPYhlj{qRcK0{Il~9E)^=w!2AUWa8du@8LYg9Ll0qr{OjtQ($7)ry7gX<%HLcB85_YMMy_FR --cEeuzU6qmMiHNyTEF*fX$XbnnVbwXxHL<;6Ho#{t_02U#1+wORM`-}T0hGNsvNt2A7L&=-;Z{CaF{j -)`(hGbIYR(sdHE~vBP#RWzEEee59#4?Rr(WwXeZ8_epme=JX0OV+)rpsa52`PUNgXM7QgU4_DqhVEUi -N(cox|feVT`vZGhrh#nEi5uMte;$9q9~7&LkXV`5-4Ju0ljKK4~(zZsix8(A>tC#6i -)Etw*sm^jS|2+zx|r3CdsCLH6N)b?ZW7NBXubJ3{k#OGZLzu}!%>)hUdvrxprW8`+lzyn3=jsooqsD| -&;ekXaNC2bc1``9Uyk8uTOPQxx-N&{K>rPIHJQ_)phbU;8lLY<=n0R~C)z$q(q5oD;Gso -p*q((2kcp#x3;$BdnaQ?nFgp{^?y4-1dO7=)ZUYp_!Fj0a*tr(aAAjZyh)OCA1Ov{T40Y#z6;9afPei -K@4q*Ea4sa5bG2uoHx#^#r8adcRUvEF0bf(D -zkv$Y#D8>~YUhcw^|OrPx*oUvH^JKy0XKQth}jRLl2Y@X66h5rm?7~OaJsrU6czB`YDOZ*1I!;2OMKK -g1V-V5_<4wAO#E$g_GkPNwaJuXCH9i{%tzwTmPJ$!?jar8Kgpj{GHNIx^vn#8QYjNoWVQDNv!Z_sV9c -CXn2qo$t)n3!ay(Yh3_ON>la?KWqcOnttiqv+(?3};GN?hccRpb<}grRnz+nxf>blXyJ<)ObY}D)+$_ -v1>h-p8+AKyL**aA9}ExU@N@w>{99m+qyqf#QJ4#pn6vSW8TdnTd -@@WJ`5gf8+wXu6C28u+ay9|=y{-B(%*XI7yH2s-3+OPW9@lA75orW`z32q0cz!lmq?gQnwkK%fW#r)% -?*>}?d>E|(}ErJuP@mFy}j=)%Uns>M0cYnDj1RYQR+DBI<|HRcgaIz1Rajq{gHxIWYJ`djDqkezsTHbzlM>FD(aZ$7JojwCt -!#&17mj;G&5NK4cr+#)Hr73Ol)WweYf@{37Hkd)-`Qul%dQ`hUppq;Y;Y&JTe&4NxIMVK^ -I&~;drz`tJBcE~X^^%&%}dQ1%5-6N-(9K)hJ%$T(zuian`yKqm@+>9s7&Pn5SJPT=b$iI(*!N#}J)Rgy-Jp%AKwvS^w*6qJ4#W=JP_n{ut4{EiXvZ(qTm0rgQe)K1<@!-?4*dUxpNy -{A?(2P`X93C|ncFT)a6qU4|c66s!)Nj-6+R;EO>amtm&YPY~Xu!WfqXByf@4y4yC|i$-KciBFL6xB-@ -)S;I8zN%K?12B(z0y($%8Y(%Er8*drtxoLT#TqwHab$ifE0u=9@0Yi2bgU0`v`yt0H(@Fb~!br95?** ->F}%1^o^I{J*Rg^0u{eR$~5i%P_J}bZwEKH$0j_sulPqag+xvFU{fqyJYKI|&w1uRXX;&yWCm_2OL7Eaa(Iin=;%zE ->Ov^30eI8sm1hOlAq(R=&D9F9T4`z^%^PPa4jTV6vRF#JunDJAAZmmmXtRJ-VRh;w?)Wde;ZHEP_lWu -mZsN3NlXgqQd#5Hh|+DfdJ&GSX|T)4a%bc!fH_;%r5`m#&Y%i!=>swu`$lZX;M>d%n?Qvk|PZB0D(i% -@|DD)vSja&xvc7I*wAxr^KvtExMC#|VFKl2=PYJMzyrG<NBCOJ)qO|IJQ?_Y7|}upvBT=t|iXptj*-q -<^}8r)-ir`-z2Hbt|Nk8hIXGGh=Sk8g9a!cpqSUprpI35}6~G2%EHogUjn6Gof+Zy@+OPeP}LsS@jGw -bJ-`A9XMP}?x+9PgMXC)=IR~Q-)E)G2C>O$RJ}~4ad7+*|Bk$$6KY$|W!2OYOOYKH#T@yT$0fPKT@uy -?h|KuMvkEC2C#SjvZj4Dv6(7#xzc9WT)0@Yw<8wWa%mMkiDJ>t)$N903`^|z!_U7`ySv*Tk$2jkGS)) -A+HgaKYC#fm%Jm>2B@b{ze&Vc?%0W(h(VxJ_~*WW(-Ej(h!>vK-?Xz@JBjm-MX7G4J6KAyfkd@)mpAp -GF;9zKWFye9rDY(rA9!TkQtAe8wtWhXWPH0xf*f?O1_=tnTjeMxIWMP!LT3_)J#VZ=dXX8<@OpDD}HX -*$O3{hx03aT%(nE?*kA-9|l~mX-9^4bXud|4lY`Hs%UFGl^->c%uDB-G_3gf_wU+0QS{i@z?5byD0{? -Qbx*t2YbsUE@yg`Q3@-nsE87xjS+BI$JW*x(1b9ArCuDK75sbAD<@I`vH%J5s;Enf;KhGgZr>&qN+>% -oa#W_z7&Pi+8n0Ar$KL8nbFai#JMe*O|!y1F3GzhoQ(y1d3#`J3X74ci*gZwkgO{?wgD%2yQaI? -hlSck3u7?v#}6km_RbYLp54la%<>4zvhe#DT1veAOwU&G%>w^zKxjPOe_!ozw8{JYdQZ1-RCoWUxZkc -HJh!uo(AahIQ<>u2dZ5Z}(tJcD$B$VRYWFsYI@ePBr_r_l6@E@;RD+;`n}LAu0Z(;{>6?+*(d#+=(5z -tsZ{A3EUhC&oJ`FPkp)Fd3|J_UK6QF$nKkSzXjM*K1%vyp&j1(U|mPx%DnJY?{A^8XuKi)5x}j*D~m$ -tq)(k_~v(qUvc^&yrLr*z%Wb;hDuJfR_{Nk{pa62f8pLFe~JL%+uF)i+O{`mhEBw3|F`)ho{55f$ZeYl3q4Ihv|CQ--wqf>&n+FsE-u=k)FU=5)er_wO_(#ZhKb*HW;vpJl@2-GC^b07Xvx*m2EGZJcI3|4VK%o-n2Ux3-yPp1d&(xNKleX@=*DCeb8Gi?*1uwTU`g<-(gZ -DI3nojG_AM%5;_6^#F%P)h>@6aWAK2mt$eR#Or@-hpcY006fF001BW003}la4%nWWo~3|axZdaadl;L -baO9oVPk7yXJvCPaCv1?J#XVM4Bh=Jh~6N(4YYI%5Zv6}+ASyy#bRP2isU2Zr2h9uIc||$xCs1skL08 -1G$hU-lT_6&_~=QXOCABgNV32_G3;D-yg2qHa~fQx9R^<@B914JvFmj)m*?%sYo0QpRx>V41EV0H( -L>GncSP0HF3rR)``YKy;m5fSQN%eVUxW#IiCPb2)gJ@OqD3*<&qbTfqcT^#mFzgnMep&jt!ibgxnV0d -Mjd%iUs^UZ7m{tjSl{d1tUvEyH{)&1ZRO+EM*H4(DOb5AD53Hj|4{T!KV1qdRXB}@eg#a2KL4)%0Z>Z -=1QY-O00;p4c~(;nv^1zVApii_bpQY$0001RX>c!Jc4cm4Z*nhkWpQ<7b98erb7gaLX>V?GE^vA6J!^ -9t$C2OpD<;+l06uUjS-M0)8LFZvI_8~TM^d>&U%%$L3zBkjuez{Y7O*?hGt -=GEujw8v*G-+&x~{XbsQ3OF|LyIaUuCsgm&-QSYEzXr* -;3akRcG8ql@~whMWdQht{h})C+776Oi?Y1VbUjvS+gyzi@MrisN@!l$ -dQ(=7Dr@tp-rA2%w$^*As$8pO+GzZyyf;goHz~hflz9#_!)2Kk@2aJ)^zv1D&$^y~gT&rD0-vX&<~g{^9V$>+{*^@vD>5K0>ytMp=K|ykU|JYUy-M4>nbE_sT4psMV{t{_rTr``8eJ7>K5LuIjSh1!(k8pnh)aizxXrcR&TgP@p -5esoa5@E2)o!Nm-<_VH2?+gpp*I0x8KviC`6ey4r+S%HKzZnWQUK^|dA7LH>vXJ+uG->SFOLh}564wi -0w#}2n19i{(%#dN;SZR5yJ*_Vk?$z2^+{3dqRw!2#7x3V%X9;nuq}T)KYxG7&-VIO&$GroQ8s$kR@qo -x>Sm^ko2)8}nd}LESm>r=)*P$)s%-P+>|(n~>)JLWVAz!SmzW9|at!cqvjSR|%k7@Gth8xsV4tO17=7 -8>d!$xn1>BWyks4W{m+A_34Fb}iH)So~v6Z?=7uUCGwN$)GvWqOwnr%Nb8df_yKRiFdVSp=Fx&pRTi@ -aQ1j{zD22bpqvl`XE|18i*+Juq0hShN-FcYK?wOTc@9LZQ*?WG`Fc$7GYI4bEImr)mI8e)`>DLO5xv? -L;XUZ`)i!_ovCTf1>C3w=r{=(Hzv2q(}ntYX!(@k+G>Qvz$6?fR!)WMiY||)Z?_uGhM0XDlOa)_YFW_ -_5eMa=eVO83A?>Lpo8pp5vW5Eq~<&D1dC($00jyF0}h#bVsFI5fslQrVQp(5F9_!i@X)5p5Wmu&sA~{ -i8L`o%*Z2EG+=0xXN%0re54fMq;b7bFi6%{);jE0q#B+W_?pt^;1D)pm@{x9T ->_b9JE^Apo%io(G0hZH;~AZryXU)IIuuY!V^~2Y3ryHnG&IG;DRdVAIa_kDl-BAC1HPAE}qLVG&SKHo -?-=P$!p(y3tj(+K$X{*lK8D(?eJzY~5m^haG~8&KV(|cXjHpjp;-kMh`;-Biap4|66oLU>sSS4fn4AR0J^Lxsibg{}p-xZxM+XdXcuZ208Ae2hRb8^DhI?rW? -@SdN5HRKYgKn?f61nR62#0XZ}zM6f`LHe4E%!9qnncS{UC7Fde)|Ku8STxFej$$4@)?o%(dPeP@W^#R -G>3rqv~GN5bDnN!S&8^uZv^m_Ki_&si~K^BhRL$2_(o6QPnevnH)@+O^JCsNn03Cj$VnQN*sE6K?!%1+1|gVIs$2n8ZyjfW`a#x#aSTM<*r@wKw3kZ~`5r(J -{)-fhd)Uj3%5@66y9DW`+D*wSrL3~c3=C3bs6o7I8#lMvLVNI_mrlXpsN0Jgr&KgUJO13?H{S577Ul70kMHoYl|g{5E -Pz38BkhjbUKm3b$fY997AHC8zas^ac(1ERf^(*s -d1Zu90QTNe|UCua-tSjDQX9@)C;oG&4iK&$Vp=@&^)_Fct8NyFzUnk56`6&j70Va8Gt;c7z{~B5{WqC -qO6Fl#2{oTtr(t0zXYahNw8y9=cTI4{DygJtZtDX_PH~W!1mAjCj>mezH3PNi2CMZ!jOjp1~nK3okVu -U&fd`U9;-pyte$(lqz6h0CzQmc!Ok0UU_>O9@Q}QiLf_(tP&|Rl28@AfhA40d{b1|=#Ydh%#ywY&*|0 -QPiJ2ef>Pssek9_zgV8HnELz#tJ1J0(~%*@f>Yz`p!)pb*(Yps4W^#It%lRmtjSFOLH&o;1P*bocwlq -O)w1o!by4VYQ^Fz;4HQUaN@$I(~x0>PRy{x~=0=(1(I!dr+*wCgyjW{qN%fve1_;-a-VYG`8( -brmU6gq0xWD&xv64bbD3!)U7B8HMao0Y6pKhAQKfiX2(Rq5p#O*l_5!;%ve%!zqvyWL)8u>Ig&$Qm07 -m>Oh}G4yk?C^y^TXfJ5`NPQsp)y$t5F>C41RQx?EF-K -ik9vblsqx1rlTYC)FhuA0BrdtJAhXuID -K1jyYB7Z35e5Ib?HkCUaztj_3m-CfCK#$HGXx5WZTcpWSBc87$4f#*V|v!@)7EOQWUs%NLKhg9O8mDS -J~P=+W4k>{F&t#0jU2J|aA2Azh$_htd!BKaoy3KT+939CS~`sB?y8$4lZE=-> -b>$E$;;JV0&PRtr5)7{3HqC`-u#n;aZ0%XOL+Ns}+T?MD&dBd|vE4E$}jL}C1=zkmK)a-23AI=~Chdq -QT5DU3FX;G)nCtgwq<%k{*|hUv%t@eVOhOuSm}f))4NyHiCa+^MsNx;;reRr;dMz#8BP%P@u}!JwGnW -c~v7F9DD31s;taZ0gm4F)0qRx`q!2PaZ#i7Nd|b;vG8?PSIwjW2dCicubU5TPrljd?1BqG3&GdBH1@R -vgg55d;0A6kGq?rrLd~-E1I+eQBM1>b#d8T4Zw{xPtLa+jo;6!w5V6Q+Ba%#(3Nl)@fAN5mKqMzT|_T -gO$=aDZif7Mm)FwiibsC180Tr5Key0b!}T?%DGc}sdSE6IF^CDgqwi*py=&ZH;=b9ym8dj(3f>I`QM^ -#wC}E;rny9iKfSg&kD0e_6v|w+S8+2ODVmelquyiX57F-KDTqJK3guqpLqtX4{(h))nie?!~fc|6o&Z -bFbHmiYbd1I@DqzVr1?}9=5)E@BPNojd-Khz$>gX93EVTj5qKjRSv+cNe9#0^azg)z*H7?H<4a>QhI> -=r>3?s=d9I#jDP!^m%=b5KZl>Qk#Z9d3j7g(vc5F-PW_b7XWlnoEQO!+*3BLV!1E-f9s4H68KK`LH~_ -$o1SFG_qGk3JhIwFe3myn}%r-`vQf7@De5^My;p7&^0tc8E5ImW}Sh7vgrPe9Rt|LcEk;tN3zO8 -iGV$N54Mcj#>z$09n7MqOm?5YE2z9CzsJ3+EnVLSVXRDw%S80yHMA&f4j7)Ah#j@2f%==FhNDz40SPo -{Z#RQU6je2drX#3Zw1zRLhyiyJ{&>JDv42Y6F=w*)XCe^x3_X{K?*brWN1&*P -Svj%8l(0O{F8N%DBkzuysAqt8H(-@xWGNi|ma>aLnAI5yKs%-z3vgb0u@pak{}q;vcsM;~wBrb%_#ik -{2t*|xh~pmaoL(4`Bpz6DT_f9-&Jcknc&6l02^USq4@SDlQITT|rWg9~z=;v02#>voN0?+_PxQi-eez -?V3FVuqb#{5xkbNX+SZF06`?nO3N88KH(v5>rSZX+pGS|o#;WK+cT2r+(CA*Q7RDo_L_gtWO+U)Kiow -b5!EYly)$K%~(td(6G%GfNye)8EH&)d*)01LF{&$NXJoe-hJ0^1U1kkFkx7JMgkm?&nIRDe -3&w$m!1D`7S0v(LN_c-=`?+G3W1dqEU%ftgcsTy#~CX^M -?D;Av9YCd7i#C-J3C1S;2zWkl#iD_aFT!L^?e7r)Z81aNn?AbOdXG|{Fg7Qzy^MR+=yhKrK-j(uoTo&Icdv&?u8t1VZ$5=g1wu11BGKrbU}0roMLrus#N(ZcMmmwh`{ZGHc}Y$c_-t4D -~|~=lWV=vRnT4_4K50CngX6YlSDpFb_cZZCNb^qd~^+&|AYtp7SoH&@Y>pKX{OR -NGYCZ!2=yfNPY4;LVWs64uf5Y_8H0aPJ8~*D3yR&oD6>=)inwbL1Da`SS%rQ -h(%%qMt=nbRrd*#oznV3<-p`Q77ZU$IioYw=c!ClEN_2d;Dnt3gA6*+)y7dQDS2K>lZIxm|}w{wFKYP5>cl!X|s-z-+=Xn*;DxM -MOM&;f)_Tp`@K2kRv*86{P@!g9{pX#Ie2su`MHr=s4AQsF-vz2*wd;B*a!6QubqvMfQOyUFaxlj&G5+ -GQhOkB8H3p3Amc}r!uYQUSNurHrt~bW(lveJV8Ot-?*o)`)L*J!@H2-d@RKzc{4T-SsJ!^(r1@-?6537il$l`sBOk#8Z5JM=m4kdcV!H$4|Z+d>KW{1TwawQz=dN!OM1L -Cs_18#N!(be%ky?NoEg8$l2;){aZ}Ffbn1s)s_$4urx6Z$o3Tsh=G)VsVPKVo6KP>jUfNbhX(Q+Gz78 -`48Df>X4GlPlTx&OvECq)h`niZOvU1mnGX+C5 -s1runArY0TVhJc@J-gd88LJEj1P_975SEtg64|D3!>jg+z70r;k_lUnY>E~?Su2s>lFLV`TYMFcm8p$* -B$$!$zx}zKNY@K0^o88!P*3ewpvQ@Go@%$)cOtKJ{-d?b#0#{Nd{Z+e$H^TWmeHf$B8AzC9_Z!TK4qp5Fm?uCCJBZZ|J -DKsB%CfaWeU*3^7T*=<>CG1*+yG&E=Den6EB1A~^%F7Tj(m=ct-dlPIkCIdkqxYan0GjUtHlOini21p -#^8*kM-0htuoG3sSmf;_0wjguXLS4}BMt~%D97WVAO=-lS+-PcXFc%l(7Lx8JW-GM# -}iu{b2=2G5#t-vFz%7Gn+-~)@QfNpn|dRMFVQ%u?Q8lLHb9TSzQ-@&>!0efu&?n;_*&e@X>W;WV)HD0 -=QF7G?YXleS{-`=I|ksFW8Y_px+7hp&T?0UA#Bt^xN#cte6(uxxu!kk&513FHJ+LWy;Erx9H;wIpQ%m -9vc1UQ$Tv}rKN>RdH?su(#vITs)F8}olc1WTlNP~gMnT0!4>q{3WPtD&N#l@7x#ADvDaz8jU#)c&h>f -44u9BnfVo8e2LNj$B4vP4THY^2-$XGA2a$)RGOi6-P2&~(nV8pNK%tW53VM#`LnvVj9z5 -;KKPIp}#Ns@eDdcBTMZVcuWpHscQ6ZfXQhZW1Nu9p9%(dK_VoOnIz?YN{r^-`5>nR^ -T7vqKn%ve7rHqJ+lA>Bm*D>B0(Ug5m=j+)m~6_z)4bIJDVU36K -Coh=Xq_-tEy1SF5L9}HUOB~~La=z0cXSADJm2O}Vk|mdFvTqAiK3Ha4k-peEJ_|8U<{&)I~Mvi5k?bM -4C(jx2r=T{y)j1IJ`|ib5nPW?R0L`(5&u=2o9lS6rU<_18=aRMibVB=BaI4xRRv6TXPmJcTnwi3&_H9 -yl|79&On52E5$wW-^9fGlv}8_6Ha<+BzjB|S(y?fb$r3r={h*ksRqE|-_B4sp~#Olel|I#<2xtPnW*!QfW1a@3LmXdqDyQw>2YO}j{h|u=!x -1I9y3bxD{IUSTu;VOZ4`5n8RjlaoL`17}amdQ=jJCQ01zP88@au>~fF0%u9)`0Z9E6UX?8tfOry_bE~ -A>^3MkdC7(RW;1_l;RQN)?uHeWBnBrc=!JNy!1NCqI2uUJ|D8mjq_5o -+*pzi3ioA%AjiKC27fKKNpQ6kE^?<_t5dV8Qua}D`q0VO!3gdcqP%m$_yIAW!e){kacKY8${11J!?;| -+6UB8m#hv!E>{)6cMi-?W{c^KK3I@e?>KU9yuI(~h8e*FJLllNkmF>r@oCeI=`pzp=1Ak|)19UTJp)C -oP*NynI&^}m^stFkcQ$sp!@aiySStGInWMlJKEXq1SNw@MZ>eg4OA*rr-uPHFUJKemz#W_l4 -b*2$oA0N-w04WPM|XhpXRN4LdMxX=6ogIpZS*9aSck$DU*x)6!3?zqo^v3Z=*i89cI!(1zq85^9%`fK -`w=6&K{D=%q95!y -R35UyII#r_Sj^>X(RB!qOoF9IRSt`q>*_EI1>^l(g+)nBCOf7n!w|IWm<`$* --e4;Fd(dqGd^f-yHw8QpiUAPyCQ8r+zHX;tiIJq7`e=*KlOo)i>{5{xC==|BjA0xb9=khR;#(-IJ_Q> -FVD0^Z9|_4mwgL1fkAve2t-P*5iTT-8yIS!D+4KuR6_smc#0`}OEg&5OeDu;4cnH|DVyVNo{SVEhA0x -ZxM0OC@^Sc~?(qh**7=ysED{dcX;u$S3-y$+7WK-kA@(_ -SA(szJgYbj6YH3bd1VuPj3}Z+b8PC0eBBH`N?RzAed& -P$ubNvVY(xNN!s2Q*gs?-j;pE5PZ1_7mnj1fB8-IoF4`JI^y13 -@&8rxvH(eZZ@n0;xZn}jHTx4NyJ_tw?-ROOZHP@AtE5{r|2`@qkhv7rJ61@DStQxLXU+Q|HS>=>iErO -X*+!rrhzVwsH{Iv#4{Ky?XIUmbK81$BGYx=n-`^!t$9W0qkp07vhd-}j2N1nau{q~X%HAHJTFZ}f-V> -4TNpj`(qf}$1JyI7X5yLBt{7!U5T4ts)|96tz^@S|L#$v?LS-|fGqdc5)n*)_&OYZ%q_fG7~o^M? -JyG{{hG1vnI!ca$?r|orf}UB{L{bgQuw&AgeD-B@%vmE&z1@GOA=TI5Dc@BmXnEkk~~fx_g@=?H8B@j -7hVTdXIO?JzbnC=X}e`LM`7q!EBun29luhMsJHr-?o%*#ZQ*sHW+{KQLel(Hs;S?1S)6!F2)nPTt1jc -|b54`_0TTR;3$<$Lr;sqgLJ(o(u>cN`E3|j_7W^krO9KQH000080Q-4XQ$5SydW{AE09+6N03-ka0B~ -t=FJE?LZe(wAFLGsZb!BsOb1!prVRUtKUt@1%WpgfYd3{&Ga@#f#z4I$D$#^Iwwqnb&<9H^ywCS|dPT -Og6i!%^JLK0#UU;xmz+UZyH&`ZCrU(&@cNI-H@9c*IvVX@dNaGPTm1Yu5Yq$yJaPg?;kmN -%4Lw*>VY5>6S}oR-a_$SuNK}OUDFjMc2VRwzj4P8*OVe)1n34+F17e`}e;x=Y{W`bcHv}Z>6$K_pP+5 -5)DJU$4pd}bWW`r$)o~|Wde+QZ`G>zV9TJOR+U#q3%e_nm#v!>#oGtbc8$EgE(!*}H*elB)po{uX65H -zI`2mLAeiBrA4;$QKrqIwtd)grl^+p&e){q&lV)Cu&Un-=&aS>{`o_G7w&tg&Me)wGl6K0r;!d)*Z@d -DpDNwL`U9*+(Th@1-v?n-j%sqTo!bx@~t%Get6xIdACWcp7$wU)i^^AQ70g@Do;~`r!bGuxo(Jw!S5t -uHLUhE*HU<91AR=Uo>!jz;ubLe{5$*c$kQDCZP&fC87Jxm)S{*y7cefKRSsVF#Y_ci;267!V(prIgIf -eCD8C_Fnoy=IfLGTJNM%kp@79OY+0?(UVZwoqKWv&NgsG%kb80|XRUIC_(5&e7;rR=1r|+9%B%=k!)_ -Zwzltn8nJNJiaH7Q|9EMxedS&ukarN#z{h7NxCtn4&OJS0O0?>|NNCK;hrihg_rf{)13AZ>c88U<1Vz -(T=afxtb$aiJINJ>r77dQ--P -SSA;qbPZVF&?*^89+V#LOlRiLu~=IL;SpqMFPU$ONco%2q39>AdZy;D#SIJ((HV8I^^Y>M!_2Ft+mEd -ViuH`m8f|lP5wJf>@U-3)onPWEuUct(N`Cwtfv+ORt{Y -V)0$7}3{5M8tZk}Pt8>u&bn>jYBBj-x6Rx9*=EmSl3p2XwgXXm+>VH5^4o{ -MAH!e(z;Qe@EH5P+gMS{z1okmguHrGZ2L#$3*|P)S`VcIq_!KPgpFqqj@WYy>J~bTv>XVzsa;-ltb8O -JW-f-NN>?T)V?WLiuc*R2P%yCfmWs(I_FY^M`%&=uD!_DY?j48D3VDm&*5Z{1+wFmHv@?+89V8614ph -e*xdAYAPY3P=1N(wu@++bSj<*!DT>%l8$VY(N)Ock50y@OWHv%OQvrQtXM6Bg|xt{l}^!&~n=9(|WBc47T&oj5BV#AeLUi_55DdK)#J?6%gh_C1p(>jJk1oV({Rq -eDL95X5k|y*V5k`d;5PaCx;x4_uI~&MKN6J(@lR89wV%8kC<*uhF6K7!2BD2AtG -{H-lybpW#z(zztYL1rWiLGW_6*8s3JJ_;^IfF`A$5;nbQzbVv{H6}Yp8yVQ0vDXAP^l+)?H<*!+v;md -3?9ah0fVUx9~5SZDa=hRXY8oHW=Aeeno#7>a9 -n%Sm=!#qIR^(gM^XHSl_$gJbGW1{{T=+0|XQR000O8`*~JV2#vMhd;|ahy$b*UA^-pYaA|NaUv_0~WN&gWa%FLKWpi|MFLQKqbz^jO -a%FQaaCwzh+iu%N5Pj!Y3~U6X42nhH3ZpJyI4K$+NsGck9vnklkt1p2C70e^S`~`++dH$%t8BTg9^#O -D=62@HEYD$iv4ldGvff@o>o&_D_)Qd@ot;GnA6vA}X4aXuaIHZb{r&tGMQ?=@FoMACgo8%!(ZKJ$5AQ -MB7+p~~rLb^P*A`^eXyf2lQ=-B0tt?yz$_iaILqX`J9O01XC0XS8QppDdz5Yh|Wsx^{Uo{FVtepj+6jbPTx}^WtCFll?2zBU<2j}@bX -r6{?Sl&Sc22AM&_LY*$i%X -I5q(cl^M2!=5fr~s(ySg}h!p{LyO&`p7I64_X-oai{35Bk -mkaeJcB%iG__=W0zLX!ds#vG_5@2fMh-iae6@qx6Vm`MjOF9TD)P+7v&i;7rGF?aEGQDEcZdqT(5FC` -1#&F{WhUB`2%xVd@v+bu(1!UCMbt!Z(vQPR4E3>S=_1nO}f__H?DOu_nMB6lcg2<%Gr -D-_(c?b%kB!k$r;pDTnDTPCp09Wz_60H)R#I4+pmnBAT@Yf_UH$J1!U8q;x!UR -QUsDfTbHHFp^(N%(XUcbX*49goS7CsC1xu1?ZBhtJeNKefyVE5G!!yCEKw)(>q%46R!i`s9&(kCdkAa` -0D5%s)E>F%n(e$Gt?Y%#6_3c0GV-uk3;7)iOWo?qsfuBzfcl?n!@}}?tlL9#Oe-i2RJHwG=v=ypR^lp -Y%C`eaN}3e?9xz&V;cE}4h@|4YvI>;IizuP7>M8q&4CvLe>X|f1!sQvx<4V9mrDIt1sZHl%bd1yGM3T -jxEQ{aKKPB{12Js;EaF$RnuR%ZTc!UFdTq1vb>TPb@jmtU$dx}I4_ey)$(TUup^I$r`QNLIf9!Jn8o# -k$T8MdaUX3 -U1)PgZ^B^q9ccU3Nx`=pRr^0|XQR000O8`*~JVR?BT%7bgG!qIv)T9RL6TaA|NaUv_0~WN&gWa%FLKW -pi|MFLiWjY;!JfdDT66f7`~f|Lariqv`^5DCn@`x{T@6k>y0KW4k(%*UByo5laac2rvMsBYEj(e{=0( -agcKQuHVzIjY(j4c6N4luALojs;!4xYMRGMb}?4VYPR(hpKNRl27`@4|FqO)RR+r{Nz09YHvTp?PA-# -D&GUGfYPBfx>m=5t!lp@er)JA+S|xc_s_HVTR8(lSEOo4IlIk+a)RBPRuy3QZ%(2OAnW|Y{D1BS$B8y -T$E-RPw1p*5Qt&L>9$cqZG*3+{7mQ~OvP0r1eBsZUO+p5s!Q<+b%bY;I)#d2DIyR#iCJzva{)ONdxiV -SFF-~XKO>sgV{0f#izGHrSPwup71Tpdh~i^K7U7q9jRBqr{ -BY#`H@+8q)xL!-y~Taou~SnJia?9io6(ZY`i%*Ioc0j9lky|RukoWL8hx>>W!2a@+WBX^5E5r)7K~A{ -)_ON(>E_)AB68-oO}n(iT#2bgf)^!_OsrdHnd%uN%O#2e8$F4@oHCc*L{F -u~iQ1pi5m`qpZ=_Nxm#qtkYzk06(g8eHp>CBCzAV*xx^RcM=}F*?;@;@Xfa{@?fL}!KXaQm{5fd$^~$ -?Ukpa?%ZKpR-FbdAMf$?JvTU536iYok8}Q>T^LX%KxB-*gNM&WX3{Yz{V-={L8$ohCCAf!K3;M{sDT-(ap#7O2 -k?Myl~NYtJ6}iY#(w5+{3M|%$Vws+7U-JffO1zJX5bg-s_DuD3hGg)v>NBl((>eJW%jbDBU4i_>PcMIbd-QUoGGGPRfIpM)tr}n!4v+~>)=cNGOI_Uk5T#3faO<~;(4E@ -)KhMF{8=mca=$*@3V8Q}<6E<6v&T|ya=}mM8-;{{+4(hg#U6%h=#0~0Z7(%|yVsDS{V=wa%_&>|52GL -=q>?1SE=P>Ffi3y<0^iof+)HP06kvb!}TwQ8qwxt5L0=9u3sKsTImcGyfhLGgDsR(fR;5q(!%lvqC7E8}E4t`X>LSh0Rf(o-HvJ3~zGg52r3D5)7j0UBj-n;-5zdGxF~Wz3!!AMkh|y)?whj^1!HfS ->WP5_0eN+P+(Za#{6JM5>M(QyP9S8OP*hls$c?=+p2=9xiLW3Z#H%Y@_LiM@O(`1(DxE2-2QV!1rE^_ -YH?l;5yVefWcWz8`mY7x^>etrF$3K;AFBrh6tAsEDlFZ@_Au`~t2nki#QTmGSrpn@XUUdu%sRXQMVM` -n2$l~Gj{ehg+2*gSJkOf`Zr@LC+uV>G%Pd}G0?TrR-8O?Bqy(yz)ZFyrCLMRf_?-@rDf=TlaQ;UY()6 -AY7y+Ua1CqcMn`!_dW}1(S#4&S0xGyTi*hUGo4wuIPylSC)J0EFNw`aQLI|tSzgLBo9u(++45&1Xe{6 -;dGmDaGBRIMH}fdn3Yvj`8?(jEc&oaesIeqIwIUCFs`s38^2F$V?%p~ZtT%Zg+Pl&C+S=T{Z$av>g)aC)U=*kx3^al9B_j`r$z_|3dRP|(x@&a -zpsx6@uX$!=5&<{!i4?rL!69SM4ATfD -XE6w{1WN_IkxQLf_Gm#v`xgA`C{6Fsv_*{r@|UIzM#8{UOx-@GHMUsQm1b)Py?UANe9vbFp5286z?NzoaJQ)OozE@_yh$=ssTI*gwAT4zd -XQ}KJ2EwVQ+(u-Vt71H!jA=aBv~8vIK@_vT0XzUqnTf5Caw{DG==~Vz~VL6Nrj(-5fg03Cz0Foz!P;t -`+mIRsH&v?(Zf>$2_`4iZ|M9b=c7uOklrpTq%r})uq2IQZRC(q|~8CyvqsMuU#VwcbXNL)p7xRgaAmp -MCu~B21x+G*yf}GYJ-Z$qpeSBTP(o^kz67H&K4wWi%8Urd;2{`{(vAzj^^ -ikI;S?(kx~u@SJFzaAjj#)Uz|O8Tv9(Z}6XY80w_u-)N1#h-$O1;l|OREeu;$Pv5qLwoFO{9O&BxY<$ -?>1b<~ZKoK3 -{@Cv$zA5p9Ohwv7MCVQt$2TK6$Ku-vi2Yy8t6$d5!_?7w5QT4}|zSG%!3}THV`Ce2`lf;RgkYhXn`UEmrj<1-06uw$*GqL!C{`9Y3ugW -k$i3UP5fsF{Fu;r!7RJeQJP7)s;4O`%y3)tA(Ja@<|Mt;ZdcWaxc8-{0QfL6UH -3k8ZN8RRT5bCs8rDT`CZf|L^ae;LC=&4tbTP`34?%GIky14-14-W&%p2{2Dg9}QQH(3#Ri-;>jb#Rwa -|vj|l1-BeO~mqwPaa4a@xW!r9CI~QPgDlq^2)=;TrAW2<@e8B|3X;Ih)RS@GsXWH0|H!~0u1}`=LDRDm^M!7)An&}29CjIfIm -uGvf8j6yUuvd!iGOk1#Pn#=p1z8)Lf$=LwZGc)=TiBVtNUL<85VPwrr8j(DM^~fO)3*;?B^ziV7g<2w -7K~;52cfOcC42DN#(g0ZVd#P<#NhK3&b>%#}HfoNEf_T3il -OFpR4hCHt3nLYPxdDnt?6aN;ki!5NRFVWxG7${{1t@Uya;_!TSJt3JE8%Cuvvxhi{dNJ#=aFer8zU@> -iAdBEv`A|EGJED#o2K&GA*&r=0sN8Nb_Os#YzK>ox9#!C;~94~IDV*oY{Wov34){y9%RqMD1cbA1ml0>;GkJzX*Bxt)0 -7COmb+Sw#ALmgPUBKo?dFo;8l7(3&U8rU*aLKMY1B -OnXOy=Xw9vE~Nkxtpg&HZ{B167Mu}v`Q1ByVMvbiKZaNBizVN7mhm@wik;N(M%6i9$z#NuTF>O?qvx3@(Ri}D`yexccnc{aa(?v$CL1tv3%8yRTA2}WA->2=5RlNGB&15I~{dX1D%c~_Eyb)M%b -Wl;@X9YS_T> -^QTqSLuf1cNjKF#!@-n%~w{LWD^|M}HDBtGQIu7wLpv}=quWWA*a>Q -wsxjH%D#k4*Yf8_IuVXODP?mYY$4$>Y&hRH*qf -tr84kSIHca0ZBCli$Iy|Pt2-LN}C30k}gijZ6c1b+XM5qpnp`vYCMIFT8=*=sXvM^!p-0146n+sbT(2 -0p&&sUh%`9kwx2x`O;c{{BeU(H_1Sor3|RWSwL7LS-k_D_L~ti2XIVgo1k -$ITyJPFr)VKw71{za~hJDYk|BFI`(pK&l5}@WlX^e!|7Ew}sj^MBXAK0~E?vvMql%)_0$KXlPIt+>_E -nUpKS)NY@#NxB0yc}SpshAA#;8&-q;a1Xau*`lu#O)9p)5j)9VS|$LK75-tBaH5Q01^3IV0uGP9XKpL -vjd|pWhqbx*fFlcWZ@Gipc9%4=Jfs}>Zk4&Y|IYzv1B1IJII87>3K6Ih4<^{Z$KRBe`W(B0|V?*G+@&_xL2t2T~d3s|+&OjN36IW}gL@{celr;Jh*7s6fLF+L6h(O|eobIoU~!k -8GBNd+~^j$KxqUvXaKHzgh|(WN0t=O8$oEre_%oqBnf(ik0d(Gw5~1PulS7Voz|Q8V*8?KIZAyWDjk7 -H8I-72m;`CAWNL0qx4%UCRr;F<=ws=>#J$V0vzloh~4pN9SOwPy$VkLd&5g -%Q{%P!vnc}70nlE`$!G|^Z5#=8PT|d{qcK{j_*Ng2GwmfF!3v7nSN`R+1ouhq*F?XSl4G1Ci59=nSyu -#Q)DrY1qj=Ky#pmur*HNp#Epk$o#;AaVY0lKd11<@Cd+q_6lE-dM>NbOVKKLeLFc -J=hrxPE24e9Q~tn5QzAuMkRee>W767$?}4G%~9ptB3dvZa5gvlFDg+#-9TtqvflR!6A&(N5~V!acFXe -T>_2Vwtdv9r!6GvBi}k|0hz#2)kRGuWXhn?p+;e>F!&+{AdgczAc&uxMRG}PA2;dUAW7pwobQYabok_ -^3YoiCrQs>cqV(n8E}?9DCKDfdN^7xWT14V^V~!QDq2q8=6JGOZZdIbJcbL@~ew5p}dM533&p^Eb4y< -z*ym^l1X2z*Pu*%?pgM=YZQJZM|bXl5vWqdNexwrS^NuYdGwhkF^|>(V -@e6IuPpHOU~lJX56pi&d;c}O0jTz_Ul+$_WX7+j8{mm3SRj`D)E5m&YNv8vLq@LC$}qkd!19=`sTxC= -|LO9*VJB=$nuWMqS2it&cY<8IbvFP%8oQUjtU=Ftb`pG%6Rzekj_8(94^zU9-V_29SHKiF@gG_jliV4 -P7)VKZtk??-FuXLIEd1ffJOZ|x4QfHp1V}RJ?&e(JiwXa026go;y=b -aY$5ED!qwZxTSa5wC=CcsQ0~pwL2;>BRFd1~RecByBhl8)tXeEOiSZCu_A49b!IlB2h4cyP#1An1_)} -H~%S&1{iDgZ+Y*ml_K%JeO_t1Mly$x_o?*p+`jQ~rN$MDRBDoktvYlRX5L%x~`G?!dNpHdO&=-3g>$J9r0A2i`itHw>QBCrhBIqGnOBBp -cSIW)Fu;cqSg;@Ab5s8~J&IT1)uP;fp<<+WeqF!Uu0+Y^&t1T&e%d{?0lmEsz&q%ky;HA&4ILbJH;L% -z7)SOwzTL&ctCQOY;+H+leBAiPkBTUY{H>C!jl9_XuNrq|1JNV5PJ=^6;cJviw|tVCnwE%K65-eqhTm -{~D&vl`EzL5CW}#w)%jC)sjrMpA+j4)e@?*PbFjK7)2+Z2Lt6Y<<>8?N_+NaY&-~a*Bl%rrn90^3nHG@%Y%B}&!l2n4UiH;>qx*OPSDFs$)iAfFi0yV?aNJ0ax597GwLpz9 -#8#Ip?+Yz4MX1(_k?*F6lULnD2XM~SH=*=c9#@#(DU}e>0jdCsmqn@plGY!{VB@?*|%LD*q;F0avNi9 -C*1?jm~(3+t+NU0&oY+o5xk{hxvLhF$kseUObsGR_{T6me*t${`1TRhBLX6A%XogN*!K`$S5+oL}|eK -OoQeRFhhd~o!`!OQUY-~?z7Y{_x|!|LyP5`4sm4# -bxs8DxGKf;-{jlme)78cRvq6gba3e_Z~g|^2r~b{&6b^|2Z%YhzSzc*!jyMsinkewx!EyL_WYB#+Wfv -z&tWhG&+5aXj*kuC=1Yfl003xF95sa8g(UyJ*nYH!6wreQ7XBc+188W{li1{*X6L@9FMNr>v(`&(q)F -kxEv20&6Xgh1t#D6yCagp)WOXbxMah)D$kM+XFKD)THpeAV~@)rr?MfEmqAiSX||lVuci0irNOHJ)5` -+OX!w^Swfkh)dxyr!v*>`}T!y+DkUx;mtJY?$ya%N>P!H5)>+%~+XNu%#@?3XFLkvV7ILXpxZ~UQ&eQ -#kIr#GyG(|==izx92L*(SNSynw>aD9(wUi=@fNN18JTob-5LS9{iuH>~AkjMDIR*=z{kihOy2pXY+ve{c{ND4255E{~kH4CH{im}J -|7Xw{I@^?q?D{G$~5!JxZ9Z>~CB^G(4gDv?&^_z6~)4tN|$d{m4{ALrVDig7L`FNK&52Q&%#RBc9>Mc; -vTaiA@V942A8gb$Cv?H5{3BRosvdugbYMLA4@jQ^;YDVI8Y3@WM%w;SC==fbf&W2(QsN8F!6M0& -*LRTHWPKx-e^s%fR+Hd`E{WXmQX22>l`l1A_ly8^Ey -q>|vfliqHcP`R4VgdXP=W_wF7QX>d^IBX0`)c#C}f+O`;zc=_af}%nI`;xSuZ^o$~n^U1Cj`Yg%&_1V -$lw72T4A1S`Tg6s0vdiu5ZQ%UB#VvT%0XqUC?mez~(X9U7mtO|v|2ykNt1QXR?M(jEKa{_#o;_2K_SB -Z*d2Xp)ml7`VvMP^!p)k|;`Ju-vZDYF$?5D-`<4`u2czf`yrpJ2LYslx$($(NxNhi&8uTff8&vL5@S! -_v0hSMZ23~*WH7Q#PhD%n8?$-MLBY7cfbi;0(w^N{NWT}MTN^#FqV_S#G}2L>a0nel!>uG?MXRLDNUJ -<)>UTq)V@UR+#dZ1tB3GaUF#?V`4#&(DEp{-s)YtCq9}@trQ>{VUzO+D!}V9%i`b5=lwUAl<45N9#It -(rKK4n6BE@pez$8&vf%}$9tUh#AG^-+&Ykwt3pAMkv&AG%VfD^pElbndVw`yc>mmwfyw!t89Yp&XC&G -dZ_pX`q!yvv-7`^cw|0PG;zmQ~#zz{5CiLE_O<)kJSGjI*W?nWU>kW1y8nKsyLA2C)wHhCJTcfqI?nk -WCUW7iw{$DWK@*=6Q6cHU|P? -l~V|CAg_TrL*eNcsaaUunId%w=Qhn+m4OLT(}vsu;mW^f>Jt3xL2hZ-5UZa|#14EIB7CjppZZq^`zom -H`EY7p7Ye1HuZk9!>0nZgjGkHl(tG856X)(1N)HfwgV8vB_5g)AhB$y3t^$B6e&BQ?`oOw-zMk^6@Op -qcAEkie$c;P2wWaD!h@~*;`8Zo_Q*!6CH)b{bdUhNq&t3yjRb6(%OS;;=D>J^CNXJDWBYQkqaMOk7|2 -+8wV;oRyGUG)|6UKhOzmZi(u<8lJ)MklM5`SLDSDLm2phjKDn0-j3rr{S==aig%{6*KGHVJeNmwUjF8 -X=M|sy7K~%Zd8ydAV0-cQYgz~cXgoX`G**1V$Yto9C&@nCXOR{hg=!%r2ZLOGYD@x%cy$((bn~}N)N_ -)}U@2H$zo9!kO3wBk-qeFKqEa;fZ6hFa3yA&@&fKLS?jPIgL{V3!CvbXGuZgmqLfS5=E8URRmhfaNad --P4RE8B63;&5ZFj!Ap!Z7n0Ov2m9YEnr_vls9(D<~V&@F|0n0?PZu2b|5t45(nhU8%n^`IJ6q9TD%jM -kDTVp+ZppP`Qp8U6GakiRWZC^yaFJ)%!`EtV5|n?a%DJDaPN{)0}=(UsaA{6QA02{?oGG3SMJYohC3b -EV)+MKlTGdlQyk3z=@_}9j3>?Vq-{~qHg0_zS+`E>MU?2Cf+7~aV{OBF^W%=uTT`fMqdQ3mM6Z4eWI| -;!`pgiMET7LB7K1=(*&dw~`Q)?gpGnIQa%LfEFuituT}IV6o@uG^=L|%#(iqahwct~_S1A==XHvA48l -ybr1AWJ*E=ZvQZ_v@~mx*ip32oR+k -h1~TVvq$e@7I_NPN`U2RJ6BjFGh3JcngrKXc3>sI2j~!p -vX499gL(C^}-`#w-bKp?}%xi{h@UNq}SuvR0GiYAta5nBq-4jUbfI%;!w&G|h@%KZZ9k2`hSElVfdrpKI{@Sc#*h=M -e5=%eh1h$*HSzrG8&1bQ#3VmialnmLobRX6)Y=a~=nE}7FFObvvZfvSIOU!G))A6s|;!lN^JA)wNFhe -I(Is%K0uJ@a=VIijBh0AKT5oQPlFO1yo?08bU&sM@iv(To3KxaOYckA~tfIUo4BuEd{9zS~IJbKiqYi -7pwU4KVZYqI0Cur3Nk7RFSv1&dczzayFfYDKjHQ*G5mW?7nG}*>5*DI5vF -8{^Hw@WeFH(MpW4S53fBDJG}%=+f-_aj!4q;uFtF8Th|MB&uW#*aT(15hqzVH!GT@In9Z^yJl}z3}zH -NOc~5bI`Q|DV6cjo~Ia3Xl$5f-cb_QQ>RXf*(>~~LQ{quCB_ -7q-wtkAJ$(JkV`?>ws&MILoBK$QIejaP~ -X|$~H%tr6CP6lvtLFUhJR9HD+i5mgEBE4+t` -Iai^WPi6^d9?`N6e)%470VHr5dKIxtU6Xxm}@!-fD8cC{v@17wyFJ3l_HMn=$dh0QfE!?Q6i;`^B8ur -A@R(F7P>$17|0kWu)A9QLvRm6&J$|m1UsvgDqkri}lNc5` -q4z=6Iw2oXZE}g(j?y4bA$0ki`5Aq~;bSKdm>^cffw+KwK=7HL1ZY|Ru8iYUdu9})mQ3w{m -gvJs*Z-jUvnmfy=A;bN3X9?6e@T^y8k`utRETr>C-S(@YcO!B-@gfEJ@NR6VDtvJE@2F=}$$b|Gu9CexMHf{knGQVB -YDqLOncak#Ng{(UOi3ax9D_Xx9yDmUthr17E_jp5!qmRsw9rlWh-rsTc2QM;HEPYBpy%F<|CvaO1bMn -tDt>3=QtqkBvGT4O~^!>oaTD(sA7Z@o3fv+tdy}@6aWAK2mt$eR#Tgz-!<0&000>R001HY00 -3}la4%nWWo~3|axZdab8l>RWo&6;FJE72ZfSI1UoLQYZIC|?f-n%p_kN0oNfSaGT^#&3xEbPROjF=Yn -v@>iRX)E}5U|*-@4dU+`zfWZRZ4E;RmkuXrCK01=#)y*PTCgiNtgai*qRC`)^lLA?WpfGLk#bR$D%@j3M`r>@0+DqpYLHu;T&VsYdXJ3XCLEX4~@O9K -QH000080Q-4XQy*o!4>Sh=0Pq$703!eZ0B~t=FJE?LZe(wAFLGsbZ)|pDY-wUIaB^>UX=G(`b1ras)m -Y1p+cp&4=PQWW#4~XG0aFA6qfOdbG$@*&Sz44t*{mf|C8=@z_dS=C_>k=)lU)Y`!5W&v?3;w%SqSX+O)9LObQsZr+65Uk=hR{EtS -|Wc#dtDNStl8+JMfSPt_~Dt&8$G;>g6TeAay5#*!P%nYLPst2yEVV%%>QEyV~!*|W45$uz)&)v{E$)f -(CwbC&>dtrBj7LCxNuf!@Urf7v<(c$>ag?qG8^(HLAK#+iNurOf`(_{pj+JZHeKJ$T07c;6Ji$R(#w{N?}uZWBKpSQz`P -NDD;+8qNb%yJNrkyet4=P_&!HlG7wzu8D4`ca+0i5-`BMl><(N_nda3{J_X!95aKA=VtiA510*yNZ_w -Pv*}(GtiIj3wiz4oEz1C0w|Xa?*teFV)zrtX+Jjk!aXzu0jI9`$czU69-8yichK+kWGUL@+y)CCzP`3 -+S}k+IaKf}32-@zNlP_4)6_-M8t3Egr_b4M*GaN!k*Y?q^UyT!vjQcf;vRBC)u|2*nt4|DJ%-ihN1}49 -25zc#>OouN7>qUC=}8{<=2dTMJ00YN44pYscBe;DV>bu<+Q%!1o*j<8?kYVms%p>!QdlJwGRxz2jI(y -_aAv{(0%tm%+^3#>fJP}CqkP}$rMIi8>((Vnau^5v#XDg#bD;O$>lL-E>TaokWeD-;NAZ#juImR%OK< -0{G9xyV9b&-o07nEqX~iee#VkXTTI=YlekudEz$2QgkX`^g=4<|E>_`HX2ycTyoP@DdBzfFF131VrfC -?t-nMOXAr~7TDAW&>UGa_P3Gr@XY)h)WJbO)5VfC4`;(U{)FPs#Xtm7@S=OG=%AK}nnKYw0>;G>y+2) -M2e=!Hd}ktht3cUbXG_3c!sEUD|csJs-)F=uyl#vVv`4KWBO_`)3t-?>HO{$rq?Ff45f)~V1P~NB(2P+hGMh&(OSKa(@ -P2y$cJl4H9OVQTm2F2PEmI0otnJ(X4=7f^Ae37rSc|^IK5R;T0J|$K-euD(@?__)A;wtDu-@I?0ZXv?2w=%(UIB6Q*L=ae5ew$8QQb7HE*lJ>|L|};alKz~sxSN~E;5!7r!^8IfdiwB<1jtM -EK4B(99f4Y&>ar>4kl%v#}wyVE7qQOrm&hcD7XHQN`!i14Rj!Mhi)8TVW){VUCAuqah|!cVLYCLFoct -h)UTGDz105+hQ>f|2H^VhL-g-f&zlZIDRP^l@d@6aWAK2mt$eR#VFXU)|~f002}4001KZ003}la4%nWWo~3|axZdab8l>RWo&6;FL -GsYZ*p{Ha&s^TXk;cW8M1%EPHLjJK2g2gZd8@H#{2TBLQO7!+5p#otw -ptQ6U6A#BWQEWR&Ofp~CTG7-92y#XA$#_SsQjh)12#EKg--GX+9?)AdPRsAVDF~|r4H4wqQfrb}&7sf -8Wx$b%^eVxBYT%cyO7=Ayar$m9+YC>)ZmiAJjHLcgwwF$fDeEgftYr!HH<^ZMwW_#;qHZ!dt! -$<0GuzcViltDh<0>pC@aY47(iNPbU^ZM-_6`;eDPs60E9#U03!eZ0B~t=FJE?LZe(wAFLGsbZ)|pDY-wUIa%FRGY<6XGb1raswLI -I7+sKvg`idI)!IB|!Gn;*CK*4&)lc15!?!YmS%rqJjC6-#1rbsS}Y*}mg-}jtT_Y2kCvlBiTYO?Cosd -K+nS)97IAK1RFYrY>u+sK(vuiB3H<iop>|cHix!9m53Kx`$!f|dE(VDb~Rtj -7(Da*toe%F&3pF={+@SbkH79`OIU1Qmr^pbg&)7{d57~B_Sf=8!Jp4Ruw5xRQ!`)%R@OD+W}IYwDI~A -h?!tznknGe}W6g^hP5;pi0}_fSZ=8ZBkL_4j1aimOv23au#)u|>#Xd9_0=FA?yBf&HI-ENkIqUTf3?K -k9^t=SZvr@7WW;_&TBi^H#C5C$NyUj2rB{WdR@Xe{g$q6tVv@sCzCi=KD=qF)kwdnA8|O -hIz$$~o^BghR0=u3kRO1Tq5@95imqnTH49@QnsUV(An4T`fsdi -xnxZ{FbUc!50^;}g=nt+x!n7~OjxwLe}KVpCg2k+z$MI7CaFEJu9Z^kYOR1Hy@sD-4T1-sOVH5$WBFk -0z-QFvV_R3KwLRyo9EM)(#(~QlHR2uH3TMBzOrZ8U%ZEU>d0V%IwL~uqy`vw?{gK`%5Z<;?Bh;#BLWW -k`A+zfh13IHo(SRJ!hGRJ}?FWq6u}}+#{=PkRfYMIXVh~(rH-VP!v1!i@^8fwNzCXA9qgzYr#Q{4ovG -rJSM(%|qzCs^@3^hx-Y3W+H*%EXCwu)<7hPgQ)!PSV?Nv}x@;qJypBO4H`_l*mZN^K+AwqeBWV2pCZ( -L#f1=BIABthDoB=Ob@uJ*Q;2iN*;B5ln;AZf2V|uv@kOxj!ETFn^EkN(=<7K9?7%)?I2@ED9t&zj5mN -+4&>*AsR3&sI#Q74uY -Ll~UTbjDew$zCn}WAQ8kC-e2*sXfYZbz4P~gMkr?qJ)v29HY+q`3p9%>_9V(R{#sgV2=iA=MJ~%=3fV -gM(OSLShL(w8cBbxYxR^zD>GqbG&%Cu?h{88Eh@i-!6+>1VmbjN&0lLknLU1P+mD=gFW+F_j!m`M!@t9oU -23q&7X8^Fs@@mC*ZT{Qe&Zp`Bw82bd#Oehv%d~HWXb0RbNY^&c2dcPv9AyfgHy)b<#D43G|$OS;{6HU -VXS>$ug3V^U7$aa;-N92ciZsy?ctz@DkrK!0N(_Gk$n{VN8DyV5HuUstChg-?zxo8JDU)K+hC(Sz}C) -XU%kMxnp6(B$}vgfeQZHD9njSbi$3+pCp|CvL_9*dySrD5|2x0{2^lUH6&eyN`Osf(0;{IA-iLbk{`- -47Mtja%@K0YzaHHwyBiRSzc9a#E6P&qcb~fE8kDG>{H(9KJgg?OmU?B{#@&}9pHbV+~(OkEjccaci<# -D4_?i1NH5g{TE8}O|b6u1_OGL(#axY*y6hOVcHBeh1-*%V9G7PGXs;H6Xuk6rRn -bef{?cL42A8Y62rT}~(;#kmMKAXy_+6?W+U=ik?<;r)i)=9C0|UrsyOlC*j9T8RKYXNOs9TzxZSWjZ}9!MaBws?q11 -1JKL*Pn1p{FpukbAT$&BeO_+ouUy8I|Pkt;9wnwVvjIv>6)a;ke>I(>ZZUIEn;VPErH1RUBwn>DPX3g -tpedLj&V`L&;|JR>p5o1BpZ_F$335#C{!^LrCzy-fATh~u3Tq5Fvf4~XQZ>-m8LG51`XV2rlXSl!r#X -v&mjKp?SAFrHGb2^e_<6l|*@e=9gTIr-bc~CD@2q0>Sua>WAp6rN&?IYO}k90f1L>SbLae@@-5 -*u9X$DbVPL+JA_^(p^RDABc4rG);T%Rk=QIi%gee&sD>3sBMfQ?7G~k?GPABDR~&X-4Y)j5F*nRSKcW -LMK6L|vN13v;%-a>;UT0{brN#|6+3($bKp$7g4A?hiOcUwNaV5{y3@to)&f8~97I|ht+Klm{yvSH_JG -2Fp+KhY_e0jruIC4lVRibgOFYkfkO;C$3_rpkqJ*>gXLS-|mW*LFfbwp}AL4E>ZaD#TJW_!`^#}n`cE -WO08-k_RSTvZ{x`lSJNC?N&9c)93V=S|9ql?~&l^q4+@^Mml3potTt_DDG|_f+s;+(CR{K`^?+WcgpP -U=->7vIynfd;_*7wu1(|+J59E0H4=c5PYKG>fp6Z0FP8=6b185sC+vpqy2q>a)M_5*w$AlHgv-3e%fBIZ-1NKCS>3*sv>$a -ZwD2*P`eHX?|2#NV)eaYlXtcI*hz-in9}qye)7sMg|<6Dw|RLVDckQpGRqKKuUXiT8hx-WTvNUiLNS< -+=t1PKgQd;Wd9Mo54f(`vQ20%$G!5kUXT;FF?B%n)$QzzSJIeZE+mYZgL_GpQ&ip -O>3*t{lAkJbqvR*tK!Jf3|fW9m#-~IfSs%I60b^op)gX-3o9Qh{mPU;U5!+(D3*88`zV7CX|OH~mFtO -kS3`2+HB4c84dqf4xOC_%*7U&=>BFN{Ex%Le^GL?6^FoDDX;1CqP7cY5u36Ii}c8{F5tY>=8@CnjuzU -t>HUMGXSLRYYbgbW8S!HT(TdWP3mHQ>!IVQJG5IV8|XGNaFIV;ZyNCEn_Co(Ijg4WVoHDP%8Fp2Ub11 -zJ27)##Bdx$|KtO(7Nla#yc6|&-Wb!sLg2+*FPdmHYhV21px!o#APoYSE{=5l&RL`V+fe>gxVb7W&JN -=B_p&F+J30P=$*>miw?82yc93ro(Wa8-@LOVykAbl8NPhvgN-<#rISs8V%wfQNeJ!19x4FO?WuYkHll -U4#>O|3lNKwMVQeH|!0F=uWg3%dj-~1;GJ@;VGX+H~P(ffaGd~(1n;qmnW^)}o^O^MP@jg5CYWDtO%w -AJ&_8+gn^q3Y^%=5bHv;VxPcH=+)j{>!Mu6+6xwdmhwH?J;N`_I?M=PTAPAV6v#@d-|WOcimjDc4kCz -<;t;r~p>tl<*);Q@yM`<3D-Tn=WDJY2nPOye3Vqig~5x{~B?;Qt0-dbnBYJpZmD5q3uD~!i`e2agRN7 -piyF>W1n(6s9fti_Md!st8xR35{IC(8~MS5Ss{~yPoe!rhy6Cupp~e7uN)KxwJ!>2-T?HA4!f{$V%(T -P)k_W`<2jd?DDZ49C&(|7+)&A~O%3(QVsi|{Gngwd&R;gvsP<33AXU8BD%+tL!8V^vU%srtpH~;NQZ@ -IOV`2n47J+7L>2K?{5m*xt|Er?IzHNt?Pw~No0GmvQ93Rsq_Yao*cVA&#@DT#}xB-rj?U#Sen6IP;e| -I0%?CoD+#gfWIKFMEDq0HBAa|WLEws@OXH-1g6(zKs42dQ|GHz7VZKL;$t%7HAO%qbcw|1_8{v?>`CJ -p_7mpc1nHuxgYCd*Eb?o>o_WoY=!30#BS#I!UFKcvZ*{1-j>p2Ta$(Xz3@>W4)BkaoXIR@HO?$2fXMI -%U@!pLN?Q>CxM4qh~Z`Pz!zEFauA(Ls8I`=zV<*xCT?t8A> -8Ov_9XqQdzK5i-k$*nPL)R3Q8X3@wj#$J5qQ!nK3ZiNYl8)~bypHid -3vRZdUtc5)hwcr>O&zm5xwoH)_wNmu6ZIYP&D`m-V@6@lFZ@dj0tSP{v@6_8@qI$Mj59f7ksW*@+(`9 -JV9E%tY!T6D`7w(64vA60*Pt9puhjmP%TF@&-eQ$qv2O8xE`^8d=E!>D=oiH>$&IhCvi_{WkdVz6wPTT -xOut2!&H4@D|W&BQeeijnlq><>PE1s**uZ`fl{E;N-(?W?~9vyiP63!)9fA4&%lhU@S$r#6c!-a%5)L -`rZ>tOgoRp_PC9;H{21b$(*0jR+%zZ&q3}F-JDs2vQGJh>RG-q3_FOA(329?P%}e3`dSYa$@Z>P2W`W -@W}@D&uAb%&z9V>r8c9D)0+x6)+IWjzG)&Zl%mlfUqh7OyKKv3Sy%*mx+bDT^uShlITkIwrhnZ_n~mj -4TIsdlMSPXno%zR*L?;n+`AxM#)zN!(Y{Ea+jR8ktPu^pvX=5)41M8A|D6h@3*Wdr{cVTb#Gq#Q5yLH -(2hy4heEjtK*Ko{cs1j?=iXV|%6)l*rI+}s-st~?GV`)b0SYiR7&btY*#89i|GJ4JD7)~TD1-ME8flB -p)4^g4|1*j5X`_J)066=a;aDN`NrYFUNqTaqWC3v;gM8;B=e#Pm-`EzmI0R?S6hZV#fom27KHbZ?m|) -m}Vi8X(gE!8G-3yn9h$@O*5~L(bChPbcsoD -Wlwml=Oz5hetR9^7jh`!?~l-yb+1Ia=gy~D|14nm#D1?xn+X#iw&i5NhDkY4-tv;!SC7A4>kp}$oj|2 -Dj&>TV6I$63GNE?f41_6pI^qCZ7!so1_(*J;&OY{FaQ$)SUFpvC7@r$~ESNHkzX4K9#NDsMI6X@uZ^S -a>K^Vr9=JjZ&SSBZ`ncDb;y6P^AEjTU#CGxU?hXsY8F!=Sgq{RdU{8biD6*DRU&*I-dX>=pROR0~;T| -lkc&A?cZ*rM8}=JQvN#R~vpHbt?s8{p`M-B|SzZ=6+e&geSse2|^UUn5Sy^+TfGHG;^FnSmncyXa2`}rI-g -o4L7X>DY@k&PVKG|$GNfAp4zcN9uG5uY{b?))~C;H`IY=A!7?e=H5)9*nl(Il!72q&g;NUacHdWSM3# -*L9kF@q>#6;n~5FR3gPUsTBrBR)6AZ8NbV_Y=&n-E-hTdNoEnN)H+hkmam)IMtO&a -}o2(QePQ&yWw7X$W6n$&Yw01Mdx$l3*P*V`NA{KwU2h!bKP5uF}pphRIOp;DXl|xm8*2pcqVe}@k3v=?A~m}E~$jLN~b;z*0S4$g?0Uu*f$*=)QNW((p4DJ -yBS#D5#}{1kLwyMso|=NIP`K%Q#>vN#%Vi76;3Tpk*=WP>Sb8}6Wb2y>MVAtWE$QJhH0`4M>a{l8Q2D -D^T}uM6F`!)A{AJjA2F`G144R1m#jZ6!y+|BnEBC*JT=27hlta)^O$NXMWy5mRk_XW4=4B^#1bnm{-S -zQ-M!gKAKLd;_u_~SpzIg|h!d);NSIXBJPLB>1ipjK7 -UGY=P^HgBOU`7|N7gIGGE5ZTj}*a-WAf2fi+s^>PT`<_dTs%XZ_Vg&rKc$?5MTMqNw84e<*kqQFeN@d -aJps8*tNTjyxboQZ{-!?>^EeS)8xdEbwl9Ar1ao -SD0NlOmd3cbBjm2Lg2;|QV<@k4xaD(&#!_drwMrp-l5w#7TGZifIAj#v@djYXgF=@`SsHJBb(Cu;mch -bx73~kzB49t5VvAI*37u)P|JxTA-u}j@nzJXy33xy`mb}MwR$)gH=Kb+ -gcyeHhE`mOF)Q)tlAaqWJ*0QoQa4 -IMrx%3cQUAiStN4hz37@NIrS%n|VBT!j-Mx%Ugk%oEC69z`FE7gPp#FC4vv5c4*;KBQ<<$!SCDt?lA% -Wh$IbS;ySXFE`}X?5hPWTKu8`?6tv$-*sZnk?s*9Ih87qcJh&WqzAg$~GoN+3&V;`@P$tLI#MDMh>ef -#?>7z!8t@#L(Uo?Y*6U{f?vX1(y`1JVfcXOKd34ul{nc126x;n;t?p}>KG9)bzDyCR^axnjQQFygPh2 -^Ehn(^Ui?p;{zpMmp#P)h>@6aWAK2mt$eR#N}~0006200000001cf003}la4%nWWo~3|axZdab8l>RW -o&6;FJo_QaA9;WUtei%X>?y-E^v7R08mQ<1QY-O00;p4c~(;!*Dhyc0001-0000m0001RX>c!Jc4cm4 -Z*nhkWpi(Ac4cg7VlQKFZE#_9FJo_PY-M9~X>V?GUtwZnE^v8^k5A0WiH}#XRftydO)MzL%u83&QBVp -_Ei6sVOHNga<>D$Sss)?-d -#=1#0Q3jD`60`jODge?9g&R&&}aY+pus(4=lObD$i=c^Z%$tS?il{_uk4R?Dn-WL@$5@p9~}BH|H3X( -S&A+D51xI^+oQi@|M~X{CTD`Z6-CM8F2Eo2a#?fs1258i(;of}a`0Pr&A04_JWWJ#a0nnDWGu2$B&^Q -h6|0tlovnEc|LBnk`w6JYJY#Rd6BZ!ANDqz1zXuH4yk;vvn&lO%O93ck$>uT@OgzM5T``%lIA5(($+K -9njjWbLMk5IU#m^c=KC3uDbAVsN)7*t)yds8|RkfbJdbQbXLQaf^d9iqvDxmV!hs*PetDB2sV3xpt%u --R7tPsD{vVdjIwv4ZVmzd9h!<27WUNF8W1dLTV^13NxC9}nZmHE8d@InBVM3z+{XLaQX%5Vnjbpb$BJ -Y%D?8+Lg!VsFoGE^j7)?d8WSczVyyu6|~Jy1aTfVFJVi -WW>X|K>mSAi6RsU%Iii5kAZov7%JBymU9`yy0S&h7lJMFdr@St9JUt4N|v~hC9szOmQ=1}#VdN#SyXs -%00i@*V)HtSt2|Fj^P=3^pVhJuJOT9fg!_z%7b~4uNa@jOb?vBJ)gic2$9FtN>s -}{Q800y#dT4LtYVg*ss@>;VVU8Y?U*d%yvS?VKS6B6+74i)mui%!DVxoH3!qybZD&LC* -f-^+mcf{wFC?Z}}Vzq8>)&OuKQr_0&E@;Pcx*#2U=z#R;Mh9rK6q%AzRElRMwc)bbM^Xf5eV(kMc!>v -_&r265U(LXCJqHG7LY@J`lDDPQ0iDYSIFOzJl384c6(R(0{S0iDKhIwQ#|sSX1@)4}^vr;>MXj?=)98 -x?MJ%4%!kK_g6Z>qUhSV&mrzp+aGU^2b2e5R-Ilv6r#6G+zKutqgQkRn+pno(UdU?tN`Ab%AXGlIfru -6W5E!HWAU=bt1ERpC%VYiJkUD8TE~4`vF3us!{C;}C;IE0+KxXu2!2U4mamG8Yl*SJZJ{1sPi{yOWXQ{{n;(yFePuXjl6KmP2fzKy*W2FZ82k7cTNB+UMfgk{pmSTEfVQ~vLAk|? -0jDDFv^d%5-l@(^W!F=MR3S%D<|4bqj;*X#+Ojb7fKKBw1gPaAA^Dv6y6hs_F@GIjh5k*fVC$oOL6%# -fZKu?J}xvq7Q&tTFPd@Agbgj{egGg%3E6C@yZO7R8PqC4#;AB15z{`Ya0+PEvHPL>wTdxTU>+2}2-+- -T1Tyu%r&Kx|%U^U`+Z(-Em`+%{^)l9%A3i$K2%7XplJB+JM$6B%wsW18VWhP`n8TmzT4YUX}?qTcl(8 -Jg_@8=VeOSxaE(psnsW06vNKi`^F6vjv2r!G+0A^a{Wl&r~X^?2nE?b&MsBUx8?u6x-KoKcMGU#vP0;wRicMIut!uY{qWz^bh9XlB;nK0Pg%5k+mG+E2c&)kkJQG`YiMN?H6k{EHyzX_GJi5QbQKtGovO-eoG`rg>-KQ(CM{aG`S<6T{3_Fx2vxs+pA52hMo>wX -}Yget67P;f?sE7T``kL)do*yB8>$ieeRhT&wfI3}aBC@StmqB4jm+6tn0S0jyDDa8wn9cNMBJocE*fO -Ab0WA3c(^o=zVZq9S9&=@TWx@jn$$BU&mw;tFvzbl4uZbsI1eOrJY?v`jpWM=s~^W{`1vMA&^W>PXh8 -^f72uQ!B2+BT8~af$eD0NEGEj`{i+>^h*nY=FrBd>c6aid8s$%E){IdO}{KI>R*9vqhp -cmqke_Za}}LHuBJsiGqhPqZAl&`!NdqQslLG6b!m@LWV)8CXC)pz4QflQ51R6=X5Tz82nb9ax#V`mSo -E&Xtzh6s$%O@Ya#E^it2S}b-PENh{)6?;2?QVWy8^sJY8rIpZnMV)@xA&p@1^J9nb)7{AKu6FQ$<{4y(h22*5Z1)k=r4kR-& -izu64jCVG3{Q{=Tcz#sPmIZahL0`y?LXNF`K`Z&bO;XY>bFx9{%! -Nf5c#{wyE7fyUyuK*#Mtk0>(>Z_OZ59O+rwJ}vAl&(k|k!l#$+SU`O)}v*^M`RGm2JkbM%6$!GsuLF{&~ZakFAPw=#_0o68qo^@>{xc9xhse{c4I&9 -o^_V!%M##7e;1+o6SN#Bf*l^BMTrv~L9f_S3R1ZO_v(4hD>mBwhdsbe;9n2Q6PP010?*5-oo#I@~WJ9 -RUN)4sYk?xj0TIaz-pZF6*1;TOCbG*nXB8;e)1YV#6V(K?3rJX#aAxDp``Ib~=kF#4W@(zOH@2|TL64 -^q}n=G~lNcP1>Sr-7L|`g#e!bi}q8Zo52l;?uvaAuPoxZSe=FhNIM@$A21q@Q3svYj)NjRE0vlUw!i; -!>0S#rmfDYQ?nobaFpgCLM#NC%f*o1IOm*W=XSTOa@#jM!1Wfj=jhkXp@8?$&)U0*qxQ0U8)*S;6*IL -xfSK%gf?bo8JFt@)x3FuHb`54$<05uVY8*p%#X~l@3!n86{}^;_UtzGm%xCPDnsy%sIl7T&#q2v2!&T -d{?2X3pE-zi7_A*1HRH*6|TpAYyksEfCsO}D(b5fO&YTQ%`gtWRSS&TI#LCHZTM4}4?&QeqY;Rc(2{f -jp6)Ja&>Iq5h-=ddzn)=F?|?g*zDn)EvAUFfuO58~A0eTemtgMGYrxk78DuZQ!|9|n^m -V*$KkCl8M@QFx~hPmue!kdSshT5Wu4*JJ18vNZ($?14}l{n%JQ4~#C&HiGGa(aQPgXhsi|Qpd+e`<}+ -%j@nZFZe8S%4Od$$V~n_ES;@F;q+EsXWL#kh$#@^nqo|aaah6D`reHlShy1zDu#d>W7&cYS+a$O7(aMsSO!WfHgvgv^ISbh*gjaWfK$RM$ay0{n@mla+=5?8F5vE+yeH`T?!K -`i~D&74yRzr0u=5>mNxK~)V%RtI2uJ2a9imfWqQQA}zyA4B{0jb-uHhgQaQ2!kG*TfM-7@Avz=@g_Q6 -mq)3^d)q3q&Po1LOGGY-Yj|L?YjK~nPagsz^hYo<$!-YVX#~jYk@^!*b=N80$y17(NRAhbqoN(Y6C(C -5q+V8Z7Se+DH-j^#@S%Tok-LBTX3SAFq*&0@8M^en^F*uu?~w;qt%P7tP4SRt^L>l*VZPFYAyP5AWKq -V)n^NNCvK@PZg?TIto8o&NE;TyR0ONc$)R_j?MC><|U>RW>AQQi{!?Yi -i+OFA~KgNZEQ8CFSJXr9Jud1N($$4k=L9&Bo}^G(L?CG1r0+P6YM${wSM#dt>$sJrtXRH|W*Z3Gz#_> -Fs+;MG3rH^_)D7KK<~A%||md4U$!gT|px7tqAL64N+zy^2#$*-qPMO%r|4CWQ@Fw`hZHt7oQ2PXSs_j -iiGC;;F}{7Z9|rQ{CO@TKFFKjXgKM>OMp%hoiOnk`|DjHLxIbqS%_`iqJlMXWKuU8zwVmSO@N+d2rOa ->26LEL|v$jvU)WW1xx@u$P1rb0=tH1+UEk|G<^_<=!x_D>xh*w}W>QHJ#_aDD{|I;hQr(sIf{g -Mw^ed>IaF7PW^)Act7!96)O(shVHf#XG1{!;<26*u#K(aJ~Jy(VJ^`w8Ql#eKU5d3_?G|+MBeqqW}cn -X_#=cu^wiqnr0Cjz@SCghf-%K?yjR&-3_MdQa3#}%;WH&fOnMk4U&`aq*dimo12LT*m-qYoqAWV`lhp -QlJQFHn}H%}uLFA?xZ@kYLFd>JmEY&M(ElzgF`FfR0fhHH{`#pwkx0vyTjnK!>NN0_0RtYd)1yh$*sMp){{V -gxE89%ffg9fG#o~4V0G2lG*JlE*hMKbgK%ddDx1>nY^?CluPKvRZ&y*fxuumY_*n1?(0egd6;$DEb4= -ltNnZ_cvlLXd;7xA;9-=dWj%2C(!PKXvB%Z-r1t*+wPhrg;ZAi;|f|~-bTE|sDHR~R6O_*)sASbls&b -MUhHd$Ss=q97qwTVWr8?xW)?Nx1w?lM$%gF&FrgBRobwEfa8M036w65G21F*8M5Sqr|&0es(Yy{6S+d -cyi1uT>WX^{HrS;UVeF>(dHud^68W?qw*y}K-d)|V$hlzdZ9VyObm~SD6Q^mSk -5@j5eVZ*OgH+4T`lUFzN;%N%Rb(DMu&rorb!eFGY% -CMArcSxQPGM`8?p)5>Pt@B-D=smkQQpwRh6!&6mTSM!v2?BRlu19)R%!_9E)^o9QD6{jxQ%F_trvIyR -g)EAnY+9G-S6KVVyH-GvyXGV4ekK6(*HRu4TAs`X%i0yZRP`fE|Tc2FI~QnVT;Os+IloeZt44Uk>I0j -k@tv9Of$kypJ?p^t0SPp<<0{r+ZapF{3 -jEPjKRJ}{{?|G3oxvIetq}fmyLf-K4>jNHlj0*5q%`Q6;%`2P^9jn=F1ONc?3;+Ni0001RX>c!Jc4cm4Z*nhmWo}_(X>@rnUtx23ZewY0E^v9pR@-jlHV}Q+R}7pNDNvL& -Eeh0yi#nIZdb@Rs*h#Pm0)duCHn%dVC8;=W(GTc%^~XA-F1CEh?xGLXgH3X7XNHGE9>Jx|SBlT(2F}m -E`5ylH+i(;D;R2OriFdTE@UF;60j`+%D2qK}spkcQw@hVnxh?+ognqt*TegC?GMl3Ej!5M_Pf%!_LLq -1g%p9SgvxNRCNeK4@hD!nG(HZzwp;L)E!H?u&B@0-PQy~o$8p#FMkn#)xUlPm>Z~=*2pS)6?a088HVml4^FEl}h^b{owL?IJ!O|uulC>WT-VL{8Vs7X ---zaa>A0Z2_ekb5~Kn)Q%Eu+E!L&thKpGUTBc^n6q1)I*GBP4VOoW%kx{;z3REg4pwl10VAO$&{%9}? -$;ZQB{PyN<3=jR=+x~DoxE{gHEnMCVuLk46&5&kqpg;TozYK;~At05B!r)7;EPevXZ3%H|>or1b0Z3& -DNavXFlqZyzm=$b>Fq6-y1f>Hxss-0}BQ?RBl9!AxxM9Yv>?9R=9tD)jKw5~zNSWz+L9d#jS>@#0OO; -#z(<_m13AI*+n0uut=Xn%AqnJt+85AtxW>xF;jlo53er1C2s?Y0RFpalp)T#;mn)a2f{C|n+$U*+BqK -kgXvTQ`vL(h9q#3-D;5zwm&5{*|m-ZSNQ8d)cViKHs`Dh+FIDHwP$`MYP?c#m~0>I9zWB^e_k^lg>v{ -Qb>h1YHm4n_>!a%mp`b>}$-@u-u9+Y(;*2Of$*MQ&rW@;BO4C8z-K}#wDZe3QJNS|E6fzM=C;YjwW_T!VKYm -`ZrV{4?WX)#m-^{xZGF6*`G#muT}r8|PVKGRPJe6#KiQ@+|Z6~#P4m4gO&|Xsl#zW66)XnREFzQ6|}wEkDTl-mbvO8GZ -dcLqEjmVahbv{SL(02KrG9z-H>`!*Rny#Ppk_UKb)N(2-Tz#X*4m`#y<6~h-O67RQkc5h?&Mi@Y_Dib -o}zK?Fh4dC3D?nPr)`FQ?t+M#)+w+#{yqW%*s0ZyGY9_u1^(3*+#K16^d3UK|Js_x&^ajsM>|&Z>QB} -ItOUnT8v-iazkI+H!1@AK||j^{|dY@6aWAK2mt$eR#R0xYXyA+0 -05W=0015U003}la4%nWWo~3|axZjcZee3-ba^jdb#!TLb1rasomF9P+cpsW?q6|GFu(>J*>Q_~@Rki) -;w|c0dx+g-MNufUbduOmBui3p)S~}=M_F#|*&B-KgJm6`-o1M~p3dM(>BjJ)bTAoTd=D>b!BN6BlkZj -CJ3CrdwyYW4FeB9FpUcnA>E}7zvIcIH$k6o82=bDrC@p4COA3K25hBe}x^*i<_!EugO2Q-@L*D~}ZYd -c2Kn#tlp(0YL9Ml$xf?LSBK)|OIwF538Rh1T;$rNG3UD>ATNJ64`(^06kV}xde*YuR{mf#2i#^$?J3qBQuVjn_{ixwjBA@7EIXKtQgxf~>}r=RBplWKpvMp-_)#B$WdiO~tL>NK5iEViCO -Jj=45+R8-jOQ9@-L*v9Vux<(UKuSYLaG_2T@}#6ZN -pgV7?k0LKX8p1XKl~;r_oFng4@zlpkczEEw;Qqu%^aunRpIe8o0cK@s{3S23>7vuU#3mF%*Z;i({yDr -=gQ$sLW17pB+QZKJ+>Kc=-iSe<%N`*y7RZDTCoVxEK$*9dPO!{NIrUpE}IvPp60npM#FIK$oOUh&4+e -63Hs;r|W6gbm0{(_}+ONhT(7*jAZlfFli73zoGLZq$7g77NR--P&2ZI&JTEE>TVB?bDCOhpl!c3f;K9 -vQ$pR}8`Opb4DRp!0rzwirjygvDwNJr*S>w%Mx1HBI=fq@p`}q~L>wlG7v-}j$zZz~+kq -LmZkEi9QRXYh0pY)r?h}3kslPO{UmD$y@g46`W9&RR(K<)sXyTRhtunQ2=g)S+SY2}Y1l;*4jLg{W=c -038Dv3WIkdUCZsEW2lP+5m54m{vAwT$bJhlg+cKO^>P4#FUOuej{*G7Wlv15ir?1QY-O00;p4c~(=X; -TZ1n0ssKm1pojY0001RX>c!Jc4cm4Z*nhmWo}_(X>@rnVP6*LK{H^q)`+_)?u&Bwf3ERw~m9N{CoF|X&`xs$^*!|v$HdEhYw(&i?xyIis1C*-3RzqW)c;g3-wE -v-odN3wyRRWNSI7lFWYCw{PiAYVhtmm#b~?(hFr=BmBkoJ#U-E>Lcb6Z|1_Nr{6u4=R&W}eK+6IArsc -OkGQ(PzkTa>$1f@l$kQNe|A>c!V1%Xr$>Ac9KP!U2UX;rUjNCKv~X(;rP1fvDx3w}y=aUiG`ydo-k-E -LJ?fvANB+N9lV1G3%A@nATa506=F4Zl(uS_=mMl+v)x88xW~M?vZv{D?gBU}p}_CYVLbQYOMgC|cbMaPyckWd7I594&H9tYcsPgY3@KAxO+068XJgAavq`dOrO5n}A}nj`KBNc2XKEijUajM$e-OOYa^`W}s9J5Zp>rF92sSzgR6nam{ -#sw=Nd1)j$XwIQH&!SkM%7@b2Qiy*v56>Ad^DiKLqLjw#;l9}bXq?rj8H$R5qU^E=Cr>{K^_|srBou) -ObtG&rQLo(}u$g(->MbGnMOh6=rCgfYUVxpFDkkGn*5B&WBzYWKqVWJgoH2A*Xcf$}d0{GwYBuyW_L4hZ -(n-2>(mbJ-nPK&t-H%s=h!H%Pd1TC+7(|=L4!NRdKUT#D#-s6>vtVuD1qpN_zP!B(FOJU*ujhp;=v4Xt7uTFLJoS2Og~vhB_*qWY~>30o(%RYW? -}CYDNMds3SGLRIEVH0>uK$w@ou?vExp*k{x2O~O208r6jC{RL1<0|XQR000O8`*~JV(0%2py#fFLU -}m!JriE&rFTXyfuQ#w}Ei9DCP;g(~70^Gpq#g6}90?!fAK{M^4}mOaAB`Ikc*V -l1!ztP#Yu*ZXo9Z0q;|+EN}^_Dl5U6Oc5GxcilFFNk|sggN52!Mrua-ith5KI$*W~-dS5s#^a`GLe_z -XN*80%i_G{Un$4H-{Fq=n`K1(S3~2mOb4@X~EwGAMQnn>R!5UB+SfP>Dta7Z4=9bGM0Hbo-FpcDrabq ->#)^=}Dk7ShhsenesB>2-9qS%MCX&l7?d0a)C#p-ecSJU-+y4*zb7*=bTt(ND}CR#1&?gLDh*YG)7o( -F(5CfWw?EA99RlDiRN=H?Y6?g8Yg2Qn3=Jm)Fpl10r5ghJh+mJ|mn)Fn6WK^nr#$VKpyTV}h;i%_BG( -S}M}ux&;Co|kJ?LdB~vxkH&L9a@wu<+g=EnADaFLw)(s6K<}*%#+z-KBG(@Cmzr>XoV_@&ehfa>byp) -+ZhugCSLSBFT)(RNU<@YW49xxBkeSaae@i>`yKuWj^D#lN$eqf-#_cRAtedWqa#t#QhsJ3Q)pmt4N1A -b9f2P?c+|AZkRrV33Vz^XB?9kIOz0%pZr5QHj>R3ho15osw?RjC`wwVjZ`>(0szm51x@X3$#j>(Jt#f -g}j)mL*nv1)7+tB6wT^s6sv|z8C`{bnE3)2G=-QmyvB!mUp#1*$J{hmlx;!~I~m!pTn0Q_fm-QNd&T& -_nWs}iL%E}7VK7-A=~F1M&h4`6xO&&}a*n#Uj3Q9l@JueBUzO4w1SuH1)O -?7+=t@98)9g64I3g=+CgoRD1?PVNCdpkuyP<2WO`Fc!BvLP2)7Q~^dyWZZhH#7a-7hd@D;rk`#9h(2w -po^4bRV;reQn?W!jYIvIE)ktLb<$TTISKY=GY=iPnO_AGI)KbJYT`NK3|7q)_q(v;kfkq?prQYh#2Hs -!YJ@!Zq9=vE;%!BUZILDAc1e^1Bb9QgH?GXaUnj4DUvZ=^`PHFVp4a_01B#jOO#vY&o4Q;Cc?@>)FM0 -IlZ3I+j|(zZsF5(c98&5nP?lluZ-7EB84v@=DuDF!~+3^?gZHiGf{|)e5tbLB|@q1&?vG46`GB({va* -M%ZWvbjd0wBm7h$pD54Fuap2mD@}sCQdIOcHSbB#l*M>P%JQJ>k9+=inNK1J|pTm~7e^1i!2!zxaoh#&BtrD-O)wP{Mf6yIQ)ZihgDZEtE%;jZEbf$n^ -$Zs}wEH}xSM+b>XtDVT^+NfakIWaU6YUz%YJijNv$JXP)7%080A~dN02=@R0B~t=FJE?LZ -e(wAFLY&YVPk1@c`t5Za4v9pZB)^2+AtJ-=PRzl3rsY0-Lwa$PNI|zwkk}7uBxU9ndBz8Gj?V>P4fx+ -uKn1qos>aGgm7XXpL@>nO}a2qMXh|`g;pYR>Mq*6m_RdkQ<($G?+puX$tHXVO+3;hIVBD-$)Es5-!4X=s7^pE(QJf#89gec0?+bh0- -8Va03|^2>6s>VS!6X)UFVm$poR|cI(TuBq34Uw-jn)8KD{B3wp{9alot>ytTF%4Tn`#h0F^LmClEv0h -!^=d@_yV=^2Uj@LMU+7|{5uPg92VhiYE2fcHQf{o~o6;Ty^ -{kBi9s(#TCLtKdFpmSc8prc^Kpc1T)ogLQg1hl@IgVEIX$*@cOcv4gd^KN0^z;$N(LH>bN7n;DiiyhL -Q=y%H63JbJn7X`T#3KQjY6O`A6Q1#ee963IIYO=;QA@G|1?n9)?jQ}xONmAB9k~Q8G0n$b@3H4`&}BQWBe;K@j*cc2J -;1nZ>*&CT1Z;!D>T+U{X5G-ZX=pKhg^!%iV_bQ^&OYB;OIOTrbkk86yp>6n0NLa54C{+aT;z^3xq%X+ -3W#IT=9X8X-KA=F$j{tn|rWA{ZT#pI(=SHm#9l*}bb%hAPIbX`98PUH_33gEn;4{9YH_dd*4BdQAmIH -fXRT2=@7gQ#TX5$kJVIL)dU%Hv0})6DGu-)%ha#qvvy76fR^YzWa0fae}-H(PB02ZT-jQf%Qkto9_aB -Mc;x~z!F@6aWA -K2mt$eR#TW#ExWV@008wF0012T003}la4%nWWo~3|axZjcZee3-ba^jwWpr|RE^vA6Slw>hHWa@1Qye -%Kkpf3)(_t4LGR$?|#a-bHak?T13W1g=o2^W$6jfIk2J9X7hI^77N|Z$TCw0=STY>tbRmj8d{CtP!9O -(dtBFQD2FBF_Udi?$0fBtp)==3qX$YMr0JR|(A$T|mWQt(2gi;TcIk+E2vFTx76BP%ac?DMbjDLd0SU^kYC11&l)= -mPyPA4=AjdS`=ywh=&l@213jfL1}{W3H}w?azIpJ@ItAie{!-~tvpf~>IpeNiA$mMXlx< -=ipMlfLDKgblj!Cw2a=#I0hytNFi#8Bd<|fMS?X4gHu%Z9 -f{xbO>pv<29wUWk4iKgVKZsEMfur#pfBBQ<#enRC06&5-OK0)kLOfUWh0$TQWsdv9jTm*Xf`_Ar+8WP -(5NS%#+F!1Vx$1JbG8xzmELj`Dlpt|J?5Y{vs)skg&w_KTsYD=_$%dz*G(f<&r9y4@n$P(GJ?_bb^=^ -WZ|+uBPn%Ixi@$^bW6Z)w>y|&ph=)WZ$l}s-7n67-cxkWXzHPCr#SSJ#vta{lB$IWi}jF3;QIk;_kAa -&anv)4Q*BHix^3&aE*$>|Ga=&A7X3?5d&dI9xYk%g^M@#nGbMsqK-g{rgln1PP;c27uB}1Hy%q3$rvj -d@DKLTe%Y9BUX`BYPiTcC@7>6`7I5UPOTiD(=1jE0}l{sWB+p!r#eq8JK?Nt_J%hkUXeWA+n -rMU3;A;;v&J2x`E1Q_ckN!V$S(^kUfjLC(sFn!0Pic-rusZ!#IRuC1_3NIA(ll&*Bxb?soK$6s$X4v? -WZoLH#bkwj8O&srdPQ9rf_~DIBlHQNL~$u4%g2Q7GLz!I^u~;VGE*OM6fS~jhID6+bCz-=_c&1TSHLH -$gO}?uI}Q_$M{ux?DaO0+ellG6lN6X07;l`-PkBGzPd*&g`}DnyZ*IEY;n+QoM;)9*4)10xOUU>t}6E -^w@&z0xh%odxaAOdo@X6>gXvK3mdm>`Xw7ImxVF-WXNj&1ULA&*EN3AB%q5mGjx6J3;^3&W2H>;~_ceKAU8DCiqQaft8^nq>>BPoS+ -qVDenAD9g~fWD7UoGhvD|KYfx$U8mal%FgP4fLUo!(oAa*ee*F;p=ACRB-(}K>wAMbDQS%J1whp5Y3J -s@xZAW=tHZKZOOy6bg0}vJJT;p4UpSbXx|M>v6XJx*3IxGQq1v)q@Morg=y{-LhcrK^!jHyqcYp*fxYrDuB5i^>5VDhaJJO -IXV2H1Yvd^;uDWp{V0FjLzRvYYxnI68W_Djv-gXMs8!G7mS!{a{YmW;jb49A5>>x1R|COAR=q!bA -TbMX?e|CKT;KXax~r>KD$Xip5B=$?@YoKMTYiVtpU`a!HX;?rpNv!-?+6pYH>$5KPTJqS;`9$tO9KQH -000080Q-4XQ}tHOb*TdY0Okq+02}}S0B~t=FJE?LZe(wAFLY&YVPk1@c`tKxZ*VSfdDT`;bK5o$z4KS ->&=;#C(~jHeK^ll8J{>Hxunnm0Tuu)>DK?yf0$pA1*i{7vD|d(9;$;xV&A@f`xXRs1|w -CkH7^$yuCHGG`1#MjuCK0N!MjFqlyJ-B@2UyTj!^JUnMK3kj%lHm$KmfU=<5fVu{GQ&k)saI2=akvC= -KS&NLm8BKp566qn}Nr55J){TuHc&uHf7O!c8db{X`6F)j&n0QdwvW5(PIM-TlWod>YPX!|8l7j^Ta=qx{$&sVeYV(zMwWZHTP?JzpyqXyJfh5%jx0)+al78)XK3|$#Ds`R5(Yt#2}XSztLh|W2G2=HG<-Aaw -N=6}9HwMB%PhDsv~XvHO92iHAW1T+oh0W5MbW42LTVD@qzoWD)E4&O -;x(j7iB^jH)Kom^kdeVVF69PEEW1`(g7%(B!#xd^ZM1}Kq)X82(kuOq>~ejIU9OMVyN3JdmT>QQ?3|_ -HQ+WGudb2YnbME$$dlDX*;Q8M4Rxp>`?j(QblFVQ~Td#PkB<{63WaM#hO#Iz1dfYxbf{CZ|1_HPMEb=)!*O+y@(#UY -G^mN8SwgsUfQcI!L!D6aEWZH)E!w|+Ec!K4Tqa2I9}SZPFi$So_al@>?u!k6zuc~x^vX>3fniMyDPXh -wrj3`KmV<*zSaN=q1dy2dfdiFfBj#&XV9H~wCgzi<#TJ=9$454_ySN%0|XQR000O8`*~JV9o`~2tOEc -5VF&;KA^-pYaA|NaUv_0~WN&gWbY*T~V`+4GFLZBmZee6^cV%KOaCwzhOK;ma5WeeI43vu$s7f}MMZN -H19mj2rEu129Qe=%lprw(_txT#URY(86Lp^LcanV)>OC*QiV>l#z22)wqisu^xSC@bM3A2K0tl>WE3U}qx&~H~mV~WIVZnAF6|h_(jTvdR?i4p%S7dP|e{B}XDcQ9Ez!oTyyO#nz~A{u-ymDNuoxlJ -LaHeV6qk$_CLf^>-q&v-(gtVrlR_AVq$w>#S+GQth&SpraZSFa(%~fl7i7%E?c}l -Ki)7|e#@I;264;w5%lM_{el5?-ycLNfCdbYfNuDe&pZB`IVtY-R;;e*T3jIoG3J-$c| -ty!jh!bW3R;T~U>Iez>RZ=lO>a64Rm*L-Tf;v41REB9*LY^WeZlLP7}%;LLL42zV}}Lff8hs7$G9sfS -tZZMqu~HuQ)y*tarqA`4*{2EDg?K_$KF8t+k{Vzh*-gYy7}-~9?5Qn>G-JRW$@UnX!cbRxa&Xp?03_q -DjAF~vRYJLC=XB>5VcACRUfyJGVoat4h?|9;&KJz;0MSR1)mI(9rddhy>s|8qP#K4NdmO -bW$ba`jP{ohMsV?2Wd|lC#&`WO}u|{Ok~aeqz^r&0gzlCQRoENPd;6P*%)Xsql)qrC{TNr}(N?2JBzL -Sg92|PDZRp1l?+>+y5&#taZt9Fjd;I(h9JUmd#}*n7B_x;h0n`)p?Ogu2R8P(k=Zl6_W%k-c>C0)NxR -A&=+``$8?N4FYwa2;$%2ntyT&5f+pH5hFKu8!(T7X&M$Ay4ZxQW~f^T{B)lmXuE+Va8 -WX8^#w#V9jZgb!DWJYB69|&)td}0WLFXosrYhZ5XPS4En|dFcnv-m -<&#_4WAj_Tv18U0t)YtIL-cw-;BJczVUgm+#m=E-qgVn1GpZjJPih$qykVnNZBgUN=H)2r$<{AX|u3& -SeU{)S~1I!4~?nFbe9hLYQ1ya-@ZLGjPddE**FN%@;}~oemDv#xXuk>z@W2bzVQYbpghm&fMsn6|zVs -p9R)U;)LR;3x@M*PjdV2pXZaa*XL(Y_U|VhhR>s1u7xw}GoEEP5L$FPoiyXH%!*4lnQ|+ -75HuP8a@G(0y4~)03a9YYFVq7%e%mkisS)?f5-E6ANDL%7t6%nSVbMb2gczID0bbS&_{L2q; -K1u_Vd~>u$={?X$zbv3aJs3$oCs!0DZc8=uV@W}18rHjOTULwH8+Or;XtmKDw}jbia?%%~q9W4_{?$| --LZLgD@9p@W$CLo31)L;n?XZ8 -@0zo+L8v_7NB=YZOU5rI4}yoE2d60KHV>{dQ@>ghY*E!1M`w8v~(MnR%pySUdfGcBz8ORx}NnBj031eqj)Y!o~XTVt0 -2N}y4%pEPu%Ex!ez`9{ZuO%Hi4mui^v`@L=h&BydU7E5lHy`UXr6$UCI)rf9E^=9z>nsJZ! -<=JtX{|NcjB^439{fr>qyAlF9{rcEEZ&r8?Znp}*zH8lQH!@+>TY4Qdq6JUvyaUC6w{SqClMNm~W>ry -F3$!kq2S?{C#I@7?#J-KWv-zAr!b)Mac5wQaQ}`kIZApQ!D7hF%5@#gdm6Z -OXu-GAzGFw_u~3LN@~(4~35)Rq(kJregX^Q%}OtiinewPNV$KUP&+TX{5lx`v`@Pg7%WMN4zXV?^*u? -d%A)3J3cXs+z)JM$Lq!bCQ0-*127Nf!--FpFrjzTx_43OwVUN}2o$KloU<`uP -C?77_~VPxBGzxMBUiuTCqKPK9n$VDLu5Y_`G#BrZgCYY!)p9(VoG7eZKRBubb$oLIY5_*&B{lghAjO5r0SKHQHa)fxp)%+ -GV{(GE-j7%)=5ffBbYP(nisPo0<5kOv-+Il3#)tDo$Nsf}nEv`-}IBNec1*=im3*TYe?<+0)!28HCe@ -xLc`G`}aD8BR4tAKGB(osirF8@FWTQ&U>l2foEZTM7XG`t};>(Hg -R^^8L75+S(NQEa~mVuDH^Sg$C~BdN?#VVuQIYekY>5e&h8N -Ok$}u*llwjl-UoaOa0;r7{HmP*YR!c#*6U}7;G}EO?Ma`~M{4zuS!f8Z#=?wQ5Y85z>GNp*rpIsP?J;55IAd^=> -=UCxoT*jTU>iG9P*V-c+UUOD#4^UBr-eXCS6J=QpAH8M$#Wv(jP*k!+`C>!0x=cmSH+gOJhUlRow{qX -3gI#F9Zq<6(jFtum%Bnxh^NkovWVn_mD=&P3{iS -%kX;V2(s~k6sIpn{q_aj@!hi*BkK*Q%j$;ej*4JCbv9M5EqqP9LcOU-Nx^o}S5(AQ5JwjXiI6hk8mY1 -*3gJfb?97Kj(@$(?d7F^EkOCzoReiHpo4@b>Ox|0OkMy0384T0B~t=FJE?LZe(wAFLZBhY-ulFUu -kY>bYEXCaCu#hJqyAx6h-&^ipz6QL4qH%L!ku&X*(E)PNCWefz*W8>hITDCzp5Ma1ZwoQHJ2d5~eOSQ -pc!Jc4cm4Z*nhmZ*6R8FJEwBa&u*JE^v9xJZp2?IFjG}D=_p2NfT -M)S9W()=dE%aWmh$}ld_e4oUP(g6l8Nuks6YU?eqG-Uv~op0T9$9ne3ga5=$h|?{0KAKt}ULniVWBvM -9ORnPusm70(Nvvq;>y2o?*t^C?T8o=0#)4S|d0nD!2X&*-0@2L9P!2WP(wFaOe|OQ*@R_;MD+aWILw` -1L*t3-Rqkmi`Yxxhi49YM_^TJ-!cJej6huSn-{)blfL5_dK5 --wiLj<@fieqrqr+F;Ex&I{1hBe*W?7^xU|6`T64A;7a}ccs3H>=RpzP`$g(cgMt^)oLiUUGz?Y*x*euOh#G-^{L2}32M~9sb`?o -dh;P8(fz`M&Xc~sojI-0k}2o~6AUXS{M$f}$lslU0sk`ir$Sl{5HX+^i(&?A4dtFaX!@t&D5YXF$f%N&g@Z{8uea`3WF?*f;$c#Tr9zD%dxFnkb)e0cqqoApB -aS17&+wanZ_SF9u0flA@4=uupmjKm*zX4(7QgwX;6R|%XoWp1KJyRF%XY;egUUF5wR05cUiC38}EEdb -GRBM1w@w+u;gSs?|GO``5?>Eto^qjUW%_B5uH5GJ53nJD-da-9Go7)Siq)fxST^UU{TJ1F!m*gqbY&w -!4!Z$JU0|d?Vb}NzS4w?0b~~-pMc?dD95xhkANi-R*yLklAMO4BO(VRl002zAs0v=? -@_a(Y{~n|VRqR;k6k}R3o$JQ1*248Xu-{xAV|((s2|<~ZEfJPi&wj4w3MU^u!lX#LAH!J2yXBM@-K)$ -v0y?hKxF_nof`P;F~~%45E^2`EhN^y6{d3(l70DsKmYUvJrHyhW%gYNmk4qSmWKL_qZ~3GScus?{lf7 -I#_S}R3t@C>IKd-8c{F{qX(a0cygoMN3}3d!qR*+P^b7ziSan+YapS`Z -UE`)Wd8&3vTN}|D;>+1vsv`Sr#>uy8Z2*^h}BL7JI@FwM>0y;POg(DZYiL~{9_e?v#l5B9jP*Bf-Y -3b?w3b@~IYJ23sGzjrV;oe4bcff8sKfl%s^&?+o6bol4yVSM8yWRl+4pr^M;7i~leIJ-n|r{WDc1Iql -2$52yz;XVwMY@!YNC9i0%vGxmj>thA+l|oR!w-kb86*c4$XegYp*6cul$#}SgjP?un?K@H~eNgM`b^= -rcm+aT+T}SJUBy`*hrcedJ_f6J47o#uu<@I;wR5u07z%?~f{iD$K*(6QlO7&HhbGpBOw22{LUIh_~BV -_yz#LPvyMZQ57(O{kKBWM19w;iOYWUY}fF#`&rTFsa&C(&I)x3fkY -Q_Pfx;Zjb?m!LuNfs4O+qsHov1xHA=%v7pGX`3W$RCbT2gcU7C7U9$+D9y)Q&~f?hTRj+SHNS%u4;DO -GQJ|dL28^yQa$)c4%8%98AcSlxr`jeL;Sl?5pjk15Seh!9OD*(~qSB-(NiFZ{2K^0zZB>Y}q3SG9G8R -F)1vzvQ@-|NCQfr~(##e&fJE*}WT5V9stOJy&AwbBY?dXBlaSMck0cDXBejfp8#XVdD(Rdx())0K9UC -|pMwbEt`FtA7JCj{AkdDTm0J5e!t0H$tUUdOU~YL?%!i3(*~pMAn|knZ%=?jN--NHuU*ATcUh(FZJxS&}RPXl?mP*9ZbNce(!i2&#EtIG3cjIM_GP~y3wse -V(-w$jzd&QP>e;*I$2>trbaYEKh}o#|A>ly^&{Flh_aZ3zA$#>&GIN*&qCZZC1#32aMU_9a -B=T|ktYT2WM90$>S$D%#lEZmV&fjxo>{S2<1-*eVVs2;}esc}pa_JP0EQu|MIxlrSaBnk`9o3@50XJe -C`*Q2ffy-gupb+wD8Ozo_@P%EpOh_T{JS-e4!OTW&T;ZJs!HG`ogPotWuWXNA*AA>}?SL_Reg{6|;hF -h%Ct9)OYlgu{BueS{2RKvde@HN8Q7TeahyS8Lj7E=A)`Y$BYga3uj(d$SfXsk_w|-7HfIW!zJ}dhf>D -*^F7WwL=Re%8DA=)CDkmlGypB{uqmawBOGk#qQ4?ia4J&N3Q}?{8i|Eb;jF?+4x*PJMSV_O|jQV~oV6?wrFk-j=M4nOULH76irEi -X@K|A&~q(TebV-GjzFsqiW*g$mnq^xP5xn=t;qBJenXz_1vxrdtN{>M%CYpoG`rUiF4Psu=~Mt30%;G -9HU4M@^Q(I1Tp01NA4jxF0D5&0UQFN$Z8uz`ild%a%G$g7^DvdCTB+n+28mDjNa7(1{MKVw3jLv)YfQ -hFavBGkP&)MYFCJGzrAH~+u)CnO1vJ8^BdM0wWXxHKSb82Q>7XR9rvcA{!tD02-+r4$~_Eo|pZWItY0 -Sg&P3YB9~Z$6l+#BaS3&XqpgcoL%K+~k^Fp%L9fB{tmM*0fpMOx>zW7))}%nt;l5>;8=fwlO} -Kz_tk$&ID@HxM8Aw^(^J>vNNc-f+)dLTJnTl*|`Vl75BKG(kiqal*b4z;xK_qY@Wz%HjEQ4cXwq-Dzg-l-Qq+xM2x&N+l!RnbI`riw**akE9%c#2|Jh$44t70bx=+YWW>a^mYfOlCN(?i(<&`eMYZH ->KK;kXLza=K~ZdR)ikS}=w}GFsI{m^-I;db(LsYr&-*#*1?}vb?{2HHMYu&%(zRxR)yP2i+bF)HQH2^nR>ERgg#vuD*#@wlqFPoQ{EkK#jvXsbRLRW -V*zWdz-0-xHT3L*5}I5Klv0)U)bzUz%UcUr3b}0Vm80BMZsUPD%fU-LYit5#{U7UdfHRo~lZ|{dD`b; -s!^VIKs@8!P$B=+x1-h?j*BdXbdgDb%Q6ANVxSClC)26q)R>O2)-p94Zp9FanI-qQJiU~Jlq9E4#T(~ -D8f)nc4Gga@#y0H-$MsKiF=b{TaQ(WNbLse$zl+eQJOj=i^sTx(dq`Jr=zksZv-Qamxu3tj -FSOPSx9$z$ru>!S!fURh)(foleo2SNQC51rglmrXQbJaHETwo -Fs*r7L89$qbu-7$S?zC_P;TxivE)!Ge8<&UpaW?W+ReGf$x6VrSNT=3upXXsvKGx+5c|U3kygExEwvL -(;2m#cG#HF!qS9X`n;H8jIJzfC-!+r5?Zy7!Af{&4jzlry%A4sA8wD#}=?U&JhBzQ@(r`@SCD0ReHgM -;0eX&UwzIi%R^H>O9q+NZ}2r1HFUPuG7%JRQxxbv_QUsUMIp7{|q3?jSK^M4+VIxM8{l$j`IqHJ>A2> -kzHaiQspNta_qU77shMIyJ7hG+Os|pMw`e>huW8?XoGOzvoYcPeXUyX@c^33n+hbv^8{ES3YZbqc9tV -Ref#1Lo7ULtKl6~+~Ia149tHXC3MhRs7gP&^Yz+Z6=8hk;9HdVk4608H< -w4E<5;xvVU>5$zLWBK5y!SnByRmC5)WCF<}E~DOJdo&!GnFvw+V8e1mM=WAOR-604z7t192RvQ=vawF -U4&h5&9AS-Q9v>58ipp~WlfJch<}%XJK+_E5TY6hoalb?xT5`WK3RSivmnbulTgXb==F?^2f9x$#w^J -6cn17%PZb;h}d(Tx!IW#}SqQAh#+lPSio66=z@UwZ**PwOXJF<5Ep4D_$Dlo~d+YZ`=u`wsWL+_@J=!C$>*{(4w2lEyT*w2ZNBA^IQLVZ% -b!mmdvNAp) -2o+-a$=&bpsSZTNzeE_gRCiJrk^q{AeubunDQ5I@%mnEdwV(H$8?e65v{SEl{0ofJ-)krqVu?;YdSrH -1kXh#%weGH|!Ivj^&=gahVPeZb ->x^%L|o=DRvvmUU=!v+Xx`Jk@)KU9AmaEidJh0Eb=q@;rM@trA{m<=;R=>LMZyi%p35cs5#1 -goLB|W(_nN5I_Q@s(Dt713JlkfFX15!VFPq=7G --!~`W{b_*L#CUsw=3EqaBSXnc+@G5*Qm9c4Dtl(bhn8G~Bav>1s^H&{Bbh+K{keLy&zU=T);o5p9vIoOcv3D*mx*{=WS8v=SEYwMG+bg -d6d(2@pz(5^h#JY5;(A(C8@fk$Gx3O?f!MZ=nMbH!@c-9t3FSrnef5kt0NJlGoOv&x;aAoU3Y?+KS0H -=sC|hZ2N#hlIM#J^S1Bti2&;AP^>6Ih#$J@7uKfk3Xq1tc%cyxSR8*p%Na18%CJO2w%O9KQH000080Q --4XQ-a(vfX)K|0C@@k02lxO0B~t=FJE?LZe(wAFLZBhY-ulFa%C=Xd97A$Z=6OD{?4yhZN5MZ2<$eFk -*+7_N*b%N9l4h(2}K+|_G~JSMayFKuI_*DzVqfNiq!jX>chPuvOew|L)_6|xEBfJ^Uf?( -I^08D-0X?7V~yw|aARGq(ygJT$o5)%qFl=f>~3LEe14eiPq~!GcI;apI**W)VCkAq(Ba#B3c1zza~;6 -x|~t;3u4rWu(#^VV44oRb^R)@~v93qR^prg5p%{`b{V3?}qhRO{EKw$@|;Y<$GSEaWccQ3Ea<@>v#@R -Ioo2te}4Ga@$5jP{S-3QX*!lw$=Tyf&@O_LYcFJW=fm`%5t21XwKNQHh_=5tPc~=6`9`B -DZp5VG-L6GFr@Y%7%d!fz1A9Is8O50%U|VP+0LAz~2Y5fpsZ071=c`oC9E5O%>qo@KCkLAWksIk6Pz* -NO($|X(z+|G{_;OV5^0GxO_&u*W6cfU5Y}n}oQtk@OUe=g`EhYrDtlkCRA_tFoRTd&L;fC26_c~DVlr -`nQ1IkI(QsNrT9@q3?tSa)uSn+_nsa08b6peK)+D<`Rg4?m~*i~I77&R(csjMg`dno;UbM;~xgTwk*5 -a!<#+%>K3SUP1_rpc>#U??`Owr0X{g=lC{vf@CxVAar3fU*TQY~Ugj4MDUcwP4;(JjL~p*&CCv)<=H9vzuHfcAm -aU=8PE~V(yzPypNLFycY(O&WIKMbVcPzdBWYnbEXj&=Gt1gGa}RyU;uzrAAq&wnx_GRcoezV6Y=d%!o -QVAUH~#@pO9KQH000080Q-4XQz1!h98m-S0Luyh03QGV0B~t=FJE?LZe(wAFLiQkY-wUMFJE72ZfSI1 -UoLQYl~zk{<2De!>sJi42x2c>U3v=;py~FJ0Gq(rc+n$4OCy^NMQZtoSN-)JlAPa>iv -@7VE+7wSxQ2W(`to4L1WEgxiDL8|2MTmbVDCRtjq;NU_&i3bk87DXiw0UFP&IN`0ap -!l+F((VpVsIO7;C-r1{nj<1smX7tEG3y5?vG@;29k>*m5r&NWI&UH`o)FBygvJziQDy`J|7`R(sWyaN -tIfuC0m@do<2umZwlM@+f@rQr;)LA}Lf^gx_oIL++zx#a_|aP>WH4Wd>uT##FCRuB;bo{OKrJlVlZn& -*#j^oCQ4QjU0hP(PPbXY^v4`vW{vcdTtzRwdoq}_^2P;cU!9iByAS%tNnxhFDmA(XGE_I?q?T;XvM9wuRuq@r7V|yw_Q830mjj^G*x`py -iH3kP*6S#hb1&?IaNAefD=l|$KrO+))vXi1lN+cORK5PQ-*iGWidmG$H8QF^h=u=*Hv|LS+=eI?(1{_ -(lHp~%noii`iD-&aJGRd2USx4nIEX?)<^u{!0Q6nthN@V$IWd6Hjl@)U7Gd=Hi=ABLxahF9gzPgI+Kg -S>!j49qx{g)LbFuuUW>@_ZyWLD5VfQtkd5?@i?X3XDy|Mlyi%bVn!Hct4yN7=^>Vc?q=^@|(=OnCPko -*gZ??ta=VZjX?VEBI=$^)eVnoCdc(c_|`Ijqpa%$OCt`B>Ip8F;6H) -BeqP%Mwss@KNG;iIKq<;ffG_-J*q%;IZ=q;n}_RvQgKL%x@sJ^B^p)XuPgN!)cT5xoP5jhvOS(ivFRqO -Pe^Ic7o}!raXl0qTJFAQ8fl1T@~+b?Tv85Uny_`=JLP?3p)+EZDBj)b6ge28*6C?OH4T~%49$_oNi@x -2D=*5zug#*%iz{;|E77qlkSPiJ~>Z0XXiRZ8lG@m^zXPI!#nf;7XC#P542q=PFzdw!jhZfXmqIiYxq6 -1MR!1YZ{vxxE3g_42rq~|wc>~6c{9FuJYUx7>ETgWOQKQZvP3jJp-%tL{{`-u=d^UsFT|qe4tLrNw=< -}4sz#6>@85(NS1`SnyuYR&Z*%i^@)0{li;mLC!7Ph^0Z>Z=1QY-O00;p4c~(;$KNE5S4FCW;DgXc@00 -01RX>c!Jc4cm4Z*nhna%^mAVlyvaV{dG1Wn*+{Z*FrgaCxm-ZExE+68@fFK{zNZcWt5V91B~?E^|>$=4|rg=g%MP& -LrgI8p^DGsk02LiuLbMCz|0aXX-H~QRseWpUGrm{(q>7olKzzcV1j5z7Z_NUW-iL3mDDuVf#Co+_5b_ -&=-o;CDVmra&xog2POXyH#dybB+^U}!(va7#rOP(Pl9EFE4`?kn2Q>6+68NIEb_F^EVglQQSyp!nfto -)@6Y@oxAm6g^>z*UiVd@znaIQz{}trJS0ru7DV3@$lt8}bNyqMoov0wD+zQ5Xa@3Yd#l#M#fS4{>JcG ->Jl{Ys&$H@4123ufx0vC%kX6zDgC_bGT_Yy9=71|NBJ~3}v-(6-3ehz}POA48LN#TsMeEDJ?sJHy3$c -2{^L>;djHF&#s;d7q>X#9_7Jx*PCEbTYG>Eww*4p1R11&av6a?lic?~(tGKdnljP_FBb)tlo!Z>`{S1T@yG9qaL+8)sr2(Gb~SOjzkzPhrp1~ -tH73oc+KGsa3niQr$yVOpX?DkVf_&U6(_?3<3TvtgGgZZSv(4GHxRrTMyj^^BI)yQ?0kb^n=`v%l!@L -R2O|O+P%VYLKERkFhinU(8<(U$*&NSG2n0y=|9dboY$qQ{ga53~iP>6z+LB6FTJCSdvg6o8CHcw6}^q -9m$N#TznPB2Gw&EfK*&60Z5|(d~6dDsX;$cu*c*bR&!He3Um#G6qS0@gYl-$sdFL{5wmO2p -d+-^c+zt5WJwUe)z@fvjs{U4Sy@OzQKrkGiCE77Vj#YAtS^O$^sPgE10Pan$&hhT2T2A7JDK9K75u9_ -4(#Y6NajwAVKO})7K9o8OiVQDw(CP>8ypp4uG0X@L5e#=?kV%el>LepQqkE+k);c(ddX#_V!(1#Ey`s -l0^8P^mIymb%yPa3N^I35uO`J7+o8NAT-lD8E2-rU!4@K%`#rC2eQDBEQn>PLC<(<+X+k%rw*+Pq9oJ ->bb%(*;xbJ~#da#gOs&r9NbtS_wYdPf>zU_2j5Z#bM0V-@Kv>{pOaVB}52;*DB?c>`^_T8&*U&O4-z_ -Nj`6z&@+q%Fgi9fb=b1NI|X1y3xm{;tLvPU>G3sdIDV^=fFYj!^nwQhs0ITV;(&J9FKr!^D@GXQMkGuq9_%QIt+w -^y4u0oJQ?_W17%I3LE&(;x8vA1gq)n)K^mqe6)oH`pd1pn-a#Eem5 -P`efr^Z!|~cYZA6%}mi=(mrQ`mS<8D_v{_T7)g8v-{1NZ0b`Ey$UeBfV~m<2=A%}u?W#VxNMUJuZ{H- -&g~DEWo_dx@w+J2oy|aqrkyn^~blMQz;8edTh*d_@*PUOJ;o -&P#=h*`^F52mYHc<_juRZgmA@HRJS6HW;3>2+Eu}ZCs>$`10G|bbhXOGgEn@;k`b(vtV5hiXU;}S9^w -!Gd}YrKkNOU+`c-E`hr(c#5MIU2lYq2EtRuwDaT?LCFrr~~6`YQBI*kq!|I@&5&fe!}h7N<676&MVX% -mS2Xxe5U$TV&b~R70XKeAB?dH&X3natbN -v=JKGs)o$V&~+l5&+_P+79O3#Qe?x@uu!?3iNY`L7y!L8pAj27H}bVF|?=Y>L220Mk+%{^Hv)Bl6t1K -D2(I?-OgwzFxW4Z82f4H-*5?&rnP7egUU+o2pL&p!~?ECVQ^A)G|5B8Vf&(nnZ<#|4tvZ*;}t<>i)d< -C!sqj;K+NbVX)E&`ojcMZ;B=97WbI*l^YZ6sb4b!1_}C-&#yo=_>KsgFb6IC~8+Jcq)v-F}Sae$snrB -nhL(x1I-hmDUA;z?XSLtY3ALu^L{G{I9Z2;{?Sna{?i`c=KLnql0Wn6QjW?((yvJi@nn_BR0yya%m4V -@zLjOO&CCl|)WW{jcNNfb)xZrUNt9=%1GZ;Qgg%WGvZ -$>;zMuJGUl-deeM+Sb@N3hq+?YWOfTSM;-5uDUFisHGA2^EanXBGxQlbxiYMr)n-iddm76%+hW#YFUh -;jF~MZG4ExpgUM2eXi~rcdKAXgJAINpoN)+Pr?o$JThzO0Wr^OQ6Q#oS21EnF#IypBOq~l^eL_5X-{n -kpa#tl(hQ@r{(DnWP>)Z4n<^|74{ktv0}4W9x``JWoFbCp_`nKzh(yPHjeVo0wioN!#TZ-|9=zX4nHO` -T*#F2aF$7Cwx7HXMw~hN|18EAhRpkel|bSlGK73JM!{=5m;RlaVJ`Z3dha7Lkb|#gibdJ&(}_e?G+zg+3ToZ9?|7vb-wos!;TV@nI -D_54HBr^t3irrSu*#>yrpX@T*IZohvCp#8tPZ^ccw=eMC)csPR9f#eDdbQJ9`yzAS_A -_F~{$2TaTC6Y-A$MwRX%t(;-Uf1a-R@6aWAK2mt$eR#RKC9dbVa002J#0018V003}la4%nWWo~3|axZmqY;0 -*_GcR9uWpZ@6aWAK2mt$eR#Uz!Uy%zJ003}K001EX003}la4%nWWo~3|axZmqY;0*_G -cRLrZf<2`bZKvHE^vA6JpFguHj=;VuR!I?nQCR`^LA71`Q56v()iW2y|&W4y=<1INJwH$kt{)4(RRCk -`_2pi5&$VD>FaxUKU8@yra)jY7|b^YT9)~S1;Mhe>XHWmOEyJbRxDhIJgqAp$nS%JYLCN;SILI!?`gh -TCD}@U&4qp{n=T@c?s%oYZNoBy0b;PkiRC*zDKE>sWT9X;)I7tlef43g}6ABJ5iInUZ+kO3Yz -zHelozWO-8?$LumB|8jp1zN0uB$YxmU+235(STvWfD!;MUHd&GzS0$&=+~e<(yF(3SrIc;g^O6qX~7x -PXRp#a#7RmZ$OJnK{~zVWm~HuC$ypf3z&os3CxTTu{N*eQH(bC*aNrS^0Oi7rEx4isk0pl -f&S^q8Et(b=0F4?Pbe$TiQ2ee#f(`q~LmKPD^)b=DH@v=A -n{zvf}g%hM#PG`}+s7FOkD5{2m)lmkjj%#w`VKO1Ra_q-G+A_`ET8-hUf;2NK1GSA#zr3EA-(~Aqfb# -_a(-_(mAp>dj4NR_uzC8<|CQSl9eYMMtKaR-6igjKW-*14!~n>0QrysS__LM1$RR(iVt%G4?_dF{En0J+-ZA@mh?;X -aVK1MI89fX5^5VtwUj~B%_IAxPl7Ji}g493CNL`>ck|J{-rZq=R9Ri13jz3(Du>C2k -+UFXRw?Cykb9|D|#6js|Jd5(n|vz%C5N$zksY|KH-K*lBnWO9p?_NBe@Z3wpvtN;TGbf3gPbkIG(Hf4zX -D5|oKK$r>S0;B?rdMu7`1!0vefRNRWrQwd3K(=bjVe|476aoN=S;n{UB=wK*rTUqI^20g9l-=>&HZNn -`MPQZ+c#VZX_Zsnr4T0I0$*M+$sSTCD&8B^6aF?oa8lk%27OW(T(mLi!?H#e(TUZR}soUELyWw(w<2< -T2K#-6Owm~c{yHw<~D6ZK3CN8Sy~Ln4G1eS>zk0-Z0=Ui@DszNMI;qK`wQm2BwWNQ|*W`La -H+$dAdWZA_w~HY)MvCjo|SZJ7!9^$9x&qX(j2M=Agl2MjGe#)?T5ndIl~&Y`adPwWhIPaU#M6N|l=7B -3z~X5GF`hUsm?&iA8Y%W+>=jUte+V~4%Ev5O -vRz(-V!le^n{A*w7t~?7%QL%WRtAc6&byWp3CaWix7Ej)7{UR^C -q12JXF~J|TtPXx;$&UmoAxqEsO%I&ew_gI902kZ>fNQ-Nnvf@KPD+yEKE1oj8pX&)q~gHkQCh9(1KP` -kqKBZLjr~nw&|^C2IxN+Y!FmDYpzi&g=uipw^TAcvC`blqXQuBB{kvTQX&2oQs=2#yj?Cv@z(=DBTP&%;cQhHlpYAfXU5Q60_Cf;rYO}f?Oc)a?{>4o1Sf0 -nTo&v=QE1VosoWPr`dv%^Z8AOdDirl%q8*8p7BX}UZ5sSZ;qM{$lmBgJ21@>!~NEyz~x6ntEM#2X-lX)ga1@KL4Csl-<1iH6L-2 -bZmiL9%uJ|HRPsfNnrc?ksT5?0>ICN-!H}==OnFB*AsK0aQ-&p4 -;boqt`Id<~srrd$QwAhGdJMJg3+R#WT&w*a^NQWkgC6Lnf!q(>GO`AAv%Q7hv;GHzCG+Qt#I7pRM#yj2%760%O~(8y1N%s-M@5eV&TP;H)CsZNw1KN -6#12gD<_Xk)#q|7iS4E1T(bJ?~pwH}|$@ZEW}|q)fJ@14B#{7@IhOH4Xu1z7I8}&2WSvh2S3780!R84 -Uke-7!TODc@8lOn-&F|WuWgdT+u2e&OsBP!39w8hAAM#A@023&!SRNI&o-rwcRg5t0;K^j%uqQWS-B! -tl@dZ${lIyENx`(!U3CKo?cGatKd1lN7Y>(6qzn5r5VTSvR8D;Wq^X#ai)kN#C1iuAc)<-4tmgG5skxjwqR?yV1jP#}uPW}q~t -->3PJ|gaJAoF=!ZL!Fj4lUF$`31U?S$~KrRQwEKjh6`Y_A$}y-GfH0$`Kj!dv<(mruoSox~Ibr7dU=; -mh>QMt#VFLH$WWEGl7S>fPg1E`{2vK_|b?^g^4@o44=&dIu}Z(5$58^@e*}`wo|afN8+$|w}*#gb~yB -Z1h3WGgoOvlg88X1e<$d8%j@7c8jZ%i{D2$yz^*Uycz28%Skq3>H#-NS-k?n4OPe10rR6m7RB%`2PVe -(1W}7@tmOFWz$D6^Jb)z#cuyejfceH07_2fr+vGXjE9b&rRD1->O8tU!zoL#?bHynBG4*MvGqu!Oh8@ -@rPQ-X>vR{-`b(3uUKF`yYuwOfM^@42TAq`RBdb+-ZkYqvq;)Y--$TGtsw(7$dwP4S$jpocP0nfF}hd -82%@0UfQTYJkgN3Zo}sMPN9ejBFKBjAv0E%LHmHg&&=B&L|_?i$!N+A-XhqNwx-ed?(1%Ro=T#-_rNS -rrae(5Yo9v&|rDYhO+5MHx1j-G^agY1vEqQUH0}Q|G3q*7FSvO)rZq@U*ou){91RVi4XY@n-+|`P4nKs=F_ZmmfVdg7IHVE2?~Ya$nR -<2K;s;%pB&+$pp$5tX1(Xi1tp`(rM(7;Nzdmgw_l9+~d^sF-2BbVgc_vNp!GmvdhSBKVv&s8TZ}5{To -pVeGgk>4-JOx2dH0sWwM|}muhJP#nmUS(#(S_#!H85n@0G&M5*~)oOmlc;;A0TR!qMJNM4IHN15VYJn -#9T1E$k8a`Mh?1g7sDqwHBE1yeBT&H-?j~MPQ1Uze6u`6v(hepJvm!r!vyYQW!_5H#g@5Pvuz -_J5X|AMrY5HVmDdVS=1g7HzgWIX~ga4pQs6aq}Ehp=p!C#45><^uPum^vbQ;`LbKf{=P;YQr6lItDiu -MJ1^5S*e^R=qUe?i*wX1Djfszi*H(b0l=}i;pJwPDkPIDv?>$GEc+&iota9+v7E2mLsM%1}r@a_5KH` -DXrboOfc=6oK^&%U2szL^_9+8R6;JSFFw3Lob!h8px`G%=F!4<5eAd<>FgtZ|or#tqg+E~+`9X(UHe4 -iAIWqursQA?hss+=ZdZso?0Zj+nM|k>!O=rJ~){J(*Y{CAdA~yg^o0OrhKT$j_tEkY|kc@^WJPJ1EeG=d(I)2{;5c -Cr*vZaYKUkI$#!aG#T-Bq;DW$4eSP~E~rk$pXZzv|7J?XoV9uWm3qhFZ|Li^^mex`TDQ@`w ->7ZMBSw6=@Qbio`}{)zUomFz^!rgS1Z9rRZx6%4F8e@96K;O}Xa@BF3+&8ea#d{-*$K&yC0Z@*tqu-a -gW7lnwliR|`o~h%x?hin(ZjLm^ja8|C -P9-5;U$I2hgW21|+nov0vRF5RkKzsh3>vqtQ{nNizoqGk-XvCx-We0=CRWb6$ehqB~cmNmlyAv|J;mZ -yy3&rg0C`O=c=N{R(+@s4#PG6I)EfRAntmd2QJF0a>!?x>NEmpj7BGnPB*9y#_@##>NK7w#>@Jvvk(i -MKInn5)b&P$$irm?t9;lEGwgfbXSdZC*CzR+i@}HIA`IeYUmBHvPaz9hlvET;2Y%`ko`sHpDvasiOB9 -(5j(_@U2(awbiy^-G6Xv_~_1B+%Bdo(d!;KQ<~d8FL%g!SUk)wZ<;bYwZWKM0cC<=_A0sO#t{}UOkUo -hO9uQ6{P8x);(RNZr_{et_2SR{7hhTVGn?L2$7ot^>#a`Kx|aD9I-!wud-bqRr_VYRbSSJZU4U=`X_| -B+;b`QD9CN5Qq&voQ-7{sWoRou29c(*Bi(xYf_*fK)SZg_`zh` -VKbdSXb@*|@PI$#%5ve0Mqf}*BWtz(bmsHS}LZp -)tZOD?30s^Sl!_TU*%OIi#`e1V@<}k8XZ>?D@sE2*fo+vM3(F?#ICBao=)aF?{|WqSt_R9uydFS -!Bq}W+J*iuL@|FVC1VcQKCQ(wQvSc`09lgZ7#cZCJaZlf5O~_x;5`61f -$x3eHPPq%4vwEI|o@M{ -jIhc7s&qz1qG?r)=l#8lz~$3$pms7y5tTx6o24%Rol25tH?ptZYYQgDm~#oy -U9_t&?2!=6|-u4z@1BQ4B`Qh34ob_{id4N1{sZ>u!98LX0xcz+WyYzQKlS3bz#7|xC!J3hVF)jA5jSS ->2Jl)(73}%`V;(b)MzxgC-!-4|%uMfo0toky+mq;<*?T(;Vv|?E$(_{vq$0*4yu4OYXA$&VGl} -A=nmN23 -j){6(k1fc%tRRHVPdZiA`GLAAUNrpdmQ5j$;&qnSAUc`*F(#GZnD*c88UH4T#rO;*<`~cN{e#}Y#1b|2-Jg2}8*6B=d{EFGjr%wUG4@$9gM>;Koe*;iU0|XQR000O8`*~JVo&cQ-MkoLP(~ -(^baA|NaUv_0~WN&gWb#iQMX<{=kV{dM5Wn*+{Z*FjJZ)`4bdF?%GZyU*x-}NizC>SDr)X1_o*^O}Az -_GQvLj2HKa*SZJAV-`gIj7+aGY?Trko)adkA8PkmXl4eID{}Pa;B%cySlnwT~)Ja>UA=mF8Z!-#B`bz ->rLHsNp{oLW#5S@|2{a7*G1D*wfa%k%Vkk5)z7w`--=HE+O_KIHft(q*B&geGj5g`fOV5(ZE{7I&+%u -hU019{-FK$tHD5U3#_7DSDlzYhx>8RJ4-)wNE^Ecr)f<94<||EoE2_(4Bdm+B`}KPFO2gobKU`m5#;% ->;&&9^Qbmh1EgJ_CHA@b-=9N+Y2H*M!Du@>Wlk(`83fLnJGugba=`DI;~){xKFn{MY_`$1&6XfCs+1$ -^7r>$ZEf%BrQvt*aMRE9S85AH<(~(RQDBnfTtDdY<7({*aZuji|TT{Rh!CJL}Rru{9I4u3t8FSI?dC! -KHiq(GM_QxscXXcfH*E3RRnIew!_YMpRqedv{W}l&blL -e>)}ReKSz^S&%G%Y^?zH~pcdZ5TG34xMJcA6x-GB^x2-$DtZe~X(-qO*bi)MbJ^(~xa0Wl@yJ~mK`)Y -4nE&8TZcRx1eIGKuSUgu)kWzACTYdNjzW}TJAUj!{KG7tC4p637}-5w`66ETHb2M6~J?w@3mXu3-I(! -l{RLbxv3VG2*bfv=(&PNzsu)9L8oVES4tvcBw~U48REux3NJcyO5D29{UagLw&fOw4A&n?t@iI-|`n7 -z{L9OSt(0`Da;Lv;h32F57X^_VX3YnI1T!#%HtCWop>NEXk_eZE)qaNDl}|ngaehamHgo#^f|Z6xJJ2 -4M#~{3&eG2@SVUgH=>z~sypVjOL*;)j~^~t1#r76V9Z6E^sQL*upo*d1 -D6)+2as)Y$h`{J&x?YU=hXs-tel^`rcX-keyTPl)v3&r -Mz@w!J$s)C`_%nA0j(K@ZV)6EKyfOcgMV3S+#>%bug+TD-$D~{ejVwW@OxhHwU$IY1?1}+O)V+MQ-ig -mNsKvwAoDwgINh`l19O7V8apnyv;zJ7i -xZsyNU-V`?{tZ+g5!}9sBo*ztaO1J7Gh9_%`%WXAd#@ab&rZ97zYkdNf_M29%SVOdNo8H#k7CIgsgR^r2h(U -`Cb&lFWxYfy?gB36Nb3ZMv&k67SD5WV_IWkv7B1OZCISjs$5w_3ut>WN83Fr`S{Y^-6S&?vZ5ny19JyOwRuyq*096)ve@&=X?u(jhGs2spe9B&Rx -Y)&kGcXTIQ?9t#IT+l!aCSY)KJl9PR!>MW-5l?kClbC~n$A(6nDC^AbrR7bNPb=seAT1gpT#*Ejv%9P -)Q7Dah@lkp)KpdHd)q|`o0iwmUfi`$njVMJ%%c2BUru4kF0DVv_7VroXz9r(o2AlU~*6dv=AZ-ETL-w -h)&TYo*6QH{*Mfj1}ai$0=B&%WY)TR-4$bVt0);VhQu8<)cr?un^U%)4eYF_p*lPwICt+u(A2^!y8c8R&`(IihzlPa~3a}-lqX(UqTnOK3TKlsqlbg -!-ofD?Q%1?FBsIH{`B-IQpognYmF$+$rc7yujL9aBXtf7!22HeP`e6xv&4fZ;>LT?<~?JXW;4zI_u4l -Dl&Y*!ndjKdaR(sXF>KYF2HI&6nnI3NZH7D( -H*3D~;2lRb(@iSRX5o%g8lal`1k(_fxbjrli8CXGacviC^UHh!oETmNzHNywn!{j>@s3>09Ti0)rwr0I1 -uR#!@V`?}`&|BD9!WX$B$gk226LrTavU~PW>G`|sq4AY&`i=~ckv*8}l|U=CK@r*lM`eu^$Pq_ER;sw -^$>o)7`ep+rZJWB&uKM-V7P6D%S=c~Jr{J#KMsP!ID4Wy_V%=FCCg&9y&+oEjDUML}T-FT;k2OsIp8% ->EBd^ZxYOq+)6%4x6qREzP5c?!bQxD`PvVCy(+*b8gnMT0#N%c*#9CZ>UbmhrK&CY@`uHjZc--u0_vE -JwPQ5I;=1`8zTo`Sw7I~WeQhpFsgFiw8_$yz%9(H{qdrom5YU$`{Ar@16g?F`NDI!m6kXHNisAuhZQBQ0D(r!SKi4d`s} -%`*$GwK@n@q12Jd?&!&_wx=VKs=7D#o)V-sFI76kRPlRv*esAO+2P)Q`qZWlYf9>y^nzgni!RWKuIXZ -|$K^Rp?Unu>HcjT3(4Pv@p(XJI&NAGG(ru#66U$d{f%0y@A;WHO<4|V&^RMg_aJblm=g&?bSU9%gzLhK%o -|O~*`8e}saE^TiPRd8_1dIOas}lTYiCB_8d3-~2%c5)vI8;%!J^{&hQNZK+U-3ci%<5}Nsw=VvNK`Z>YIv_DlXB753>oOj$B@W -?pQ$J`xOYGvK+(zM|fJqW=cl9(IROid0Pp%SQr-$8p$mPDYT8vBjW@{4oV2%3J7`$A}%jldE53{veSk -p6+Pq};w4W*F`Rt>mWb$xo6Gjr5Ri`#tmB1IK$U^lUk(WfGy`IzzJXM-zN@Ergayz}N67?Kl5o`zE%y -a$6S<2)P}j82W8HV`2HHj7*e*|bWDDvYVn`|r+LM(Gfg^y{fO;RAyb)k958UeH!`;!U6Bj_CJqnuJ+d -0O^QG)T|krwavK@I4OHgES<&gL7Jz01>+n+$0MUD)Pu!)Kk?+{L@)DQ8cic8|{cra^+_171L#Jdf6#* -+=~u80{2RZYsvCbr6N$2Q!M~x7Z -Xz&3|5=FszQ47N`tQlDpgGQ!iP196cCOffD5zJoROfzftXt|oCuIzMor$`udDEl`363`~YU4F?txtD! -LoN#x?JA7W}2Ted*)h>-6BrX$;H%*zO@BlVeaD7`hGG<%(nb%2LGa>Ks@$pkdh6w8%XZ!&$308?Irl{ -`)|eT<;uyjtZlf9SANk7@&U6l@Q+q~oJHgoS5P0&L_paSYE2unblYKC%I+pSTLFJMAAtxuJOg&zDCWO -w1@89z2n;=Y|@aI$vrN6)F-qPt+N>QR_>f?4j#{dAxT=LiB;@Z2{ZV-w7Yl;FX7Xeak0M-3j@Fj8h-+ -jARIO51Q`0UT?spzA4J0+l@VV`m_e)Y4W6m~YKw$kyxKNGc`Os7b*qTO{Cy170*r*GrS3hYf2E& -=K1I}S3SAydVvKv?fTrPBi;4hdSpY%JYrOAakVMQUf%B;hyLpqG(K$bZnagL#NsJ5UG=~hj>bq+ZRL(aUz9`&^Dk5g8{2V@I9s9I2=DX;g^NdS2XEO-L?hVnKq=Bkx)?}>(v`Z0G~FZ!e@JhaC_iUBkTcUTdSNp`83_VPbZf_g6m8p!(`V1Wer}_GpfNC@qi$Y_5jiySf=^m-N0$PO -2SU+EH+1j;PZ>`ES#XkQ{wsN&ew}{x3t2DX8|&;=C^w{ctdX$hvL*+-%Sa}QPon|USGOXUM-a)=r-*Z -4`3h*{EWpu`MFTu@ZHdVm-5qo=n6U7$wKJ&zTfshcZ=IMvQloa1a)we-907*g`LTqLbZ$HtDuLuH=Vx -LgC?>)IvM)Y@bW%F=Mx6n}bX7mJxX!6Dek|2O&PK8zeYv4S!61<0LAQ$|!pG653MZPvSFlWE1&1}Nu! -@I0pV~!EZcn3hOX>!KP+k{DZ_y$$@<7_6b5xn(me?mPFgSyuM?5d0b5;O+2^NbY=EQy1$Y>xwmfqT|% -1kwGGkE$Y>2$uTK6K>i*a`9Roq%N@`n6h{*OZ5)fJE{Em*5%SL$g)!baw>E_wMU4t4N``KLRA277)ypZ7X_-uCg2_jvtze-mb1g#+a2EI!mYrX+- -D8qi}&X$IVZB8qy~sQb)C)MU!A-us{Y<`yng-u(>q!UAOE${DaYLW^27V<_t$@fr(wbsy -GiPbyYf20)o($=5lu0_?NV5sRezHf^$F;R%aiBNPQN-iJv}-7%}JJHSWgeAJT9tZeR_GE9qS<9G8z-Cx3l@IlVp}yl(;3vE8@8QGo3@WI@JR-NZ5z{n%D)*b` -NBB4B~Wra8jL6E_q4~N&X_5T2q7c$N=vr$B3nq`K0cWMAQ@hSiK!TU#z=3bmv109Qn>@fo1H8BibWZk -jhdzUy!~7rn;p*a#uG-5mCjSriwCLvLrM$_V!|rdE{=z4Q -nLq_VpEAgpyJ~}{jewPd;gAeOkh(O~@FwU5_3*?J<$4#DNzV9Q1h=B|dF-gdqYot|LHRp3TQ87tW?&! -4J4LA16ELe`w(ooeD!;9P#pjm{IZ6~UPa))HTbISHNUnCkHP+|%MK?Mi1JM(gJa>3NiOUI+#>|~EBGs*lFNru|@~ -*8g(+NbN)1pkuO! -NS`2s`1~d6gG#45KOP$61Ea7Zj5ea+-Or~*wWx_ly0=HcL9CBA%Vp*C&!Tmcb*nIE?jzn0G6ctdc#$7 -cM9?M#GNtTS)$_k9w@nB5S!Z~$;|q%`yq18o`5Ytdts^kqs4nv#5+)>$;+Jr4GZwHy -8rh6c`346Q^jxu&u!OpKF&Yw7M&D*Oh;>GxZYO9F3VSyP!gfsYi{Qo2TC%Sn$3c$iXcVS+`d_I17L+YNSTg&;8V8p)<1GkV16J1cAaQvuGWWVPn~E;aj}wk@xc@CIrcB0-|CpGJtt+>8h4OJM?*J>bm)%i+pg -9v)szhBVw}hE%O(2294HryW8^2tZ_%B_xW|)g;xWt^?94_^!G-&33we?1lwYDAP^Zd3?|m^t0xCZuC(8*4 -kye!*VXSNbr6nVHsu+XEfRDm|2aDB@-@>FzXEOh`ep^5ns>pfQa=;|0d!VlC4&V1Z)>Nv(cuYp5zyv_ -=6Rhm(QIV=k}pgXBeUOYP;!++1+dA@k|0sHXYPl?kqIPlSvzgr^uxJ&xqOx)PBo9ud^@+m@0XIoTs3l2 -N0pX+eQPh!iAzS$i<}5e4j@6b{k5z?%Qws@%cMR>$IS3EtDilWWa8i>zbsi#{W-^zsUBy!huZ&j%k5D -;P1@}m+_LPRKvHZOl3NS)S-?BRr -1RrR*JM?@@FH}n<{G+G%&Ej9K+3{GRSq_Szm0G<$Lw{+-MI#F!Q86V^;g0v0&2C*>(291mM>&2SKM-) -4oi=wVaYAZg+G9BrKn13KOUK-QHriD*P~?OqUPPzuFB=`M_7Af6cvElslB`g@`t0eUsE2T ->SeU}126wjm3slIk?apOe(nvL-LQ?&-i$8GKwe&{M9Cv|nUQodhbVs9qNmWgB9cVe_j#Et7R9C1l4@# -Gh6G*MBgQy4CWZ<)th<`p*??7rBV=h1t`Wc<_1T<@)EAy20_!y-`3et&Po4_xLV7SOWPqx)!lK4A);O -+E*-04olF*$9iq@~rhuN-O5AdS}X3cLPb8dX%W8CvJ~;dC+g-c_#9=H7}UD}#~qF|Z1atXhzGWg8#m6 -lOxhlT1N9;^E!UEec9=dMKYE3d5pDB3{Rz9k?ZY*?~nX6(Dct=p<Xg~IlUN{hE5{SiG(U^N3Ne$Q};{s*KqVpU+F4k8P-ceuh_z -ZW;aTBSX4B@ib2{5#7<&dA{g;Xwli_0XdVM09p#Dp6+Y^BMZQzN;buk;(%-)cORfks<=gnn=!UH9H-GfR~cVTROmo -*s15r(Rtb~fc(0sFzEgK9Sl7tYId>=d#Fmd{+C@cp}Y=dZ3Y*T4cszDQJ=>dlVrC&`AfR+Fi9kznmcO -aLL~!39e?Nq=?gWI0d^41HvSa8XJhCmKXM92{<4Qsp+g)aULoH187=YTgLQiwse>dfIlwu+EE%&>ooS -tq>b81mp%xT~YutST2;moiLjhV?EGp49=<#+4+?fctT2fHyXm!>d!qL7156Clpfy`D>IoI{C>udDr{^gf -pV&Vw{CCI-{X4vnQ5^$8|Kxxs~8C}Xp#F=u-dcYhbT7_^a+ldM+F&lJ2TNb7VPgTRBLK9n`(>AP*I>ZG&0tjimc%zlz+g!hnu37*f8!VM4k?E;fnRLosVTac~~CR44MvGz -%Jp2i@uukhWEGV2c7}iNWCi;j&j-$hm1$#lm)WW$ -_=Je(RWUp@Rc0u6M6lK`d9&>?v2QMK-4FEJBx-Ez3RV`!-`6brm4KS*RZk54nCmj^!Cs@tds{}FX}zzqp6b!O#br2 -n#f55AiX~Ogg}W(e0AD&Dhqm_3)*&XRqy~tLrlX2TiSYJ#P%|>60x6j29&#`zw~P)XF=%;e*j6%y8O( -1{+ZK~{*QP3{b2$3(`A-k%JgW1y1Z{m7@F7=+Hw6hy0uPEd8PMvI8XE=HwpIezr#M>N5GM-ud>(e+_-JFQ#FxI&0}aT%lUS77`uKbgG*gdW1SVOEiLGj9 -qXK(CQtk#@W9I1NIipeT(RwLB~wG!l;nAw9>GbqBBg9Yh6dc1?BQ2`_x=B#H3W~Rdp#(qgV7B-~zpHyChp?Uh>7m0pR5 -2#Q_bUV0<|xYub3E97PQEI(pI^$!a{>6|CxtJiOg?#g-sUQGLG1CU{WO->W3WZbvz -+b$r>An71n6~_(E$%;f4Yp)tA3hCp=7ka6=$|Yx(~8%CwZP@gadCqa66OULKerg$J#NPAGA3JK)t`ng -)i(zIgT`8D1ZwQ*YxMl-ye$e6HaQz2R=Adwd|7M^0pDv^+5Ad{*@7 -j2pOhM}A*ZbCPO;_<*8kv#=TJS8bSR8Niqexu73@~n`*JZY2TZ<&8cw0cYbGxCi9_Rqyd_M2Masn}=P -%H-dC=FlWsY<1~AR`IO2zWAjAR$0pwIAwV?f$`Mk&}KZmkV6)4$Mm|E!N_Aha@R{(dei%fVSaahdH7# -GbK!~n@>$Dc7z(0WbCF-Bs_}0oVX*geFo}S~tkE*2P`OVMmV(eGw5)Lq|GFIb)N8z*NoyntzCGElE>a*imz!qD)Pz{h?tw;`(tIcEnh(Rc;3B*>EKN1- -9+V%Ul0R4>TF%-XZRRL(m26_6sU|Pq~uG|{O}x#o5UW`cRS@2Hwr-OH_HaLCPDN}Jyl;5!DST{owG~) -?0IKQyZ@yz77_gWwJ`2YMO%K3mLjs)b)>ck7)3(=`Qb?s(fJe%IQy@&1tBKlhp&eJA8AX-FP -4>fjV0{;q@A`wF6OJ@C@ynI^o%1FAvqb{dmvQAx&&AGV6wxq&#X|GuoCpfMCD-Tg%=BHb{tJDu(8kXo8I!sEw|5_8ZUd -4HAFd8pM*tPZ2v+|T$9tH;Tv$4JI6k&1iYoaJ+PH=NBs`}!rlhN10&c@HE02WGURc>P}e_+j#0-OROp -Dustm#O^6*UP18VzkELMP)SL*OxT*xK;gm|0hY|D7T?SzkqFbaI-}b~JH3-Cm{%BxPZ?V}ZVVYoyd>h -_W2K-;#$v4xM?>Q(Axdcb9zAJ~(qs^ag^53yt7W1_0#^uZjJjH4)!O{7po8HnAv9jsLr`*#6{@htBOx_lMEHpm)Vioz%oq?d6!#Ou*&DYQ)* -NLez*<8^E!u!9Ra*v|Zu{PlJBOZhGvX{5j| -0Iw^r32J-UfZAT5=f!ue=#&+f%tBj3{D##DB55RAV3NlN%mt`%}f2Q?4&YHDPqqR_NVv)6n~WDXZk-^ -6uA2?O=JSGPsAymGwTGu{^I09>!x63O_e*-owS!!S7}~^CtUCL1|YQgx=yyKWT&hxkt~F1}O6s&_I?{ -o=QL$>V5XsdlgJs3%YnGU(;lY&SYj#3eFGp78{P87dE-|nlQJYmWFe>0E_DFND{_Vc78l1u9GH|j`z? -DXP$ras~5jf1WgRr&hq?GzUqf=h$@$tf3lN5B?aK0A5YJYPdVUHRm?9=9?n48_l9uGm@XYYc40mI6#-eCbxWb4iV -lvCMag496P75a@|9dpr;JjJR<43*~c4p<%EYo}VwsS6z;#3Od%EjMRtg}27PTUHfFm4%6O659gkBmld -*dN>qwtR|3?xa?BG#bSzx0ZdoyZf~?Mw>}BcsU<2_%|MpX^|+Ov%-pmt&hy@q-B{-id4*C)`+I(Agv| -!AKWOZwv+JLk$Uh?Um^3C)~UC&vU$+bjW$f+49j9pDh?7fD8^Y1VrmVE*nn50k}irSR-~ILO_;JwSPL -Eq8t@;E&Tx=j(VS|aGH2G2G`_HNc45ctf=${|FPqwpNQzj5RSku2*jLy_F5|}(k;)5`YYUN3O)vt2Nq -NJfh){~6DVyAAC8iAjURLPcihmWtIuBq4<+A`*vn+UrJGSF{0YPSUEV%qvtqGUO2mlwP{QBs+{e_#YZF;K61;k8ci-+d2c2K{7wx_j`Td8;&(Hw9(eO5dDB4VA0v$o0$r!($ -k410k^=-M^MQP}rFD+dJm?(Ue2?#{c&IvqblC{2x4X9C#+*h-0~959BF9nW*2fU{)s&iKRi-Uyomxt= -oEn>idn#WPq3u4yS}>jKh1DrpON?AE~jz>Wz}7jQUibS+cqjt6=499W;XiP~AY=VDs -}~SMqf6=yBP%>K6G@*BIbY_i~GhP*JBlt~(jFKekym-bUt*u+^xY~;3+}HsA5>L`PB2~< -XTj4D5P4EE)F~`3`ZWY^sf^)M(z~bt+2AB=F?qHA(kj~MJmWmc^h*LAEycV2}BfW#JO7kl0?qnu(;Z_ -dl{VXCuHbB$>L8Wh6KinAB8I=`oLy7jtgVMfn9JH6Qxn%CBs>N%C{rYjSjatsk -Z=d#grzsE5N0pT34;S!08nj26Af{pbXgii}{Ed6=~7+A-|72cp)d -|Ceo+%!q1`axHT<8Z`T)7CU-cq|vN3su?vePI^}(LD?>$o7oH&bs+3w&w=abU6V^R~ -fm_Fu>9|yIIH+<^fm}lYg&RpR?^E~Qs$*+_{g*FZCb4Rk8jj5ZC9_-9Q6TD(o -20ue8hjb)m#tt!XAhUBXtMZw6T4yCS1GBjb;u>?sta9#9J-z6TCmIiPfPgql9X)blERmX=XAB= -#$8XxadUweTGH{4Rf`+@Zf-2RA?pfz0l%SMRC&*J>lTh4K6$eW}3rKva+s1@^0u8iJxB0B*kj@Xk3q( -n_$~0F&D>6@tXth5@Z#1QghbQ1SEeO`lEG;9w|!1f*)*ZN?L-Y|E?8aJ}aPunaj(9T`sp&jr1*yrvnd -3;lrvqFd|=^(c}_s}-KIUIGrQ>Z%N4A@=K$U9?FsL2G0lc;JwoXn5!n;SZf0!Gv%A(sdEK^;GoH9ev^ -gX2A_6mFH*W$|Xnkz4u%g-@&foZOJLZ9Pe#rvloSn7KKE5US6PzkAWb!oxrNV&2ex@-gob*gmGyz5>QAYru+%rZbIm6s>)^wO*c{c_SY -i0A>oYvqXpTHCI)I+3Q6%UZ+CD*%UNz~vA+4MG=|G5*6g)>cdBA3uEjz}|fd3a+XF3{iX)$mE9lYxzWXH}&fbcwScHP|;TF#`AUrlm$CpTK233! -*Qg&sL@=I7icRxPF=KLH!brT)XydqnaB<;;k1MevwS!99!A$ONt37R&ptq?^K)j#TUH&`i -h5TWtpsHVdoZB1q#d&4wq^~OZD#ArvS*}?Kl~~|AH=~Q~EWh+lYd!Ju5#1>W?~onLvCO4l#u7+Nnc62M!7#J1D^3s6e~1Q -Y-O00;p4c~(=$C`&9L3IG6uApig!0001RX>c!Jc4cm4Z*nhna%^mAVlyvhX=Q9=b1ras-C28Y+sG0BU -!P*ZAP||8*0y{>1GOsZbL_hSaT+8!w1sU@D{>_J?T9@}bHUFCptY++=32f394nDz8#3SJ@qAe;zohN~MuUd`t2N2;F0Y7XWX -1JaJg)V|^OxqgVQUpl)&sJa9DJ8GexTa8Eu#k4$G0Re?GYn~&Ms-ktn+fmWVq!l=4Gg~3Cndfo1LEc+ -B=_ETJeqpe&T7tOR}!A2%Uu*Xhwu(G5Mv+B?m{K-BM08Z=!f}3BR;K!PtryxR^=4*`r`d#$%FXo&o#UabH>Dl}9SFf}>VnWj7PD6m8nt}o(5&>M -(G`|;$W~rn&eer_4=UUD0>G$LhTPX`MKRS|mo(h_9xu$u!IoirydW4n(f+s=~Oo8B!%dic`J`fcM0cZ -%nT9*kLP?l&Z{!jovund?P@T2v}vvl9Wpi->OSS<~3F3S8<5(7gf$a1;bOThCI;j)AimloQ9mxkS$a> -^8)GS#j|fvPNt@|YVzd)Q6D(L%B;;=U4Pf*FCXl8lvmh1Z}tK+C$&-!$gP*Y_&9p1$yck>!}>2(i5nP -6n808bpGA9(y-8H|0$>4%}!F7-I`kjh~8z_f&Xe@|3i&UGJ6Uz9DRgDbKVtOhifIHT(HP#0yFMpS2k- -)V7PV%)!QH?Io|*f;R7h3DIy$1@9iMqE$;!>yP$(`?uMgkSFBrUTy)DvqLttQnzgQgy>}(;%(WVaTeI4$p=|8!Ez~IUTvOI~9kvMYR~J6>`bIvCkIZ+@5cLi{%Er$_AKnA~XSKERZmQ$s+02{6&!_M`G4^`Li%=<73PY#}6l$HWi -_AM+&hui-Tp5rdMe4+xY!^lw!3(6RK6?}Z=SY7^AZGC9f|PerxztzvB?chbVa8jj`Y`G2W`D}dE?{C` -!mRgqh2S^kojcPQb@lC5>~)0FU&V>+AB-z`I>sX_tQ#4EmKpAwJ}DrQubsonsY01Un=;Ruk{Xh$gt2a -f6=)8Nte4fm%J-9YSDE1KlGZGd(`6;(m;@rjNCu%B9-La4I;hYq4s0w(IaD1Zs_hd5k8z~rDvlimK)9 -fjR;FaZrD&vqa_(p6y6z66u3U@E$2Wh|zcP++=QC21nT^aF7VvK)+NU>K#)G(Z*k1;RmICOnP(eDx7r -+llTof}pX{gmDgXl$wmJ@_6sW-`97i5Nnpl8qcQWszQF;dbT|ifdp+%xN@XTjxw}%_s1OQIP(I7iKWe -`W{~&|VrV;xVZLz^$*IaJ+KkBG5MZbzSt1jb>I9~2DAnAEhxX41ZC_jt9~J>81{en>kY>S{z*I$~ZgW -(&F*z8IF>+Ay!@g!`X19rU2K>!2fzOu*G2_gWb;S%r)GG{|x+ZwKp43kY#Lbc5wC2e=V(})=oJ~g7!P -Ukri-Tfn8Ygy9A{r`}FO)!?H#Hy@wMgw93jYfwefx!VFZ60(+#aQelK~2Zi*Fu-A-0XKu)DjYJ@6+L! -w`DgJg!}WOYXf)a=qCSP|NoKiDt8Vra-Xp~H{up`6So0n=8J+8!|pj7>U;n4WnyQ+iV$nk -`X%{`bvHO;GW(4j9mOKW=I@6hK3&-`XXK;dWX2VD-RQS7kzyJ7*gvSwk{Y7QV;vSS;5^s(TZoWt+co8!UqfGu)OH`sdN=iku -UJN!HCy*=P-T6{Km(`Rkj_0OQj1Gh3=rwQY=rli%Ae%EckVSIHA{&O>qwzde}nlY#g*sAA3&n9*#Ac^ -EKqmc``p-pZa+e`kJi2#xWp|GR;(_l*o7?JJy!P?I~>gJP8 -Qy$6YuxrZU#SQ@Pksi98R=KBFF~Uw@vN^x2eEbw8TD;o82uF)hcx}^e{WBtm->qu5MBqwBU7$rS0W>v -qPv0CQN|+bgfTeFllD7uiHmAQ@FV7=b8$PX6}469vkQyCI&i+^8vKJQ#*y*)|+XfH&PAKr!d1A-!xq( -bX*}`CgHj_fI)B4`Tvvi{5seFW%F)ZMz=iC;x+%C$=#-yZaCyxvvoo)8Pu8v+J@F -;du^9*gAOhXa#}^GMoCLD1FN3+4hMTHc|g}T!9e=_b@dL4>+01Z$Nyk>K_7@}T|-e(I8f}=()M6iw+V -3r4n3*08i>wPY9O+xyNUzgzJjyG0F=S7keL{%#p)v6xf=jlJrR*y*JI%LRhY(f-$>Piw>!+c_XbVl~B>iGJc26I7uOpgUjRA(%ppP)MavxwkX#Q5%4BG4#d%Qjbu&7jCeZ%-|9KTz*nX#qObuUCJ&ztlXd -x^NygxZe}5rN_3O`J;sm{efdcBA#Hx}OeutUAL?llKPact>vqGuMO&j9;N?(yK#f{XhB_P)h>@6aWAK -2mt$eR#S(ozoUKw004*y0018V003}la4%nWWo~3|axZmqY;0*_GcRUoY-Mn7b963nd6iYcj@vd6z3VF -mDi)9mn?V{NC|v9%Xae|ah9z?5&CGklVQX{;MbQp!Fjy -2Ix?URxys}yj4vXK`mg}RaZwWTN$!C)_WjU6fntv1k!4xQH0vIQhN0i0GTwTU#g!v`taeWooN3|EcrvX`&(JlKmC -lnw>6zyA4cOm2xY1)@r++AIyI>F95e6jZZKtlE=P1l9{({g1V?*{H#y{4LpWhoJ9>%+Z1+VdwMEd#)` -=VM*Z@vfCBh<=_S)#{yLWjUe1rc=bk>tb*&NmPF>J{5BXVMfEDm&yIC3W0)xsqCpV<)^Sy1%yktaQE*)zPn;iwYJfEuPwXyV)e{cp<|A2nFKE`aJyVq)(5s@XtPA1iN|tH2%Tr7>4<#E=CTsUxR;sCf -OreF$N9x25SEvF2&b -T2f}f`dY*RqX{4#VL#%C_cs5z}4c?5^&Oov84#`ODwROw+TrLN60uvv{faCp5tIm=^ON$s^Ow@2x0tx0rDez&aC5a)4jp4c*4~+)#7Wy6d?Um5{>ipN94a$>D=5+`h*}k -PC%{U{6fdBko4ii>#)B(71U{_j>&};k8zdZvBvMBp|(Ty8bvE-7D*nW}uT95n|tE^v9 -ZJpFImMzX)_uh^%eu#~5;O;X(JGoS)_xwH+Mq(PkC;gAccmAJCDrbr!^w5=ZczrXp|Pm;2o-kpKi5_f -lYcD`qJm~B(Khbvc=A5q -6eGYaICZYNM*QJpN)-@uIF;sVmjk`J1|`c=`dLx)ncu`0(z9A9)8O3wn0Ze~7d5^)WY_o!To4Q{8UVA -;Dla5%BZ6-bj4>7g=`dWz*Enf}X#Tx9?Tk9HU3?n)>#5K|p`0uB!T`8hLKI1435?kNxugb#KMoB6N_O -D|x9rI!532MlyJm?dxm!)HP+P*D%r;+c%17*CfL23l>@FgAvWZW1bnZtw0B+q48f=DgJ$0_pWjjZ5c&2%BAM6~ETp_L;6V5OC%>{7L_;iGD1d0xpqNa}ntxwv?p|Mc>2fBpIWpD$o8vvhi;0Fo}%lzA}SC=iIj130kp4`hzd{C -8u@{8}{z2-BOA7rKdO0R>xi+qy@%^+6hQQ#ZvFnLYVJJTFT@%oi|<141^J-5$i>h;}ED3td@=wGu#tY -h9>9v^9*tcNG6U6KNrIYXBNPGYe5SG^48u)s#mdP{d1mbIi0A9yg#MhVl(B(qu20$wm>&zZOBl*Yn|XxHK_S@epjGJ2x*rZ6YO61K!lnd%#N6FnsWz|KwD -XiwgB^q;&q{TzN-+bh=b$i$7R`qHdX4s;z921tj`O)_yF;lW-~`>Iz*7VP7vu+O1p*K9XMv&<>`&gpL -)9E$Y)q423h=)N5oe$&IS0K~0-qOzD^p`c$pMlPTWceNVIbAd5Dwa;O=ns#;4oLPz8hh7WQugPDZ4_^ -YHJ>J1>eA+;B1LjWUni|@AlaQ4f{{k->rUBkqwuER^H+xxv^fxooJ|y-+SWoK^oS!rErc?U+qEWI{k(Cu-<4_ -ugfsRH2FD5v1ot_Ike4+&k^C>jg+^y{qr|>zk;{m5INSmvnJY!$4FZPv6g-GGn_b^$CExzZDjI^w7vH%E9)E3)!qsUS(5qu#Lu?agG7NCp-z!zc*G$jAxWqdTUj7g+s -MiQ8-R}J6Ker+>u;ces%7ti974qb#?7X|zfA`oZ`yM)^B@;`>p(aHArkHkF!TxIyQ*%*+qzO=U3_+$w -G7zJ;(<)JYHw!qm;fU6r-Z>?9$9P1uR2PYQ>2g<8VMMSO#o^F8!(^;OP~60W*rfo|mnD -1i~NjjXbbf0AF%xT$qgtVLe>pT_3YG!~G#!H3ozj(ln593oT4Fs6=P20JG;kCd6J1*&#A5XZcyymklJ -SV@Zbg1(-2%LbNSY@!!I7(=9mE2XssRY=36Y#TFHb1xCwmX250{gSb%278w(d5+RL9c}tdA*O-VTPeX -PD!5o)8nu+)#DvTqWps>U2(Ie+XCluYpwvcVpULMdQS!<$Z@WUgU30lGae1q)lSja4EVUI{XcYw3 -}|&jXK8xi94RNPzI9Wf?&Oro1KW#SF&Tq88UF~Bqb5%hQ#XV_UAyO18@lKvaHu|f}=neO?jNI3JpH8q -o*}Xp$px!N#8dDMgPfm7h|C5d8oc%d7#y@oSg__7S3$X7Xg6zqY!?dOpt`kNo7(59d2Va8^7&H@dkiS -*sDkMo~6Z%Ucd#BY<=AIldJ+RsfiA6)g0oS@37D=96dyEla|Qe(FhRq6-j6{;q?3juRkzP -+I7t87ZodZU0D%~Q!1xHHNV#~c&}JiTs0U9=C9N}LHQ~}23oeK3@z&OTgpGgX2e^xi+{xyEKzT|mL|1 -{GG6H-XAZcNc>|a9ISb1RT_E74LhW*oB|NeK0@XVBK0V>kcnf*>&M!OXsA -=W0(X{rLrQ)&jdSNVbFK^`@?C<&c#6$ehkTyH};pK<3g*ZcLJ^$*f% -euY@+1pIYhZ_p3jUbw~HX| -%O_tiob)Vl$wG|DMdZg)?ag|CEvSGtKF)>!+41!rri)nBmgA-6!2AVj-=W5w671fxl>2o7-s4;xvP}t -(eME#mUr>P<4KsvKp(baI9z{dsL%`&-Vny;s4k&_DLt@`(POEM9kl;RQO2-G%#Un1F|H#GC5r2V3sb0n2uf6++otGa+8lfgq(&RpApat3m~&`skIpf~9|>8=P#Ep;mv;F5IDLANkLxIMnSC9I+W@TK!4 -OQ}!!SKvsq4B^|8%FuLIj&}= -FMM$*YBrc0UlE7n*bsD1wKm8aSlVM^Bt^E)Z+(w_w}C8 -8@PkJbt-hF4opW--o56n8EIB!w#mfKaQNLIi3>TZmp7D((_vQZ3V)YOU$rv+2T*)ei3 -@aczp&F6eQ<#U+fzB(k8l=rP8U`_4liMvW(ADE@sV{f&)gO4ob=Pvr;^(hbbi&768`@(2gsNJq(+U?{ -vA6!=%so4Rv~qEPC!38bn2Af~;6hk6LOHz4e*d7bLia}vHwt~sKcuwvpFL}g<79#{>!$#kOfZoT1Rg> -fcvggTJE|&n#@!0w&*~JC=`HqZmAQ>Q9ccex=z6YTubNB1z}GchFUvCw>^%s=kXNU9@Za7~Y}1ZUrmw_ -_fHtUq5@dD3M~0#Yp=OYnCl%*hw`#(nz|~0$JBljKueYw492c)?1C;RfKz1r#UAqqYKXO6@oN+p?aCC -Pt}bR?Lml-^2HzLLlGUq8PmIdyUGBb1LQK39?TQcbD!)vP&4;o7_&cIEIlxm%hMqLqi)1U;-M@V393U -1glGI-ii?{RwppRYMj^acSx1*vd{YF0+4JaL3hh71Efu3vQD6A`tTZF>4vkB`sj;BXM{|=NPH -*`3ipp0#M7Rj7T-8`$Zk(*XU-gse;!64-vGoX|DI+leF23 -G<}}=8H*2t8yJ=fXmj*jN(cAclsIo|dB&xH?IERFV?=!JdvPkV;pQf@2r$pjX>0y+y9ZHd -FCY-^)`GcGZB^8N;c3G5NbXi=Ofv-*hVsVQ;dq{EdY#3Ft+`1Cq2~mB6%YeMmT@hFgUl*j9gYO$oRS| -mWonw#K*M;65PZm#6aG7Stau-nuWPe^AxsGXrBac#qDPJkrz^LAaOU9U^Hq#G@k4;13UY9Y-pzn<7d4 -D0ri*_L3NS=>=pD~7$xq0s9duaXvn4LXV>~TMBa~vOF`6vgcq5JmH6;xQHTmK23CVv|fdtS+t=>BOaLe~}2-udusIowA6aoWE05Kx9 -LfOxDG65{Yo5}L%@98OE;>431g4n`flwli~#EYNbzJ2-PL$I{e={zo#SW6=B#y!*H1;Bn)I(mw`-_v5 -9TB@+yBKmCaXks#Fy?cPzI027!0kpQvw6;~PsKtlZ7sw -|36UA=84-e|yvE9|xQw#>ll60X}9t&~UJzgrTQ%mmOdpJXEGpoD&_^+kEVjkhtWE@uZ2Nvtw?A_m=rX=-C0Q~d1q#y1G` -hS)^r!KkV*alrDpYjl+0He`2fL{H_p)hhA3_Qu?eh3oE6F0dvNl(^gMSa-nw$OACs&}dL75AHK_OxIE -ZC$x7Uy8Z0vi8FwQmS3&gbw07BYIEIt_?GK}Z3&E(5*w7!qjQYwVB*_7cVceebebh5#ZO4?)Ua#%f1W;jvJe-xJpHHao -9vtTIR5AcTuo>2n&{cr&(ibQOHgnIKfL@fP5e6bgDydqxPj5Vv@)QWHm)orENW%z7IA{d}+kv@8I{e7YvPAuDeeampl57U;x!S1J_9zy=gg%+LL;Hl>dkCYyjVX#TbU( -LvW!1!XD64r4R$>k_6Bn{U -O6fWs9w$*jS&gnO&I`$?Pj@tT$8OjtOm!-FEJUqaKGE2HZr}qaGf}@obn<{J|_SG?NN$usdM1rpzZ4i -6_M}R1Ft+69myl@u=RKi075@H^f>|iOFWoO%mB#GXyN=eJ~#B?m`dU0h+z+SFPdff-6rm3DKQ3#_65T -)SE`XnJqak@WEn%UlVDz;MKH^*uxwd6tW3AqLH%XzvWmCQKWyoVbL%^p~eS2p;@Qn__vwJ9~y+*U`aq -ZezPR7Svq_lgGsbE%u!Vb95n`^luR=c&$*wGKI2F9kgg2OKLZG_T=fegC5=|m@Scde}4b()BD!qpS7;Cy15 -+d-{rNv?%gNp9?$OF!`!_|^Ub}hTQF1*%baR49!2q4ufaHJ8%~nVMXGyf(!-;qwb(yRT8w+k?;49tTW -|c7nOTxS3G>FbQgQ+Mk4p{-kKO*+D4Nck!C@Fc_<~a4yzRSE5aZuF)n+) -H7fFjbRhi>{`jB2k1dEq(=UpijVe2M~z?@385UmW#w;4ZA>?A!Ity1J4r$4$%s$Ceb9IZmQrCE2t!tiYHZ -jKcjH}BtZ1>oxb=r?B<^YL2$!q?jS753~w1#jz8?z@V+=l0DPulr3;-{Y$Y(XCg=cw9yR0t@>p5X= -dxP+ei+tFhCh*HPu{5ckCw5mQRm4;#nifQ_vOwB?eo;hmwO!5aG2Q8zr{r>|{O9KQH000080Q-4XQ~Z -za0;dN60AUvZ03HAU0B~t=FJE?LZe(wAFLiQkY-wUMFLGsZb!BsOE^v9pSW9o?I1;}5R}{1d+Zna`ur -~w2BIwR;0!%u=vuQoTf(e#WoLq^a6r$CR>R -gSDE|Y_{2mG=&_O^dV -IkvlI-IvzA%p2|$-(3Vyic&)Y&L>DsOQs!8PR95?gV<$>lxxm_fA$RFQ$7ekRPsnW?<%G|N?B+i;}rn?AI8R;0+2V;We?LpFyK -YULMHYc0%KiBJ!ZmzFBhV=3J`|I`fGWx~acRD`(vFk6G#Rc1ipBmd#B{OQ_OW=My9^WyF-?gBujoix4 -B0SuGh5`>i^1rL~hnqLVZ$nZI;z0H+XB2_<2L(pB|lr-ha>)}@DAVusJ$$xI@1!k@CeOkp5vRjx1@;B}Xsmf -D{DAl;sG(6ZZ^r#S$Ei?I2q-w8hdWzb}bw7HT+ce5t(zl?7N)S>2{O}heh`;Y2*0=YcJ_oNe9}fvqnu -3Cx&7zcP5;js?euBl8oj$x7H4E+r~z3nc=? -sS#M!i>3ITs$4!!*WIR54zfw)`-1r_l-~h%qMMco3tn6Yh$$O)N)Sv;4`Fru|Cd**%p%2`QPWxV0aup -SBCU4r)`@}n{*m=w9C%A9yi`mP2y!E`jD;_9hwd5dhVO2W6z;Y_UF$Xz>m~EjP-G%ckp%bGZk(+X}N8 -;wKULXCrTt1T_=|9wiX%dh{)JS7k}0VL_hunvU#r-Icgk57_(@~z}U$1+jF8?gm3J*T;Aapu_j@h+64N@o=UNhjy)Ak`q*3ZvO!o?6w)k~vwkpolza&qL9iBl# -Tk5egeqy6!K5xPOr)XJ0PIGhY4XJ91{lUwC+ra0^;d^(|2+iwfYY*rL|V$N}zYukpL=H9rc5v0t?*^;p*y|Y -`;E(28E_xOm0K7KDPzKP_cb63WE1>VXMto6|_Kx?V)fB{mPcJSSG2Vv55QM*@jP?QZrx(%@%Ksu1vTc -4l496X)M`6qHznr(X+eu?Uh6S7>u(%BQ?oNnl@;Q>sNgU0zWREw>IlsY -GEl$+Qslq!_ix>~3y&x98@#1RbR-gYRG7O?2|};oY6N?%ENct^KagY09iCgFN}VCLHAz>5!XnW -#=UvBubr&U3%)EEfVQREpG5pQtEEls*KCM))C#H*gZ?*27pYQXa_b=jk7yO>M_TiZQ-r8zQ3yMp(p-l -)ID?uJQDiyF48{a+6JE+izj({|18pqTLc5%C0%{`*s|hXR?t@H@FKdR_Mz@&7W|xpTSyFqknV2puAMo -TRFlD0W{8HCgjqE5Mf7j3{V#%B$<=B$$Vz^gL*6?pS@T5e`HJQ{Vdmk)F2a&U1j&L)`FCVr>^!8ocfO -mb{nmUr!2_O1TOPQl$HV2lj$ny0E8@j+iVG_o_t^aG*z8o^vhiyzmvbf~mMRmE^8HVRe4La!*X$U7UCi0Ac;0beLH79Z$Q8TVH;~feE8dAv>4gREEy8e -H{WuJEM;!QhX2Um|$exJ)@v)x4s4d<2+!N6=wh$yR1g@548o}t>KBeFBfju?#46B&j`Y!Q5KYV+zTJF -v4o2NLEq-bCuj`m45D6V)YJ|iLM^}g6-+NTLd$~RPBa;Q^ag?xz8o{{g5NQ3`u%QRM&-2q(sEwY`Z52 -P_9ow6Uw^gbz;eL02skfx#ddWD=z3;l?@dgqtx`>WeAJd)UaQ}^?2vJh4&427m#pV}DT0sfsdF}?n|4 -n88YK$Om@j8zsmJB;Tw=ZLdrrM%0x-;z!%)b!tNe{$~L30eD3=YzqUvQ3L{)(1{!rr=i#Zg75ZWY -z{pUusD3T*g1%plN{i)xoYG-@sm9wbyf`_D(mAvR_!9(f(p5{kvM>{{v7<0|XQR000O8`*~JVDK8iZV -;ukhD{cS)9{>OVaA|NaUv_0~WN&gWb#iQMX<{=ka%FRHZ*FsCE^vA6J!^9t$#LKLD<)962%MljlDgbE -v}s$SC?9P}ydrs#Q?d$J00VH%#V))LLhy(D_Vi=kI}1{=9y#hLg}7V?0HN+$C%&* -i+y%A(eAe!RW?=|`C^Wc4PSH>x2JRB$`p>~&hpo11qu!k9W;(8$n4^^y9!0X-y4UYCpAhqC#xX*R&J^ -K!jOn{1ZLF}-~w=jB2&Sb9L9?bpj<4n1<&$ZJ4SKYK{?Y;k_q7WeODv1;zT=S&Sh`U%)j0@V&ggrm6X -J*_u@x&%xKu2f?gnh!>b&P_VMhiNuc*4Y>Jrfsr3sT3gnI;$tk&19aJHGsr#y1}F=CwG9$!2w7bOq7z -hWF?z-@TaVXlSz@TL1sq>2M6;!t!v=cGEW=HOsj_F>@P4&BcAbvT!NfuMb=Cv!&>G`^&CI=HJL24s%| -DANHE%M)ym-YB5hK5GaC$o7cdMw#|4lqBu)pAP{YF^AZAAfywj$s)Zk-*xH~9R5*A<~RI8zZtQuNKb_ -fLs@p1rVOHq^!J+%*=5%9%9*g9Z9w4*`Ep>jxrAH+EI{EUavuR35Y6uAKY?E&xh%9dV7A7p$k-==vjd -)lc+^>PM0fsZT

ZFh-d%m1ym@u|3Lc)shWOgG+sn% -1@5OV`$4>5BBRSAJq%d)5`rlZ}000Ff1<1Y#<&f&s)l&t%cmQE2^hXbn#qGBp+=uKh&Zf><`?1~5#E`JLAnn=fIZ2Ezdg4OmaQ12<79nBMyoWXMSdpS-cWtcIO -E_AaeMNB8vrY>Qt9Xj7GQgyIpt%-i}dMs*Jz)ILT+$IxNOqo~FNGf?ka@7uhIi_< -_Ei*L~YzJ7cD!^z8IJMQIDfM}v}UZeJsV9PH+3)F!1gA_9<3z2TXD**ebDMboHBS&EUr|3tFQO=yLtX -PoROB>ti&IS7Z=bKyDlnPnlLQKUudSylP;zG4Bpzg>)0PQLB1p~~p8WsbBn{y|jfr56z$7O{*;0x>;^ -E+$v;X>F-TnCI&Vz?6D*T=w#g{(W<`+}K^!te>VU;SXXPU@!0Hp9_~7RDNJAP`9n12)6Kcrf~G2aAe| -H|=_)Pe-54^gnsSMidr^0_+X|LoS9s&_L#t@?G@rmY&UiKkNg+QIU|M#e=Y8BH^g^Mr?Z4SIT;R*U -+@+<4bwn}KFOr7=#Yci?;{ytO9ofKxGR8xnBiwoa!raM-6)Af15cB`lT&5zn)-fHnPMA=mh1dnb8)Os -6=7&1_l74U~R(IQ0)$DaSGf&oo3~fAiRz$+;tQB9unFSh2G=G^|4f4J -0>g;yO{K07>Z7byJM<|5#Gc66iPATo&P1DZ?9Iv{?$irVHCdh`iK;gcxo9i2s{jn>iIze(i8l~W+MaO -;ChQ?{4JIq7VeUY~)0nfgraAYt@u7nELaX0HrB(gG%PL!;CuV{kcKnYlI+?!$#I^!WimJ9Ys(c|Rypl -eF#|Apj_E=;Munus2kOj6z`oMz8TScJ8j}@DowJWh{t4&#heifI{*8)Tnbqm&|U<1;p6Bc=u$=XtPOO -rXF?1qf&i9sk)@If(a59XSHmBe}+(Q!ADD*@7tJpnA4hH2&Jky7BRl1rRbfvpvZvI%d(j;P+;i< -M#sWuK7L2o+wzSR2B?311nEfpy|cB1+>>`x=W-$<6Jn+n;YPXrz}%$7AvG=mdY -f#2-K4k6(U{Y~ew)+-89mIinTr*pB -Yf4_MEc4)zjS2@Z*7YiQqGUgK8W+zq3Lm+BNwO!Pb??eW2Qa^cl@q*4+ot3=t3#=d-YB8Tz< -hXbJmy!ZrXVHiFH2Czzz;h3!1bU(fQBUk{Q&yIf2o86-m1U+*DNUTmj4I=qrebg{KJ3gO -9z^1-h*%$)@5gUX8qUkiG|kj7k+0uanQkj=>%QbbP9u+{E~L6=$qvx{=6gq(5&;U18YJdfC*X^CJ1QW -!!m$}@H6P<$Sg6?eDh?JNI+x85mJ0M7Q;IGLUUQHCe3amL1+n$QPc@mtiWF(3vo@uEBqC_aS}df&N}x -H)UGBYH9#JOPNu_hfBa|S6*x&-*gb2#fz3J1@}h2bIqf50Z58zrW?0)4zx9z+9Xj!5`x3-!*@V;GR1* -NGy==7tG0QR>=n;(-gFDS&l;q`Bx-o-i;!Rl`qM0{uIr6rVASPht0@`ylH<756+(??j2929VR?oqJ#a -EeKPS#&N6GQl-{W~&25K**H31!E_^8%v-#RzhS47kWdScXxy$0Y|1+BLWk+5!MN;xf=?XS8Vx-O8VDmlVu9e|p80>ofSI5^yKeLjHtO8M=6Lmp?shUV_v_tWS_DHRi7f`o4cLupvMusoRMsRw*o3t+bGxu@Ixj!jz$?y1115-3xyr -swPgrEG0=nKcBECrJ&PCuG-wiKg$~cFhM5X3bPTKhJe#);HC6*hbW~$r>SoeFL|~tz>Fmsvm+B)`r?L -W}ii=?tQ21k+E+^_oee5o)jrU0V+p#N19F-^ZbaX2ih8l54kv1~V_lfq^4BcOnmZDwHfUmIIkO2eEcAjH!;Xx&|wxEARU5aH -|CE|U$gVnG^ivXq23Kld>*JWE!_(ty*uG4yo8`0@>s@-e7M!aAFadu38;-_<7aydk)Ka7mX_jOBD~_wN)4MuZ2Y9%NtC<0;>K -ApsT?tnfNH-Jlj3R;-WeP$q0asptX%Q6ukn`4HoDa;fSu8^e8<)%;IP9l}O?VLIC_vBi+UPDF@WOdqFbJA*isTW!AT3gS=1Xj&z9)vt}&qi@)yL)YLvkUyqOd_5}aa|bOk-5fw4$mR4hr*`c!;(GKT -+0YCdx`)@4z*z)BKLTKC;NIj31c)Rm=}6-}*Nl9b8{^AhEkVN5|@rp{tM9>?G@tX648>{nB&*9q$3Lz -PuS+2cCA$ih%8M19i?KLKt-XvwY*{DI8lCT?=8JJ+m$h6>t9{6-vir+F8Wqbw-us1ZLWKmU_8)f&=7QIt)>yh -6>0ll>wCE*3S|E)SqEFvX))umkVLDB~0JREG`?^nKRKBY_-ad24b(TScvx&Y;@G4O0uf6^15S=_Ry7x -WUrSZI(N1*&WD@Bz=l5TB(Ce(YF+EiVfvx-)Kib(m?KIOsD7PK0R*eh*YfnHyBDV6c&?<>+7rQ$@#li -H=O1fUS26*(iZ12k&mR5;sBomL{)A?pvG=RG^6MgQjCKT`! -S0z({@DMC^tE`Z`?pr1I@qlc2Q_($lrbr2oiMZk{|vYH>^ntJiSY4ZGQyCsFpB@7Yq`Vnb!2>iO1z)3 -N{=rQLv>{%q;C;(C#$ML{;uIPS#h_Sk6q1#s3b_sQODFo+kvLAU2YH?L62ZL4$FzYC548P1Vfn9R{O+ -|RjlYy#H#7Ci0;nTrjV!PrwZV}WTQ^o1+=LOY@=!eO91fWei5$v?&@3YWGSh|U2WG~`hS<>DMt)_icxjd%l~ebcc6nD(3gSz5LVri&JsAkfbu -V@(?SG?qygRg1I^sv{{7ip}C@fBXP9XG#C$QwphNY!PdX+fJ9S#bJr8gJ@Wv$atsShUn0mm06YdvVa|_!c7KH59y{%9Z0QF5-7>dT~%%Y;jJ;|(}yx!uxOL|*#tn^ -Li-F^zVp%amc4iUEP7an5Q2dh8M#}WuSC%!IQfpSDkO^pAu3W7)v~PuIBsIZeWUCki&uOaoG9QL;A0o -u#3&q2yF2I*rA}9+ihH$q{x7B^(qc!Y&{+FO6%QHeiY@lx;0`P8K(A)8IL{N(F%G3YMGyNd?oQ)`P## -8B40z1&%9f@D{L&jcMakObGMnpSJg)TUg2VD+*Z|Q0LSAv2l&Z#4aaW=-u?(5S{PgPPEWD;H_pqfeHR -$uhk)tRhw8RpAUUi`jBe#tzNo4VXsdrXa08QB#Pu^qq&orsWzKJy%puTetUfy-&=3~=^@^I31!xdR!- -5EdhQ(3g__k|_0b(W_U5~&h7ef6H4!eJYm>xZ|cNEeF?IhEQP7hA)7ij~S9debR!W;*o{oK79HM-^8B -fj)PBgoV3q=XZ`UHcGA$u>|Eo2b34g0SYD5qQ1{?%ks@tIZs({h(#%DRC;T$&M>T8lX*f2A}y5uF;x< -5Y%kk9_rRIC?a;B$@%#I#g7NcMWw3*Y$MJo5)VT0jv#2B=c{4Tg-snpVN+Z>6-h>L%b_1qljnosyN4C -_L7SEUDJDj0qW(vj=Te_}485QFK!*R9k$`&l7qES`wNmvGt^9BTFMGIbHS>1=ZCO*uEHRUia^ymEebg -ItxId}}OKqpq5#({ySQ;-s5yfQ14TnDZB_J==~8#TGw@UZk?RNgyx-^UQo -Y{E}Gp6@NJ-UH%M?0}Z9K@eKTGb+f*e>oFJri!a`;1WNhQ)KH;VA?OsYW+ef%lWBz-L6(UUGYlU06rG -d-Nf<3=10dJrPC>3({lAoZPY*D*Uth8Z)!D7Xk6W-!ccQOaBvOH^ciTH>v%pNdcQ(?@E -OId{~cvR*IIxS(bMqDHqmU7V5QI;A{Ypu1N&%%Xu8l<$r{Vgl3NZbQ1EKR78Y3=GR5?9e5ofdX+Ham$ -^uQQIzg~uOSm^L68Hs}s*F7eiC0&)|`q_hMV3}v%aUf -2_H{Xck@1s6MkGt?n9~3Jqb9xL@TWl}VhRdnd1e&67sZ(@V?g&O-lUci2+f-Z!W)CUT6so|Trw?Hmg5 -X)fp5v}Oy?=x^fuRV8R3t(%6_dEWQYS#1F2dM)qatwZDxq6$a>sRTD~8Con7813IYD^*pqYcwQru9vg -5SB?28T)PZbpZ)!E4y*8fV1RgH~Wej0NZo`TvlQ5|gl?n$7MQwYo-OqTbM2uS2^Mg^|j4GN-gtcR_=z -Bke)WdQSkHfSOlbOOP)(p-n7W!MQ(Or>()WCm`4j2t^g6kG<6dzoX-jyhx2|z~weko8C}6{O5T+79eB -ad}D6Wy5IKbh2Linc)!3CPK^qE-UbvvHJy(2;z*q(UGadFV$=ZlmX}Vo<7Sz2Z8WuWs2DdamC{vFov= -Wg5B6ftI>q8G->;cYy9fCyj9$c8V$Y>jcvz`(OV(E(kHeLbeMx@$kCT|Yq)u|5BtLXs_MMPRylL+om% -~ro<$3JLT>?6A4NKEIoZ@ksr;F-#6yh(zR7NLH#U1r=I#VKJU2A>LPP?TrR|9?s{^*f=-9e3Kqa1Pej -Y~R0TKRUPK1Hf$#J4s0FlnXAp8S{kTxq2b5&Eyy8PiIgAq)Z2SyTSyzlsQ)-@sd)=1u3)2W-T%@hozM -0a>PM=jNmLee2h#4B)_Y81D#P1C+=T50x*v_1KOQT02e-#P1I420NF4lok3K|CFFa)!eZs*XZIRFwX!@ -ohXN!GeM9cR@shfXK4+X4r&^TM*wa0RBsjwR|SQAA@G#}>(J6l&OC+VqNpa7!;6z -0@Pay1T#^Wbrcx%FcdE8Iu>X!h{z>@R~`6_0&EAwPASNca8<_^$KOk*c4xDqAofUAkfE&Z}$#`@7%XN -wP#Q_31?3+-cCBqt_tikvkmZ+oC#6DWUmznAPC>$P9#64e5(ny63XoLmL!$`)iw3cnb(9O}za@-TV^7 -FhXVN3ojq@XiBBxXaC3n(f{zQW^}@5{+V5TgzZLH-+L5g)zKc|oP9dAAU?xj_(u$gY2^6WaVKS8DH2v -c*SbI}$cuRLpTqW5t01^_;U2ZObxPwSh=0k8Xto2IYC1QvbZ#@GEXc;1T$EGGC`hmG-r3@*lDb_}=1k -X-SLv2d0fB*SgHh@@3Lg^lys-UexniqkGjMA?+YPuga-vV6n{!?MfkV8m5%ax=c-SdhVdM-id$wn=Q% -2gHA$K}+FT8J61q4hn5#J!)J(TDiiSG?2sSJ$%N^Yu?5>QzIeC#Ms)e3*9QsLR)SD+D&N1u*tm2dp~r -*}>ti}n!;bp8Ix75mThfewAl3#XOJUQEstJf#_kd=eQx5>WO|i8z-^D_wyIx>J1zlas5dwvnrjwI-Et -g{Ls~Wtnsb!~aR0F6G1|*&Xi1N*A3W>37_ub6}o~Nd1*H=aA2@-@O$on<@)iV}Y<@yzISlfi`QT^4DQ -YpI$yE5T_S;`pB5!lNUJVe!kF%T|Cb^!2VuD|4E#F5|OZ*P!Jyo{Y?mPKcue~#D6C3#*K85_P2ba-=9 -#T^5kliy%i{Fv9|yvR)6yUl_vj`DDK|+2UGugEw>d`h%}B(NEbgMgEI6__y$9sY+WPEA-Wcg#=Kl*b8 -j=Yo_6dsv@SLc|M`LFxdZEDQ01aN<{a@fI07aPJ6GJC>TZV!W}Q)1FA05*82V3(b~8PF|L8-6D!Ovco -)zn}8p+Cxl6t9+<#C{nqjiO6HuANt5$mu+xcU0jsu6uR(2Os!R_DpB_>xtJ2m0)deQx(h3icxZUc|7c -eP8L`o;uF3eOQmf^j$9LZWt?jx{9IFca{VJ1@1<7An5?hdMy`NI35-SJK=+*FM`mAS>FZjmhzZY9z9O -n*=agXn*Pjy==;taf4}rB#EyA@AAOT*kdlp#NMIhxzyx{Qvv|Qzlyrs4-b@WQ=_G^qsdxN5!viDdDwv -LP^3(38om)-Li9lV%ZH})+0GYEyCd~*S|G^GAIjl%f1EjG*8bVjgAXtv9-x4-r%Y3I ->W@dF8fC7j?g?rJP+e^#}^caObA2#_h_*9VBc~Kxb=|}|4tG4ubgrw3K(DW6oZZe=6L94z*R2QFmhI1v>q{c -TOS3U3dqRdm%TF(kKPR2b+V%v@yzpb^#zl3TD_Cybe@$T4(v3rB;*$+!&}{~^1a%5u6|O -{XSnXy=L4XTB(v2V@)tau)ZUQQQE+VI|SxL+WgXAw|Rty=}k$BF1KOKFBKdEb{Cz9~RAo1z=6gaw1=Y -Q#igjGNUqZfi@p=sDX{2fo@`rZ9r@gCed!H|2g7}9{A`O5G8esMM){1;G50|XQR000O8`*~JV000000 -ssI200000CjbBdaA|NaUv_0~WN&gWb#iQMX<{=kV{dMBa%o~OUtei%X>?y-E^v7R08mQ<1QY-O00;p4 -c~(;@gXtBu0RRBK0{{Rq0001RX>c!Jc4cm4Z*nhna%^mAVlyveZ*FvQX<{#5VQ_F|Zf9w3WnX1(c4=~ -NZZ2?n#gjp6+%OP@@BS5oF5SRlLoR_p4hwNY4&4N2+a5|WiZpAB${Hb!o$kM{WOf^88+xs89*@3xdT% -U*D0aPxFpwTCf)6wqjp-ewi@*dL85INf2pjLAcAaqu=q3}$4d}QmM1mA%@Dvy*7Db_P4<@$Kdz{->7u -N-(Cm@f(A;*|FEsw=F4{X@VOR0;N}K|KX6a(@=Cnr@>i1YqW*xCO?#VlHoEMPS2JK1{aiO+>!y8vyxV -=-G__d6@fsIpx@L;})o{NOw>Y6CpSQ6Ri=8>&rvD)Ae$Hv}>-Z=1QY-O00;p4c~(<6WeLe=3;+NcD*yl}0001RX>c!Jc4cm4Z*nhna%^mAVlyveZ*FvQX<{#7aByXAX -K8L_E^v9ZTI+M$xDo%(zXDYzW9dew^O)%ePjww%;^aJ+T-;chn@)Q}LnI{OOcFeTl&yB!zrDM75g;kW -Zd%PGrohEwAHRJ7&}1@+mRl}KA+k1Sq^iY^XG{`GDj{-G(2{OgO`0ujNGsvRdm%PJcu`g4vfS{Joyh3 -+%jXa8Sd(ta4XbxNW#muWrm5ul*;$&4Hj6iEQk;t7j8+v>^UgAn%Clsl0~;#HjNSec1BhB-N3y(YYk~nb(~41!6Hx*-T;*Mq!bNk9wFHZ6d>tVAjMkLWG~v9fB-p9m7Fo@#Ba7 -WV?@1G#m -O>f9EHMXs5ZR*d1(6j?`FgL=wH7%rL*2os%F?&DAZ)y+MSgp0bCrt9a+mCA&B0VmcHxPi_ZOGRmz&Gv -=F^*>E|)jSkIUuthpQjRIXP+SmYqb=>zG`vk-x&9LL0g5z(wBW?12O0=+P!zn;B{2Yf%=gY>3l4jwVoHB9)7iWZgDk(j>77m#(D9+lD3jdlZ ->*F6>h-HXDpI^YxzAC47~JV;r-Gl$o$JBvWN+wCH7Bi+b+9{-WpVfsgdzGi&O-M{`|^Mx-M5hHh>?c; -A-1U|d`9FTY-oma&Z2uu=|gE@i`LM#S1A29w(fD_YQ#WvWO60Oz{&Nl0X?-(W80GRH -Q^&5}URmqJqnlL4d13}x`-haA#kXzQ@Q#nPzAzOB(i4nIRaUZGkL-aOAqO*s~$a#$||FWy26)@9Z2{G -zGKJl#>AV-AWvBAZj2h82~vhfaVn2ecZ=uXD7`x&^UxVerzpY%86t1yO%FW! -8UZYZV~f|aM)rAC9)&2fJ!W -f?zPv8L%WWku*6|G*7_Yt76GMF_8`)T>a$?fD~0hSU@+_Y16#{td}CAwkMYA<&DM}2pfx?~Z@aclk;jvH!1qvs@2EV(g$ -X%!cvCR5@R|kai^qTj#b<0doY;pKOzICs;f#*ZK0!fHH93BJ`R?M&hov7Y01;Lj;XS;<)V3}a!+`q1( --bOg((EgypFkBevW*V-&3}CvOg=uT7m6PSw$IDM&mL1`D-`TvH+}+y@(O(n%rrel7Lt;HVHGk($1P_j`N3tKiMMx~ -EMuL7eyCB6Zc7uOdkQc$521;TcrYKtwI{3xv|6zU#EN(v)-RFX43-rM7t#;a?@4LQ_%wNRwKmRo(qe^ -Yqz?2w9*+KzlNY*#cVm!{!9(~dwvkXLnohVy$K69H2gNv#8I~&d(EiVW2ML54pFs9>metBzTx`c6-u} -swwthj8f?D)G18`vJY`pmCNB^u=DLu{ecO&TFcPU{VuU`>K;@s!QZ2kJkq?%}7aeYUA^E6x2H1qEd?& -cL)K(lUSoS7BOu`Jz}US)t+SR|jW{;qYw4qQORPdd!$_#eIokamI21m@ror~ocopLkm{2Lo -1^YI+?H&+h`+&ATj#MMEyVLSe&3&-P1+pYUiJDOn9oy7mzh?!u -zKeQVKonFt$b3o8@TPO)Tq&>D;iI?|vhV$~IDP@gF0@6?>J34nY)zz|LIqXUBjQj{$7+OsW{XY*%agq -zYne7+3K%`jL(%zJ57X%P!gV&)Ko{=|Omfoq|wOh#Y?+5MSisEW7IyDiclj_Nlta5BGF@#Ya9?N^;g| -?Bfr*5H1SdiB*UJvQv2nmKJjn2t6kWUJt2SIm%}?4 -09cOfj!5CR$zFvo1CiO>MqY_*!Bkc4oz+)lsRKn0;p6l+fRdO!v%7&i8HopH`B>yBL{Bpo=bB2JthWc -^AkT7c<@Q%$7=xkA$vazX$$WLHJAuNm3gHu>rKF7f3sq_0pt?W5aN2qjfB)%=- -)X8kS?};fYxwL|6cG%uWv5voQg56djA5N5ScBcSP+ts&ccnd*Xl^519bHi6dHss>Q~q7;Mvtp+2?LlUbPPLE9lisKQ2($*>w_EFWL+Z^OlY -_0JG!Dc7uiq}-O=7re9AFU($hs)K9m$rEXnnR~<%8hqBA;045;x$(Tm)Bf@VjjsAu(bfQxoxWsFw{3a -HG6I`p8vC#qQ2{Jv$a|KuB$#;y^I}=$9F&~*+bKIK)?k=A<{{ky^ABzID>hW~Nv!IoaS{K3_W -)O)rlHn`ZSTmVkekJrbrN_HQrZkSYRiyRSDGHB9!$Ujz1`hc_Z%jn9X9iSqxk<#odH4?gOMlbuTu8t5 -tdHU_8=>`)^K;bk{mby9n^d@FrKU;&felVy?-%yqVhCt^Ck|nKHd=EZ%yoZ+T5-;8yuP#ia5dR0eelN -R)oNCFH?PY>c1Z*)FZbf_H$&*y@8-_bLtj_2BN`+8#MVTTt*1{3BJn%6Y1O@5k|;X-!kultlgy7s0PXCb3Tujs4E?>(kuNC1DV^q4^`$? -9oMoR`cM@kZd?HDRzZ?0|XQR000O8`*~JVxW_~OR0aS5x)A^XCjbBdaA|NaUv_0~WN&gW -b#iQMX<{=kV{dMBa%o~OZggyIaBpvHE^v9xS6y%0HWYpLueehYRPI#8NmpPnU<^U(CBf3PXk7Fm2?Sc -AEjE&QeY3m@)C-?ygWSTe$bxhO|Dj#?}1b*rG;WKQLNZP$$1t)%nK#hx>7WO^&? -Qh?qixzA(bG-WO5;8Md8KKEC5OA98#sq`Qc&A6B=$g-)1z7d458b!eJk_!c7EqR8ANA`O%Y36Gp3Ir+bzKn0T8gjF^rRoxAsF>L0LS=corpL{67`Bt=t -JTNrQ(hD_R*V-P@#d{bN#c95r81Q4TbT0)9O*koamJ9VCmc*BExu3>{0ZQnw@N9zYUnHCLy7vpr`^;PKpF-mSwP}AgDe+0nfT)1vJj!;W(V(az=@yK{tz@3a%+cuZn}LN`sXtCq6Fljk(sI0A^@{LHzmCC2&hEx<%MHBublL94G{9Q(NCL>>L@n&_wV0!q87GTU3ItF{U0{ -x!!$ve{ykkd#hJ9^|0Y0C#rW<)~%xw<*2zxw27I}d&0dJ!DvM>$I|IKlWnG9N7ybey$7@lTRs*oGs9& -;YID(rK{B{}R1$&W+m%q38B@M@d+P;ov~vb>OpuF(v+ZVf7h<~M4R&mu(;Ca8yP*ZLwLr?8&?smf>98 -`f;pqMXK2o8{w&a=^O;K{0l6z`3mNdq4YS;V73_+J7$>V#f9B+9gm98`Qv{ZlvqJTPOnXWs3VruI|z( -g+Xku(X1u!|~>`V%Dlynv(50&_G>=(^ha-IUS$S|J6xa2xC=vEgC+L_9_Lr#?o?|3S$EvRD*G^HPUVpm2 -4w~%LDPP}X(ccJkLAT#};ou7U#GZ{h<`;H5b_Pv5#s& -2U25F*2NLZ|V{L*%f8-Mx)X6pr{BC0Z&Eo{Bp&e^eTqtE*oUfiO2en&zSF=yU{VR+|tM$;{>ybv@bO8l1Da=CwQ$NnQG~K}QX5BO>`1&h`BKS4MCm6PD^m>o!;J7xGV?cDcD -UoQc)G$6xE0QU%vZOgczjaiKohwdBmeFEMM^USlZ^AWtAX#OEw*1{V>2=pu>;Sy!)0NR7sVS%DCzqGYtFtF+%7~slg~iz9a@%@$OV- -Kf)%o&jb#eM+)fz$W8g=0I_n>XhyLZ|H=*OkCh5WxPj{i;3+h2MAZ4sQ_%&`tS;ftx&V}zd;a6Frha@ -euJME+%VG*05(#PgcMCFob1Y>fnSs0GKTO!o}NQqsP6z3H>L?0qpiW$V~pw&l9X+I?(0={c;kdpPW05 -rAm%0ncPgU1WmdPw&FSMgRE3IMVgJ&lvOjr2EQII-d03gU8gGCyxsggS&~bGm9q+M&;s-JC!i}wWk&< -Q8|{#ClxA^`WK#g3-!y+!x)8aUvGPBsq?h^KFV45v1hW@dG76U^`jptBlaB53|EGuE=7ZuiJ-fGFuJl -+ZvFvKO9KQH000080Q-4XQ((E8UV#(<00cq+04M+e0B~t=FJE?LZe(wAFLiQkY-wUMFJo_RbaH88FK~ -HpaAj_Db8IefdF4C(bKAJFzw58SDDNgzPG;=ndUH>$I`uhnqiJIMS$HFS+g -$)8NKvwL@BQ#T+}?yF7K{DH?gHxf`@Oq&Gq#P3HJe>cgOj|>GN}r-l#1Q#inUDHE1zA;B#~QItVJ$Zo -aTip!y=Yx&f+E8$&zhlnMA!Lz89>J>_Mn_xnqbq6sm}qamWh+z^Q}tN)}>>C|M+yyi5v~#0$k$41kgB -y+~#80v=opEL@B5K2BG^r$tjCB3urDcofA*5Kn*mBBs;9N*|D`yILu%oD>Js{4=55P5?;g)0sxj`TT4iZzn3sG0&)Hf8VPx7g@!WSXz?_q8_Lawr -zle2hnGm4ED{;sk49|4^L4Ms;P2mnN3xR>kXD>kIBU*bc8NLACzt_P$3mSq}_=Km^H -0tWZm4`o`Yc;Q#L{W$#vWD&$^Ap-+7*BU|Vu_lb9_Z9*iMUXWv#0ul!KSp>L@B9i3NoJOz1|fBoS -m^LwAB#^XH;>AD^m=RY6Du`=?F9gnMEuv!0y0WY%GJo*4wT{N``1HmT6h%j4M$)I@s~KwjmTIiq61+t -AO*YxsDZ?1kHgvusn{yRhCQOlF21_QO*|$PK7A(65?MEtUXgA&rwlM_+wrUPB*`}oxY!dstfR^c`)!v -O+lW1k*G!ra-epg~p(p9061^lZ?!%5mt`-E#vM7}Z0&9_ck;| -kkM4;b$J@efJWgq_Q?Zr(y`OEK4|9yYdU-Mr#yzmzwD~y!BwQDoWd`Dw5BZ2w!bX-0zZ4m|+bztiCD! -v{&{Oqo;k`oX#7VEUtR2haFLnsQe6&$ -Jc5{1ucYSgFE`UfBT)dl3uI_rhAb>~*UNvT)`W^APKV*IJrBJYy28g%%{<+tChM^Q9;g;`m%CmC6_x5 -}i;MXy`Q>8$d_dL@wGBYt5yRoSgZvPnss|83Y;|k(eA=C-vYOquKy})`I1h<^>%Q5>037vRz`5C!>kN --WqdL6u<+`YYiGo%?E%s)B@-PN7;nJeUijt7aqi*Zl!)NEXCF1)v0BkoApQ? -FryhgCv^r8HCd2!{RwcktY7^C`|_D>!>o5+H^H>Jx~S_-S%Gz5JO5hE3S(o4e`t)%m+l_?yY(wgrDX`Hv6N+sT`Q@C5#C*XZT>^j$E$1S$w -HKzwMS6+gh5VY1>3(uTtG9)|3GK=t1JkUiiAaZnmQq^q0U8w}9RtwU3)B~whnAcqi84E=?YSBm02fCx -U`PVXkckMFK8{xX~XZ2~KwegFE0?|MC{;#Gj29n?{T9NoM+0D$KLvQLOi76!@k>(8J?2&yn>LaV#95v16tJ ->ngLXAos=8WeUecbsWAH$DR!y6x>B?^W0YCCy9U -}ow<0hgva6$>L0-96P~-ZlG~ikos4LXtsJOrc2p3hvZ$f$>aev7NRJ}ywt)RZq-eA|#^W>pZ2o(i%O7 -~0aNOhpN+uE1ur@*;NqBu>^UW%~sWQ-98wL;wj!pYRJp|hWdECYC$3V`Ty3gy@ECO|x6Rhx;O` -e)zKmdFu9<{R_}ASCMId?zC0}TLFryVPF*GYuR78Mvy5GO(auXHa1BE}X*u*^P*1dc_uy3c19(rikCwuRn6nRemnT0k4;j~D*Wohl2-3SKMDs+_Z7Y*M$T%{)5ZE8Y`HkPnlu{_&~ozwjZ9lzq -Sdxd~|E4$|g>BY{g|uIA~vgMC>1BlV5xpHGQMEifB>?gTd$p`nb(ygtvvIEI4dTjoeotGCxN`!JB{O% -OmRmGGx;uFhKxQ3nxqAxvO@BT=_GHmIgI58vaqjIBmW50ordy7P|sm|j}>Hc2UU1b);@7 -)k9Ju*E{-`(fRolsHTPG~eaE31X1(EhRRdyw^i^>L|%kjIVv!>zBpyQ>wkt@t6v;9-s~SgzwayjKC;xIil>y1||q&InCd>pF?S#_^_vhlMBrA3ML -9=49pAVy?7eE8mI<*h66-auN0&dC`JnRzdqMSleocOtYT!OnWf=zYyUX0xNaT_2EIL^yvvwH)N19*!V -_gYCKdjCjmLglz@zrLoQKuY|Jr5E$0>HgDBukGv4Cc;l%BZdaZ`*(BYHviYi}%-D-;zfasTy1d%Rg=U -1#s2Yq&)-#3FNz$TJThamKMaq&Di$d)`u{Xk?(0YYXdma1<**gq}N#RgGTC$Ij2Y?SgMzdKVH_|yb*4 -@U^P%5tw{aVbkYIIE?9CBaU?s;aey)=5wZVv`j+NGtNfZ#(Cw&+bBX-bG8kDfQu!sMa1<0A0=DVRO0z -@@J6>#S`T3ZkGuc4ejU@=k(VBW-SdnJ-1s6c@GsZOB6tTsQCAi(mp(Su?}dg2UVVz8{wc!C0T_|$iVz -f5?qTWkt=`8RSGI^C89MYm_T79ETNuM!Xd>ya5UQ@RAMO}5w~(u+f<;fajCO*ZL>!*NaqN#g^ugPCwF -~QPLkzgF`(P-NcuNW5H+SdDj9Ux$i9b}u`1KV(?O0q)|YszaSL8gwQ@_N#CS3N66am -I`tyMdyP$M54B#0NWY8e+C?Qj8p_&r2b2#Y)9%JdluaB@lKpL+Ps62i6Q>PNJd!5(=jF>{tmf) -3pU#4!#K$(~dGT;-!Zpsn7`7&bFp-=#sB^W{w!4B!^7C3wYkH8nD?E))Z+BKo@;A+8FT5h3C5?BC6AO -vQ+Y}ZkK1dwmVMS!Qg$#RJGm!WBBPRxdMP>$jW6kn}G6bQM@$Jpg_k`1~vQ%}ZCuXsO!d2fPR8tPACcy{V>S&~R**O)!UtmkFSuHPy}fvGzr`nqz`Z&Qb`g%+Y3kl -5WNkF^ry+2VD|RI-J9!=SN|O>9V7oE>> -sy+N8Q9@bf(_*mP72oXsQ6#bKGUv@Eu^z9NK$>E^Uy2Ekys0?&0tVFI6agi3!Id>|pc -z#Kqynct5c8U|o>!W-PuEltX89-sL&KwcdA+yz7;prVfX}jqYTS8wk_(t{FH_WZ|_n5Ki6!=-wSLs;f ->mAk=Ov_<4#{U&wd-{LPXP?Z_{~M1zV2%Hy+_rMayq1;rNb`0)pKxYbcqc2vwp{>aWSj(eBh(MC|Y~R!ddoXzGG)_zL82OKct9q%fpyB8ms<$ -NR)<+==o&&9L9UguK4 -X{DzrLTKZQ+rzdwWaR8-aP%C4`Syi0WcKZYA)pNme1Xra)(`Ps^D9z|NlF53RDGy4TP!h;E;;>9Y%ZD -8d|a#3Q@@!34jO2DlBjJF!(@EO8O1TZlTjQ70cayuHZzjR{>X1U{719hW!m!n5%WB+kH005DK;7UdcJQ@1OUbx_!n1$A^xu -u$9YoF%u&ZN0?|@W`)92g)8{@`PqtN{WFetaw|CU^3hrK@tNj+nE+hH2VI;a^@EcvGQ6~NtQ^osy&JE -c|@99=#TV%$tH&8=kCvBO>zl!n6*;7Q&lDb1i7t)~)F1n)ugcFxfxd2?<=N)%lTD`J8DyzMcv>cm~lJgEQfysfwq0Vt>tmow7oxB@cz|neW{`n9plEs -}%?zb#s#he-`E5=zYB9WF4TbS^FJNe)jHh+QH8)YY2-kt=pVds12Nh)--9hs=dAu{W_@bI5*lyf6|9m -#jfJapgHFKb}56xyiqnNR41Y#qpszi;ny_RM?(!kAW{qEX&i?L*B3Yg^os+;AG|60sa}sdtPZAuIU@L -z^Rdlod*wsiT7ERVckmMGCz}3w$Sfsjl<1z#ggr37Dzs3LwygK`)?B%BMhj~gXKOG~+f3DO&F~xN^mC -##6rLF$;w^n2jg*?wt(?#RVMkRzRhJCh!|#1^HFSXIdv+m**hKQkazaO)j=6;#!_yes@A!g@Rxg-y(o -2&mJ;iO@x0v82`00m(ED$PO@E9r@ADf4RsP>Y+T0Gs%Ytk&o&FqfoqITy&x}LIx`SFXVaUE|)d`*2?r -gWK-j;*0mE$|QuvRd~jsjF}$A`vJE*E~+G`r(6?#J$)#mVm$K*-90))n79WX^NZBX{@ySdKPIz)fVGn -p_&}j@fkZuJB>*>JKP5UD*WT>@u0ahIY9@aDIlMPm+;LZ;_O8*s_=Aq9kOrkNPy%t-$sOZr?3`5BV_O --ORFR>Z|#Y0?5Mus;apCajdJ`2oR)!?=@T#fHEa#~xgExvEY@r1UcF>ZA0`{-z`|=VT<5+x>b&V69^& -sFbs16*MKY+`Gy$Z!CJm;v%RzAHtZ8ozyC-Ywq*3?sOWKhKn6%rggrLv3nl+DOYps>`;`CFJ!~;}QJ= -XZLXPM2My*c}EbWF8o3y?j~i4$}|(Zb-Mw^NKlU}tR=f?w5ULTs+8A1{-SG%gMIhnkmYdZfXx?<4iW2 -Jm9Up*AJ4^2E;4<1M9Tv|e!h6_b%q$2kE0Er7%Oy=u34{0yS54A}fqpqcV$2z}dHC>5kq{~G}ZYkL}Q|${u5A30|XQR000O8`*~JV!0)=_!6g6yk%j;OE&u=kaA|NaUv_0~WN&gWb#iQMX<{= -kV{dMBa%o~Ob7f<7a%FUKVQzD9Z*p`laCzl@`*Y(q(%|pq}l>Er<)$N@tm9a^n(P%XKg>JCh?RKUzEHgRFf@G6sg~-bd{ku#vQD -$)*%}>Qc6o|0cZDj6sIbFqno0@|&Qlc0zmq?lbzMeqKlj|>XHQ_N);U?1o!Zgv1C5jR12AKb~#I9bTA*m0dc -M9Cc@0Ik!t1n7X;!-@CqIL(e%(cMaB0#JI8SszAO5U_<@24!4`$wnsA=~bs#1wf5ew8;ryg*re_(n5s -6hUN&EFH#&!!AIobX%g?mHrOEsnatDpK>|3Q6g;{^S}`~#3`{hL3z;c0LWi=5U}De~G23J*jJ4LN21T -c8!2ppS^)lv2GFYfDl3wt_37E1;Jm^>fS(kZ1v&!WEDx*v~ixCvB^N#Mhi8h)BK^&)Bx!_5<9fbs%L4 -Uo|lrjIR^?BV!MjMRc8aM2EPM&fcTj#5PNDLrb&@SbKs#x8B0&RFA;58hkS(_199wQ&qUHhI(jgP -+zQhp)NXWyc+QntGl`>jt9*nM7jpVpBJ6))zSp3v=eJLOc>Bf0rS -QTAZDAIs$&wJ6Jr=2P!Sn%u`#_Q-gXr@lT=oM+Cqi_9KGiJo9)cABdUy3S{r7)A<-|HapxDK#V^q9th -I~tz01HL0$yeBFce)um^KoY}iRiLTa>3OPYoFg6sM|O%|^~dgNiY+W@^`j4a4@o){!TCLn%*fnfK}iO -iZKSJ9dleI5t@panq;0PQGNyx@t;G~-R4$skXYzFz+Yt#lbLkVSz!d9;8v&^#Na5A@eO1(`74X%PooB -#uIvz~bNvzzdD<>l_xr^9bLzB`8heP7R7{D_SG*X6mroQ%S@773RgjP2kZf82gy!sfX+zfc@abjQY0W)A>hEYCyB5uwn4^I2=Y7)qX1x -nhzrX#Y_5Q|tAs<0xPC1+Ww{b6McFz=yOVj2g`8PbrFhATB_OU=E7sG?q%hrbsKmLy?Te-mcO$sV -jt3%?e1iEVBfLX7*U5KtdXs_MZ~p%M=AAP7|1E5#j7|#Q`YLf;n&$&5L;fLE>Zf+-k8()krM0YTSzq@_$Wjy;h`8*TQ -;&yO7`&mpb#o+p9@zePFqAy1O}OuL%kk_QVP8&e#X#H)ZfE1+=c~c3xcPj0GntM6jSB#MJ-)uYg^@;|M%O -bBMuul%^f&lJOg|2;u5dVlH~0)QxkX&XaB}nW?fApTnfN%lx){O3_ane?@cwGV!@&fHSA+4VzPK2C8h -jX0uL(fnd16z>RDAh3!pAu70R9`!#*=HD(r|J;yM;gdFyq@<)&0wOI_itT?RW|(^E7WKFbt9ydQAur^ -t~Q2BqYDRO3(!V{yZJoI9-eeR{(R00L%_vrwhj3G6O#L{bh-^l=heC|3HR@2z+`yQ*C`$xy>We=DF@VOfz|jx+!1*MmASe?CRf!u*Zsh;6MuaF<2 -?FhS#GN^Fh+etnic62YRL%v%Nnrqj=#LaqgGNeUfeyKMec;OshgQ2(eR;Wx>wJ0sRe_i0Tax5f(FCqB -KnIxRH7nr@_Jx;|Rn#LjXMyCF*4%5iwA%{=M9(7og3HYiP+N^m;{b}C3(t3(MY -KxTQVMl%q<)#p0dNabQJ_7ZWv8q9cb8(#}vL4I>mLuao_vY{m0?s{jPOy*0LBH|!i%!P}O2)wd=i--c -iwOe)uM2#i1@N3`tDpbc>AVut>oJI0u -JL0#&jsLu&DvtKGXTJAWq4d*Ys; -bl13E%88X9G35Q1U_l}dIK@FAF$uj3eiEm9D(;FkeE$I-on-)pl;JAnDRyt|8~i;9B)0s~t)EYOTqmI -DzWku>L9xwKIr#i+4k>mOG_i~-VJDRb~sU%{_mDkEK>-32x@8J2A8klaC!5`jhuZo!;K0c(4aCa(+B= -MfDGu#m|4pSG~cLAxp>HO+H&``C^|`jXLpmqJT0qgnrh%14yo=LE=BXpd-q{XfCjVNv9n7oNINnr$c(CR%W+- -_`OImu1dFU-cN%R{WsETK(LCX0)7*HMri(KXEpb6sLA{JF_2K<%Fj6z{Dx#46KOi`uzD)ocdC)RL2@@ -b?w6Tl!xYzN+ipzY^Gdb)+rEqbUVhyKS7PI{AhUbafJV -$4t%tv{K07Izx(cc|HJT;57r?}y#Ji8C+~jHT>;ABuD358^Y*198pwh*4gbA+>)%YSem*&U``z{&i9`+TRr0N{qQBLYIJ=zH>iHk#n$NhOfGur9QSp6Cht9Ue)~E;i(?)i>?JdM;k;fltEaR6)4BZx`| -1UIs+u6Q{NWjGpNae9WPia*J(nT&uPRyj?}J=f6IvlrpjDocCa%0lP^J&>PEiMgF`1ygCJQbQXo93m) -Tekfz#%n(XAh`JtK))8fCVmL1`ZDzjxxp|*B0aKD*hc5Iz;MKu+Wz9-eE{sAJ?GL)u$^bZ@nMB>G)tA -7ipNrKDagOC;{i^E2zl-O~fEYZaTteqGuE69vyrT=t1pN8m@F?|MKst!-abU$mhv;r@rvye;CdPu_X!!YdG`k&KPj4_NtHL%mi2d!DrfuONm -#Ci;@NL!xWl_Y>cj9bWCgIs{Ez9!TW3+6G937(M;r2D18X>E_6g|+gm1dU#=(ML0mm4y5ee|Y%?cw(f -pF3Lg#UdyIe_k$^cz#cRfQNcltb97vm6@{)t_x|!%W8)lpKwrZI@5>RGA~#M<0u4dj&UAT^-={q=wz7 -VXx6AI#O14as1douaB8)S8_nE^gm)(kn*c{uXN-;5{b3$BP7NP_7-2_X$Sebv7$=l>5LfNzK`v`#Z0M -c}*L7;ZL?URw3jjS@mgASVzt&xfXe}{5(0~ooRPTEA1SueH(vC+(-bl*?p}aLfn{*}Gt}qI0H2<-IvE -4z@RO6_KiHB+W(Vt<&W*^lLqg=$~+aLnTkY>cjJ%glu4d|#H2u&}D@`|O106jWq@kWCWgYh+}8-zhom -hMrf0Vb1cb;5hzT%Tiu8jYU3qfis6WSbv6yF>g@2K%4T$RbapGW_Y -fQ3OeFO%T|3Cf<{_BBc^6sQ*bn{xwZ+fq|tvTAW~Woxhp)^7h!8^&oa7au@r$YmL~LYTu4_cibHaY@h -Qgd+?nJ80@XB0WBivt9!3{9eVIiJ#FI;irE%7bb5tC)U25iM95wYc=M11s#tvZ%5P74EK`z@@9N9YA5 -NVwlI0CEzbsz+iN7k{4*=oC4E)9FWKY&w&2_Q^NCIA#-#vn67(Ou(=(em6GxLh`8ukt -7-#1`lsq|TD7NWZff|WgUi2IW78P_T9xhgR{DpjVro5jw4h~a_Mz6bc9W(E(?9AcXN9SJm<5XmE-|QW -N;cBRlzenEe!V7I$K4|92}pqOaT -f-$u>Z+yQ74^8hstZLI(I6q}U~a4+Qs86D3Xx0o@3u6PcRK>Eof#FP6tdXRMdOvl_H-o%ID>jjG@I@? -sKxH4&IxRCN7f&%ktit1=T5a{=FYO~4;1|XQPiO1z@xlS>w6%($-RwYt+o>#8mfAe7BiJxV#>QmR(Ic -HI%89aUu?>e@_tk++MQh+EJ7Py5rmw-agZhh2o^!=XhU$7DPx7%Z!DU1VlC28UFU^h70$ZnZ9e(RCq84S%V>=0Bx>?nZc2^!eV1mU`l`Y5zPVPU()Q{+{q%3NsmQ~qB0s~N%Ly; -Pw{vorpwhI?1#&veUma7^7EzAc)D9a3RTTq6DPBL0a1DG07CHHVTNh|}Ev#;?%G?XQYWeu5TIKL<-P(;Wvq4@rc&n_rz -t-8EnlaZw0vjT$Ii6IE4+bwAH3-pQc)g`*TkB*|p@8t@u)Ja}Ib99&s9`q~xl{qiA{FC5O(>97L2@fu -#CU0D3t{^@^=B0!6NrlaIg7u7<>Ex^7N(g>Hrs%tfE7}yP4ylMmW5;)u?)hZUIOCC02dKY12HWIla02 -4PVffa$PGBTSMQH87R+EWR5ds*y3CFEq2L|vwbFObHn;bm{JM#wFhb~@xD`@7-^_d4MUpfN?RVJ -9h7w-&cR87N+WYSk`117raNd*mc#vEqnP~IQY#DEtWWF#DHgT$WPUVFe;=O#g%Cw*IxRYW8z_iU4z4f -v|7A@gD5_?)|xA7cNh1nZ$BlcFk&Qr$p9@2cuDuH3;@(Mylwua+<5M^m?N2O!vz*+lweBgVyITKg)3N6^fgNTmSL-2^>1$ -Sc8?J_ayTf6H%WCEm@5OK`uhnyHMqLDt~+Q=rI2o?t#WUxPHpym^{8rv$>J0MY0Dw+)hO@x-S;g4PT| -HPK}~dPczbK^rLbhNvbt^kQr4P6&bwt=Ox+~dzNd+;j -1kgHI%XXz`I**K?NAGK~b%<9#M{N(LuD>A -qPFsXwXE&#rv9PZc+A85h|?Hi2Y|>jHd%$S|U^LeJbh|YHCyk3Mm)!zW6(iod>%!0G-C3%c@N5Rz9uu -U>SneM1;)~bNq4BAY?d0Y;TEOJ}0WWKrZ@E?*;4{O0&lSwr38hm|rO)43^+NqPIPZi`>wK1pwHhnrSm -=;A~!gL(}PZI -N^unrn|Cc*Nx71->Q#O?56%{E|2p{UwlDZ^4mCPb^AEL+r1V({sujD5N(P%M -U&xAqnUPA;gOHkVWZnV4dFm?Eh81y%qa(fBtx%@dm4*$aE82J!A1sC7{tyWb6j%f22-*+n}7xD9)U_` -ELo8hGK6iwaS^I)z}hAB70Zn!I#l7A7v<A4+IR!G^IK^+h+EcPZMqZ0LCYS!qzy@NM=bAVHmpLMZqh)@wA8|)(FW8=vthmIbDl`wV6!LA>GupeN#dOz7?ZGGGNhxR -TQ()WnZc5|&f(rJBPH1?nlu(%jrZJjZ?$Q)jnHI*z1A)>UE`F*5D|M_?SpX5#dpK-=Da;REUI+@2I%r -ta4&1qjnskh7v9R^ufPpWg0-w)C_~(qty+LrPcg?*se9vL@U{8Sh-fw*-pgIvGk*iiwpnFCZj5Mqb}H -|3Pl!yPT&r?4mgIP8PDPel7-c2Muo5ixKo_;mYm>tcinG+?;!AY!=T)Se&nPlSmf1G@rE)ctOM)?>i0 -%xSM@7zpurhzZ#sJ-Go};jIH^2p=5`P@o%{jscVpWOfT&WKOZY7(XXlH0=gldsXW2$6E+Xp~4Zse8IF -&Zx7G+MO2ofZIbjXS@^(BJ3Ijwg5&axj$8ekjv-(<}6gXNMP<;vdxwG1Zc!>rJUbnswDJ@PXxbV(l=8 -XzXZtgjdnfJoti{RJ?oh=JZ4yQ{C#aDuxJRoZPNiNs6pwz$2HBPTQgcm<6v9QCYAU)i2?%7?EE_R3AZ -?m*imY8k|5zTU*(sEw9v6&tV1}r~_N{-O0EA@$C<%-+%ibwi!s-1*!u~z|Hn(3c?C_PyP0dI5{wY3-A -~2^{es=JC8C~UHj~q6b$Hj3^HC;@rXbPSWyXBe5LBPd1GQcFikB3+Y!nQtwdKl)v4T(C|6Axqph^}6@ESASVJi%}w;-NWqkz#t_HNa>lDcTfmV2g1-=b^2=pKE-e+gqqOo{kCIa?Xk}@ -9Akr0NVgKK}P#e7b?L7bs4Qo4iAHtq~B2=DQ%g1k20zb=>3g*e2QF#H8k>bxb~^z1z+9qYGwcIU7`|S -V3+79nR(F{ojXQFEk*h44t*6fMBWT?U{D<6;es?MZ9mZLD*{AkLK+&MP0~q<0ZzMJIx=bae*s1F&#)K -C*)1KyR2gy$y86jf&LuldQKC=ha=9l8M=4aM>$}%b+_zLyNw*eFC?NFME^JZCX7~3J4BP{km -r3<=fI)a<=n`ux+buQpvC#ZBp3B>3!#Kb-dnk%ZlT0>_2ngbA8Jm>Kd4s=B{DV9w6Xe)l|D>Jy(*pqW -#=;VeO2|+Y-FtF`Q%R7S+;<{m07tFcELbp{FEZ25|1Mrket_C!@3C3SE_> -ZxHgngH3`j@jB#INqUP-eD;JVUN#K=2cSaE|K33io18nz~ -sN)@wAm^qxk#D&f#lUo=R2{S824w4P0?(ZD?g&tknWnBjtcILfgV-2)(7=>e -bNXnLcdSDi<((w3z#Rm@C%mr`PjVL!-)uO2mVU~x^d|UbCiS0G_7-zjMPw*XI>$JcNrO=0ll~+lb&cO -zm_G$s>Qc%%27TJQ^;86x^wUrjO^{ynGb#Zf)IAAs)_rYO1ybQws>^{CHDM_X!kKKRdbToit -+tH(NOeBhDEe^oFV$}~j5`~~9t*~A^rJEl9+iqc>VE$x^$KNoKc-k%;GhO2Okb#3o*|btYdlF18A8aO -Z>z+il!JDO^Adjk!u3l1z05qrLruK63~x5$QkpIIc|Nmp#zHI46loE}epO>tF$Z~7#>*e;Wps_wq)so -^6-d7rJdv@0*B9BaEe@DL5*vr;n?z*ipyt8l4M*YR(B^UV9;`2VPcK14?riwz-CDgf5;3`CTX -oIDX>vJkBu{Ou&>~4h;ElIIpu$EJ=j5Z%Rf`&qETb(bRwKhHRC`(efc@sLTWR=DXo#(F-&iLM6ojhW| -5~4LH>=oW&%7dsw`VUsjYEbZJLipBK{Ozro(*kU;G=ZIh+^*jaulro=0k932IG16%UZ-^6V5%&#?QEp -kRY#kUW4>r}sVUQqwqVgwkQel-iO9DWBGbXO6{I91b1gZ#8{pBcSYx70Na3NcXEq8!O(AC%bl+)i5`; -CrD#f#0Ap8vzyI5@rfL$Ihsx++6(7SV{##PR(jFkI=8(kla;DVVzV^QJq#nD>8 -Ji8L)OVims_!d9go)VRXXJRdklDgS8SNcjimA&O-;^hatr)GLA=pNpo{bkeyl^R%}{%~ky$`TonSkZY -n5MVH!!W)c$(hs5u$u}|S+a8>v&vSs3uM}o?6P2P$eO%Jvww$ -_uUFzmo0sZbmlr*$&-^*cyu-V_uVDj3cXyF&@x)ol*mTm=t!~(9tb{F4huBOf8nsNKhSfHc0iJ&a6t! -SRUeis*E6i_6s=J?ghSkJcf(oF9=xv(b|3y@B{^T!DpEFAS$7ie5hfer1=ez5zz;7s1^^rQvRXFiZ-Du&n@OamHuFzFCw03_7#k!FE^w_xNgfy{PfclSjY2QlYJZDbH -@5E~Gx!ljp}LPa%P`NCp~hK>Hk1Ip>V*PKRds*e6{Mp?luVy884A)WJEYbG`|(yBz*`bHByk+-eb3Zd -P||y|=Uuq1Ruc;4eEG(5Rxh0;6jhNbE%FA`vjU0AZ=VEBNZ<W|x~yExh}QEk7JV=RI-m|^-R4hEp_$#5yDKET&!pADYIDl3UV5uuiW{?Z2Q-~!As -EF@9=#BaaVcai?~n|ATlEh$_DLXcWrl0!FgR3f0k=|)<3#r##C80OWqUDT_e{pvtVxwyaOiVl@J)qX{ -XQ%H39mW_iA?q#FY$bE0*s<$__vjZ(}>8}`t033F8=yWB+`zjmqa20A#MV6Lx{Lbkp$0LD=WUCR=l1aJ77z$meP -<^;?dZj1f#@6^-y6ivdSW2nB$EiX~B)FIQ7#>E&Z!hg?A!e(Bbsv);w=0QBg6@&Jszl=t(KyUzi3zn7 ->Mxhw%mr!knO>LA8%q3ywU47jst+jPcO%_2;94{pM@CIHU^(TZzS(zF31-)DbUX8JNB{lv_;z$r+doR -kQQ=+H?0RxNQllBb-PyY1HfQX}QQWpTa%E;V@gh9b-O+j+%!XDsZYWqjR*1In{WMkIeZ|B4_CV@ZMtI -GZow%}9K{vDw%vc@yC#$`&a{hR>3>5eFjjvnP*Co-Pl<28&kJFITLJUZJS*Ak1a*WDEOgM*2A%OkbzPW_`MJ1{ymSn -LfV)X^LejTXmly1yN;?fzHcly6RKgRc1EsQ*&)_$5QsQ5#6kXCGrgsT{8|vCJ*K!?&Go}n-k6JW2*Z_ -EH1p{@7ndNL~i{-ZS%wA)}1SMKC-$+-zZ!(F4w8x=$le7ZEK7NYiOd$MRjKifsZWa*>_ip3Xbc~i0J4^pW-6!W -jS3pe}`DE1&L<*PJ_Fva~MLF_E%(lIq2lwH+F`gb45hcZ}=sTzItFu;Rl8JOsBUwBHs~9{%UIfS}!EJ -nJV>qZym6e0;KzQ3Z-0}J0J$3g03-ka0B~t=FJE?LZe(wA -FLiQkY-wUMFJo_RbaH88FLQ5WYjZAed94|1Z`(%lJAcK3K~Tw%=(gt+2O=t3HU?m|baRJ!vtt7bHT -jF2yC_n>%)hj<%1snlwKzuss32)yZ#hRCQK;l|$*&euRYLF3oqZluw2>M8(D8vbkZW%c6I0)9AEpBhg -pyvdMZobmeUGezxGK!+x+hq$l2{!_JW;CtgC={@lZ?{uu&kl2Vg-Frkb7(%+I|+5kj~E5$F;;OYPomD -GjuO>cRFepTC}Jhb6UPB#WGRf08d7mYTW63lC0HX{*J$a`KS;=~Xmj9-ObOSu_OEN{Il90!E -^RQ3S5E9%yY*G}@oYH4`P6uq7Fdm53H9b8mwQct##hiyQ-CE%OY6=Kv+0jS?^b(V57GBdH@E6-G9|Ah -JGcz5nL={z_ZM1Cw18I^VZvAhK=R!70c1-rqgjC@}NL+u&BI@|0tMv~JU+K2u#C&d(i_@8jo=%|Lg`#X2&RWZzJhND@j&lzI71$1o) -3I|x*L)?KrmD$Dfz-XIuwhaJ(&LNC$Nz0n -hzxNsE%!__593Y8qsqBf&V(A-6os7zjGVV_#&M8RScGg(2J@6jVgf$87o`-rjEm?(~-CbF1(j+*KUJq -$x~2c;^B&Tyrw#-onnWFnPOE-syZOQ8>E?4dA$YN$|ssGkImk=1w5 --Cv5&_EudAl&L@?XW30l^;Pgs)M{S3zldUxdTHf&Y=8sb94QoU0vz#ZMQH+tb{10k*{%QM1U59*$A?5 -R!pteioJzXhwtecMa28aOMXWQ*iaz)|29hw3R8Fyq30Y(#cK(4%qC8QEN0IF?y(Jfi)A1Pg0#S+X!1) -Z+oG@k(%{kQq~9M|RbU!kE6m*5D$KLKE;{yGviqF7>}8Td)Kbzf8SL?_=Vh*vWFev0uh<2z?z3fW=M5k!+TbVHVWJfUKL{MoMNd!xL>a-TxNvkcRSjC)7hVg45Op~4SHYXUo%piU>tS#O@Y59L$$* -E{NMWV(mULP$DwrI>&)sduL8CsTT~$s?)|#$geKvzL0@V~UPCSFo9g6eGAyu*IQXBm6K&fXp*Z!0hW% -HH%HMJ%mmB^VqIJPE&BZ4{*0-bw&caJC9#yrJTm4ohdwfS5(lCl7!!ZPA8%)&e)ZlX~`7XK0S;WoY_p3B~mp+JqSGc;0`VV#Uq7Hectq-MnFgf=V -a<<$`lr0YF*Eh%Q0noKEtK%nRcyvzJQC}^JrJQXE`=rA>kp#$L+xX(VK;@IfjL9&SAFL#He?O@@q#tL -eyz7KTpKJBHoD5TsrX13kCH{*wegtk6-S>Lmos^WDEzD6dCGk50Hacf4o9}TNzK08gPs;^Bgu?PuP2) -`THw{bPVmRsR}(|=>z=>4utz0vdeeny?td_E^!bvK~0SyQnB6pWhY2fo%Q*~%#d=rNFZv1j@3+lyr7; -Rq4_;QLy7cKQP&8A$wXq$O&XhvC0``Ei}0kPF@x>Srr$FcOy=GT$sK{0vg>!I#b}hEIYo~$E}vOB7!~ -ozZ%qdz#+}>%z$P53c>NxC+}JP1OBTCZd{I!RIbb>_yq%!DeLU&mgwmt0D~Fv27T#dkF~<8CS3@a -f|lr`4Fta2V}kNV+!FwY_LTYk(uYwg}^}Uki|V0XXE)?viab?Avi=aMG)b^R4ES9 -X%s=&J~G~ATyxB{{T=+0|XQR000O8`*~JV000000ssI200000H~;_uaA|NaUv_0~WN&gWb#iQMX<{=k -V{dMBa%o~OUvp(+b#i5Na$##jSF -_n{EO&N5_G}Y5ltcjyh!wth@L$Nt+IBa@BriJ#ee1}zc7$!o7Jh^_a|2>Rw*@~!(+vZ%_1V5*P%HB3( -l_0}V}95r%N-?;%yh_(`jpt5uIIC7dqYVGeGYs1=+I~CkimQmouw24N=(3}K|tNmHXY}U<7zQxg+v|v -YyjZf2SE0G8isG=(Pse{;)VH7^+BJYRbOQS*HkH)+=-+ -tVMq-C|qX|I3r5&pT%lqCOrfuH|=S;OzHOT&K&ygBvvcEF7U^5HPA(abOcbkhN^pcIW~?M(lXc2G*we -Ip*26|K4sP1{8Sn4FdcG6+)xseo<67aVt#*bD+@*(T5=8)ij&a0C;_oNR3eytRdc(==j?X1l}50kaJS ---DsT{-m1^JB!e=LFn7ucRB!MInf4XNxB_-ZwoGh8j#=y;{F4%(&6%UFna7Q{!h7zUbJ@XVAmnwK|p> -Th0qvaBOd@E8t8Na107H>E>N*ShuFmuDY^Uda6toyvB`w*?${n-Ii*x9FxfWpU0^M|VZ*?Lob!LE6=D -mL-!cbc4%GzT+1Na8Vh_MrHP_%kTrc4ZE(<|yAqozPmyLGZ73?4&(e;D@DMp4+QBuuEF?d))_!10k9< -Mo-yRjU%Ix%LW<-=g1ld(bOi`lpFP#==!z<}>&hdhjzx3l{t0f@z5y8KRNBQltNCqIv;!w%7ZnJ;w1A -hQK0H@=%s#yYf*r`qIxIG%nbH-I~xEy-kjH(mna<%~0p$&Gab3Et@o?G`=Q>clvbM19v{z+Y`So=s6DZ8lvl;Iji7FP1 -6#!`RR}WUv?;AhJ+%F#|SeV!)blLV!Ef1tm1UI!XY7-}i>D$~4pm6QF4z0U4p&Xu{lsQwCYBwowQvf3 -+g^9z6qmFklWu%}RVX8gaYlm?31xy}*m-skpBta@cS(5#ywd?x))F`&<{F^VxX1)EDBzTCEv#(%)|(O -@3pRT6xKr?S^Nk$%n(-$K-cl|4vt7@)ap?kqr3E>AUNh)|2@NxPkqepu%hs&=sF{8}>G}PB(Z9Lb`9Y$=|L?b7=Yy?9DG+AdW%C99{Q)=C_(N7beraY0V7 -GA3@|{AKf(DjYcx{U1y{L5G3pORFVyE%K`L_xGZOhJ=>8NmQ{10OOXdi2cN7tR?f=&1z@TtrL$ -^?nn46Wgah#TG?jZ?-$}H&jR{hJBXNJ32Qt;jh0AjU(K8F*k0N+B^!{oY#0mGF08*Xp+xkEQ!vVhET&$l)^FB)S)((Jt=2G+_UccHb;#kk}R|p@KJ%JE -@6`MILQvqp`c8ue<@1A2b+H_qdO`3)L#R;dPoNp>Jq{dtRr>F@;v!2EK2;sZDoLXs#Vv(lZn_91fAvF -8oMoT0#acwv2~DMlOEbwt!@ColG1p(bh)EpnkH|ZByM&;n9hP~FcdxRj4d;;@-^F>Vk!VTlQO@61EgqG>W+3|z|>q>gzO$Ql!a^7 -VrtEcZ1U>5Q1>KxGKJu!a;>-!=I)&D?2>b)=>=w6ey;It9mSAB0K#OMJ87QE2XiKVVK81fD>~M#kr!Z3I -hfe042W^=nn2A$Jm51emWqU!Q0*S+tBqU^$#442V253IX-a$;m`b0#X!ai}oPMRu9_?MHBMM&eh+O;w -xO8Qi#jCh7u!cR3*c`#?QN_7+-l&4N$CV41TbGbjqJ`J37Bd3&aOsFQ?s -*IE4oq0_u&-dktJjx&ncMQ60deHn??n|=4FWC*OzE>_tM(Q0tFl2j36H_R&{v=1?<=zYqSgmO^`PPPtz9=Csi0D^8E2{eZV?=rAJs6!fR!)df%Ro+bb$Do=UDxVa*{Spa -!l5<$+W$1o@IaVSA_mI!N!2iG#;SC{j=^Q$-bYlwxh6^k>tF3YDQOLUD+aExE!-O<7WYBOVGsEU2dE`TLhK#8CgARTxf+K_)v{38%8`R#-umq1vVyenSrz*#b4hMFa7*V- -W$*MzX|2E>F7{Qjb<1ouY2De!1WFLfYwRMmAC$v<{WIk-Un}KM&Tn&M29xtv0CuzAx7#^qF;xzj7A1R1u24$PX|NkvMWs5}mDqlHdaOr2sWG9`fs8N>(l}>D=uOda|1V? -SC+#!tlD-D%fUMaBDP@W!B$mFXEOgi@|HWrWyr*VZg6-7D^t%%{_d-p{R*k -24WC85g|YP{E-5$mJM|OeI+hcejBA6LcNP}(ESSfDWfl+2^}>Mn3p -rokBf4!M2@A&5_vUPvP_gGFYB~>$XJxPu+q+1h;T;X<-w-o9=l?GOSMJjTYS ->%b)Q+cPZ7QZ5e;~bCP)T_<8oYH$FsJsXG12U+zl%l)fEOFjal4P}4C6H+dmx9XoHRUSrNa9Wqa??k= -VsEKS_IBk}eQ}%Bc5Qq^WAkTY;z0pk)^Au2EPQ~8hR@$xjKBHGF01qZ2Sfm|_(2dpE;Fq#z$?}A+d7l -qX%0NoqWm?Qsv0MiKCD)N}5}}*WOjH*X#FsnefRn>n~6tAzdwae#7hrvi;M{HK_ -kgeLMjNEqWPjvJr3!&HX~_8MK~?*5$-VTKmv?UTnqYPsL`Wd=)P!4fRnZ*)YhRc!xfI{6GMa^t)$-K2 -sO(*N6BH?*l$hNEGH~u)`uBl(pY0wBMPp*^bdQ`TY5Fro86ZVCneYXQn+2F*TFBxT&qEB{m1<`kg`l8 -&TJyf@9h*Z9I(KXYlO}$1mvy;VjDK>BpAgPd6V;4_{4qFc*Ma_|U?Oe2)e6i+Hs50^X6M1U|O4{e6eL ->}+cn6SMK5CQ!j$<4w0rfFX5pv*4_(?;Zw+TK3lF0-l$FEGU1UiSs;f9htTR0}J36W#A(y(w!XOWf|Z -M$2+<0{z+Jt`o6k-_3<-AoYeJzk4SG|HhYRY&!kR%Qoi}>LjY}~1pA@@FlAE~+S@k)(7!AI4A$tL0{v -1J>=)dj*Ki*z8*>EuB$MDTRO61%Qpea8+fFfCSt}%~zk0S$V~YLrTrWucofi=a&%UhJz005MEEwZw3^ -By11Tkf~PT6j^EC}(E$ekU4&s#eIaxR15DO-Be0|!>XoSx@k?gQ#;CWMRQB)Xi*MtFb(g>bsJqkC{FX -Lt*fv;e%2O_Jp>jqN?WI2V&U?@LhVoBn17kXwhn!~l{nQR^G^VW>yq1q}3Ge!A*iN&QonoS21>;&HD3 -czSVqc_xA2rWx3u&nBY(O!7(S7rszR=QQ13<6Enc`)oWLBo9>VXnQt3x!U0uJ~@qi2xd{}SuuimXJX< -5*f{?~Y6UA-eErd)2jpt@Dn~ahxnAEjH>x)w* -&Zl+FQ+Mlkfer-i3my4L(kkQYbE#*k9JsL%-Fy|2KOm@6aWAK2mt$eR#UDjr@UD -a003e(0021v003}la4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ8FWn*=6Wpr|3ZgX&Na&#|jZ+Bm8Wp- -t3E^v9(8*OjfxcR$(1<%Ey^34^}mmP`#*^s8`U9h$(;&jE(D+F4mV_jrXM@n(MVgG&43n`JZ-KO2XEI -=EHA|F2QZ)C>f@#wYQJSx?lD#c2bwK7&LwGquqiJ8hciU#AE -3s^=yeV{LVWn*7OQjpJD2&RRax07~Wuq3N(FA%`+^pQLr|&N8Xx_RtDp%G@v&D%ws=>)?eBNktn+n)= -D~c*F+Xd_{HY;5!F^8>JLN7(>DH)wI?+jOhwGzDJx3RY)7L9z(!!3Q8#TVjZmL!gS-bBz$g$*3UFiOT)aDfBVfH0$VCvIq!Eziq -6B&~c*)MrudZj=)wwtUfN8GRH6WLa1pLg$kMQTk`7al51~aA9&x8>1lb_b((|*kb*B)3iJk -OSu_<;VysTjMCV4b*CTeiqMS+>fO-jSIrVdYv0Z5E!_H1$Lz&Bvop>0*z}26IlueSKa8U=L-{_lQ@G{4lgDTT4`wqqF<~lN?0p7kJ8X!3R!bV6{kJ#VT_6&^Rh0Jg5{a5`R -w;!wv^cd7Tskhl2ejiX;r>x)rQFG8KMAkhc`(Rzcu)+31#(aN6&NEmBf@Ke$=2^ayCaXKlyaw>B9pNz -!EVvIvvpVB}@020#67ZKDJ+5J>=M{t@`2GI+)pO6zK(iRjj=_Wz^BLA;(ipi-O55DeL+oYxz)ieq8#y -o4DMIeWKpWjd3bf!#7FMA{z<8L>X0OkU0V~E-MiwRHJp*Yikq!{VTU{v{fyjRx>O6nmkrUy_VscpbBK -lS_;m=Y0>a=IRHEKC@5za0c18)tJe*L6-DwdS3F%`$r1_t>g-cVXYEK0`jDq -$6weiXi(KXs6cB8gb$zn$cqf-{Zyq4OTmXJ0J!px6`^-)q>nf!pxaElwy!e(Al!Q4_K#@NeD%K&Q;xr -7chQKkC-n>E7Wl)pnbym5_p*0Bgy;-d|E;)caGH6Q$xk+H99+qarE9*r9@{D0yFc{|6=$1t#^ChFA-6{ExOEH(lz6YY|H1eSLy?C=F -mbS*p*ml@t1zv>E2|Ssi$1X&WYP>WAK4Uc9|Nze+m)&1)tu=sFq?dhTCT<6QPTFn~@_0}Q5L_QioK1` -Tmr0&(c1dQF+P%cTlK4LfKjJ^q?JK6>&j&=+OF^V|pf*qiO3&(Nm=!QY3No*+jQdI82}pI^G1Ib~+=o -$H#)v9n0U{AQ3W1+JhI5Tlq9>btDO{bP1^3;7{kC{q1#+bkbFA4e9$GrYw>2!rx0t8_;CoY4PMXa2z5 -EH7co3GOyDp_KyHO+n1tBH9sVv(3ur4aKnz4iW%Up=K@k1M#NOfdNI(VLDGaIT1(4fezqKBOm@~O4bq -YYrYg7&VcPCfHzpY-zcXUjy|G?pg_u9w+*}fJ~A{e!~)tiu;GN0$kVzcob+`tGhr0^+!XbLoG=$ywOS|eFXOWKPzg#OF@wV8)Vsh^8vN#3LV -<@uwA@E2Ds=Y31-EVmr!(j(bI=w@oqVAVCQ&Rn>BWfxGSs4qJ>~;o1?*E|QHJW-7x51*WOl%R<(o!0y -(b2k)W4*lm8Hy8G9UkDeygB7hlgtHWD0dz#-yEykG)r;K1e$M`I$37pf+EnH%7O0@^~_#z(Hfkk*J}N -F0r%GfY%F{OMpLlnN#!1?;F^}o;dne3!^btfvLr^$SK&_TIKMf7M_S+wBMQ~eoKZJWQX6I8BRhL^jLq -H2VYFScGG1P4RV*74Je*HtoBT!Ceg{>?o0<_f9~iOAIcuTfr24UL;u%HzAwnIA(QNEeV>+$M?6{_BXQ -U(y0r&6@~zyMw?(;NqB%e7Bzh2+-PF0$A|~z|Xo)i9E<5gth`K&Yqu2|voy`^K84vWaao)~>GE4{!#> -g~=q$|icH?nFF+(NTg1nzZFROcXedn`0A-T>pKL1`JlvMb#J-w>npkR`{IGJ^&wd(^<`XZd(hSh5V^^QSgJHFOP}Ly^L{JwmlPKreA+n!GXkr-lzymsp1!nxs_WNmp -yvM<~7bO>N^8b(wY@lm{U9MZTrC^y1D7lVmk-V#nx|QOM%!Oag>bFr8mmw(ogAlO+|H4tlC?Z3a*7cX -WwJo(bK%>>8rhvMrhI|7SFDPpacf^*z&kk+lEe4%PiYCEC;?^s9hZdsxJ!&W`tj=$hJ3nFG0YeZL@c3 -_TB8tfOWzZxBEt#o1)4|LVM=)m|KzIq3f+`y6gXDfICNl24D%_B>Vjz2B`wCo1(^ep^ayt!oVuufuL)~eJ<9E -}(i+axrzVHtHdW>qfgHsIEoXFY8HUu_KrY+ud#}suNNNj5`*P?IZm(5wa^(NdW2weV4GbVGWz3iOHTAu{Md0swB511Ie*)H3-j-KTQzhH%{XzjTTViKH?e|rPIshq&s&@*S8CNPF#TXY{^ -KYh$?(ae+PLd5s+y|-XwJxb*PW}FsV0PY;&p-aldG+Dwk=r_rPfe_T}8A<+^GAwBflzL=m4id9F^T9f -POh73h-U%5fSevL1pzoWlQi3TwuxBmGx-N1la{Swu8X(GWdPYAWuq7rpfUB5d5dz|b#^MBrmUspTTW2 -w^!>MZqNf8YKKc9fwEM;LLCVum-?bOhwsk8B`_CzpOt{bmbi819Y(E%DA4m^pLZ}jyvQqICohXfJ`lT -$_IQi-i6Wv+iNh7MGZzlPQ*{5jD6524~rlu*IYr>LF>o3sneB2)K{ku7~F)eNjFeS#%4aAU_W#j)*u_ -=tSb(dP(aE>vbWUeXWi?}6o2lMq=y)=CWj+^8aM2zeWeEM4Xa-=Ok6;@F6cx)#=6G_be -Jb?gBAq>p;D48}hK=C-$%U!0lAfN3cjAbt!!)ELqY*R}Zm^VAOs98vyXo`eRqt^ux=JXpOskuq@?`-4 -7M?9qh`;HJ&SD8t5*Fpgq?s1HQW3u?1U*H|b520$|_{!MGq9Kt7_O#Wi5)l0vyo2+S3Y*gZP1PP!OftHC>{hJ~u|X;Oo7LbA$mPmq1m6 -x{885i_8~Zw@TGwrJ>*frNGnR?~D^-rbm5+0+N%@C~%0 -$H!2CiPU7O1j;Fq`6BVzg%>@FCHk|S%lYP*_0|Oiy-j?YP?gwRbvA>=RfA-Xmn6U0SJ|iU!TyZL?9N# -ljmBrx#?E5$GJ^t?2JEvuULCD^Fo^i!0@bx0Q_swBAPr^tSvTPDX^t>!!8aHUUvg;kkPZ+0$_g`Masy -G_by)(I;E52XiPQR*lJ%=g^><00JD(+Xd4D6eN!OD`L>7~P$uCa9v!c{DEcvevXc$o255iKzfb!u@97w7l=8$i~ -2(y;?<&4V@bbnaui>+<}iVBXGeQZ -SM1GcdZb)a23#}}BLB6zDlv@vJxy0J0xSUP!2(0OkV}PI)h1<%4;`MQT3E@joZBHzKVwWg#@bEg*kb2 -6KFl4VdfO~2~S|gUvIHj$1S>)h=9YWyt8+og`RFkxjGzS~xu126!{NRGr192|#&SmAQL{yU~Gl|*-PV -u@(g9fD*WB?8t-j4g=U{M)aL!<(dn}C>5H}>^Ix`^{Y)TX#Y%wcKFf=j$*2^14*p5jjPq56cO9M9S)Q -=zb#H!V6VS_1X=GAKy9P=>&U(gB25z(uIcSI-IDS0%?z(8zOoBDGkz|0s*GB5zJiSNH=_Ac0ul)}-(^?W2*Y -%$0OcyYGw~ZzM{U9Tb~WB?_IgHbGw{)+23$L|AfW`q>@NfNG6*dCenpmB3aNgh=nPcMAtX_(a))vFh* -t}#zkoE-1Cm2uAS_e4df)&DJw+C+hy!<}Hdz>4@!cQeeXUIHU=MPvKC6Hf7PE?Yliib=E&&SW6B1F%j -j_tR1%>H0Ice@`{?RJyZjKz|kwE7@`hN3Y!7l{Rz{f{@1-kEyj*vorY~CJRe@NiYYI>p$a&>#4qaD|f -__Mc36CxO65x*Vm+8hwLOCw|O(d3;bm*LEhy}T)IFz*bppJHya!8HP -{FgU<*g2eL1H-r7p7Y}hdb*puq98Dr6bIAFci4WE!3bgY)`9T15_t*_Sjs{ad{3ITJ;)b8X@V|WxM#hpgrOnz3YlSMyWhP6kmBC%(AD9fv -uNSxgwE=(%nh4y@v3rlq!}O8ih_lnd6+mUOwfud`4`gF|neox>`^mLN;T1emwuAz+gf&N>kDkz%g|fL -R_|8v{JI#1{L^IabT_$VuCau9{lJQ?&yckahK&J;lO+1jSRF(U~2zc{{nMiYnM$7D?jUWFx__$~97r) -XU$PAWFcX%`P+fKxv@G-2bdHLu+P)h>@6aWAK2mt$eR#U7wjI2-q003+N001Ze003}la4%nWWo~3|ax -ZmqY;0*_GcRyqV{2h&WpgiIUukY>bYEXCaCu8B%Fk7Zk54NtDJ@Ekk5|adEyyn_QAkWG&d(_=NsWi_x -wz6m>hxgJ#l<=Cxrrso8SxqU#U*)(xv5-S@$rc{IY1L^6*QDE<&|_axd2d00|XQR000O8`*~JV-)WED -CLsU-YKH&-BLDyZaA|NaUv_0~WN&gWb#iQMX<{=kaA9L>VP|D?FLP;lE^v9pJ^ORpHj}^WufQqOsgx? -b#7=v6nYvTQaT?v{*U3)PUcL-XiIBybB2_+YtKWS8`|Se&!KY+7cjw7OB(S?!EOr-*2Z3keC|<3T;AW -AD!E7k@UcP$yV(;bNOYv4le|XCv5*IR)Ng7AJXT4|ek}QKX4dO@ysaVKFPS@fl@uE!nBQZ~;6!Bcl7G -82AM8dIFk`#L0$eePDCn0WDRHpz&+Kt;ES<$e(_%e4uRr+J= -4P^=^@KL*iJsei~>hAD_JkHav&#|qBk$Pchy=^JXkE6DYx@tve9akEEp1{^aV*cPjziB(p*@WN0`rIH -*J4RZEMwMvkOGy^>dyily-gbJzZ3n#OL*^(#HaDqhx3nD0w -5-bXIHIp)HkYDXuB010eYR@$%S^6z`4?Pk`DbB6CGGxYO(P`~8nu5Q4^mPRv0m?h|+>J@h4Lf(MqEqg -sGhS%c|CVvqE09}s)Jc>)UWy7N5C6Y07lSfU<*zNT>q#^v&Rucy-Ic{I!7I81dy0Dh{J5Q8dvS)Azib -ZxS-^-89_p6hx!@R2}i2clnddE&>ZO8R+4{5o-R^HU$t?jRpDobg^cuYPb|LJdF~A5%HcL#jN$tfA&! -l1icA!B@=Z;55-p&_bNv#qWDS0yuLt$sn4?pI?3v2M1ygviIAw_c8l*WB!}HFXE>IG1wo8*FzB5Tma~ -so`64eR?&@15&+x*ypM*I6Mz@Lkx7vS2`XzAf>$mw@ImEd@*$%P_bwqzXznV)G<(nET9g9!7K6Ok7EDTP`nVYn8QLWOmv)m3*I>f* -E|g+`uc?jjvOTz&j%y|@h=DZ{bKE=_~>TCqDYdC_-znVcVTgif4-c9^@S3YdH{XaypoN7i3HP(gkrU4 -^0Q=J1_mmF-v+~>Nt6#Wxyr;jXz3jKrsE`ulQPOPkkAW|P$_YvjOub?T0gwriq;SN6!w(i)2{ItTS4#y|2(vFk>7+6Dnd{Q-IDO`;=oU_?^BXSup0=5 -!8cZ@Byt(w86Y1;7ObETb1JqPYSuvm*y*`eC2EFun5hu8!SYssZAp!ql4W4p?-W8t|CH6KXv^+Kgf^j&2Nq4{eCQY|N~f#TWVvA)P^?Zx* -BJuUX^C+3|oqRkkmUWtR!~)pZf{}1{3gzMZxoz3fy-9h)?WaFl56w9Ncj6OVm);)25StsmhJWFjEi*= -Tl-A+(inb%Fq{po+JT9z5!cUjU@8=rEF%#R3B$)s03tf{D3J~4A~y1o7ZMjH0ADWjaYK0MY_uJx~&qS -ny7Q*w2=oOUFUWn%?Cj>FoJ(|U(kT5i>g;clUA@>yO^e0Z?2GmXp?G{-;IvUc%N2RrXiIPXfL$1%-ms -5WIW1c@Nf!X7hbmD_(NKOfR0S)_fRYW5c;HSp@J-oL}C+g1rH9|P)g%Qh^KMhLB$*nQ=)#D%LSxRVi< -+J^2PDR{>wcv4cg(38-K}thgHSVIh4u;z|6>2kDsD -%=%+Y#7DE`Rl^D{X^&a$bB^700yEd0GB5kUAJ}@u4-t}V30z* -BdhM_)ZMCCYLRYxI~A>>VO8|`H@R+zI}ZOIX=F(b+8XdYF@pQLsc9&sNIJz_!zTcfQq5IEU$Ka3CLE**t~*I^8X -V0+aQBju12@RZk?%XN1#l^`Pk8;kHT<_3RFzzKsfnUnT8&~%v~GwoEu}`Ai))tz -!ofu#B^!Wx0&tW4)t8#0Y(n})|#y@wau>GOCY&4&XbuMZBTWPUW$IoOHEo)=~*JZOqOjC+oQ$Nmkbxc -oQh$~2k>y!uB>bUWMRFYYodjN=6FL71FW!wOlq9TWonO9tdIl~$+=)p4^D!@cdc5chu4iy!90p*=_R) -)9@TEhT~pvs$d#DCKf9#)&ha@8T}m%OjQ^XI;iwUR9)wU^g4grm_%h8Q9UKZ}6fo+knO32v3^w<`m(( -biUn-FHE0JsAI*3+z#)CD*DUaQ3wiEB(HE{gUN(~B)8pwfjYr|Q3t8Sqw0Q_ko0oE|P_Yo*gi~l(_FC -M$mr_K1q)3gIW&NC$+2xFHJs}cnWkylAPlWA+^6EC`v8^r%$fcRmP&Tx0CjJJWdnNjVlA?0i8t0;p?A -^c9Q0T-FinrNG~p|%RAi%>qef97#U=&N8QSyY&g51iG4ZChoXU{V`+RKRI)O6nAj>*vem+RYXaO!&48+S1DijgtA11oIzSFl8c5+%K|h1;I^x8^}Mrgk^&-=&|4hvz -;P)1BH~e10O5MX$$Hyba9zDQ@EHw?`sH)Op0SbT8R|O`QhsH8gR0*hw5VpgItE-DkncT5L)KjRTQ+{otarDc$j8IG1MB$Q#Gi98nv`uto7s2e -7qYUeD#ZOHLf95hLnE#@OCXhs~5~cC@*a(xxvs;;^1@H8J+0LkOJEC7a>BJ_4?3RLA1iMm?9bm2>cwt -PA{GEAHd5+5KLdP9;1&R0YRwrY@bdgGD3HCj{R?|!Mu9Z}iE5UUVB)urOV?C+!SAy#zm<1H+UGa}b$J -9=&?3TdQkH&)6E-VNXXm?!74Ox2CG>XK|7LI(Tp&m@@Mx6=-9E5Q)Tg0*Q|Jm&krX1NRnhy)Io8)qd8 -w`sUIHHt#o0K3srF&ae!R+=)N;r=rrEKrblOhne^j;cu3Bx7mOs28Yj#GYr8<(`jg6ghTReQ7Bo%J_Q -Lhwu6u>+!>*PzL9S{rdH5DJ5P`!13ar-M34Y6wk|-`h!c5$nwB;4+D)aaM#il~=`>o!Ug}VHqZ?nHAu -0f^zu$(iP+={It?-{dRgXxi~61E=8#5y1wAnP~R8#LFC8xDb0-q^I#^-B35Om8E*9J1N8o0pXa>lQ%T -?HxuhDGfT!YmS3Rn_eChS<_3j!?wTDS>FeoOD9=~(UW4WdYco&h*vnLm(PtIFNq$qxu! -noHc%w38KC~5ns;N#@vxE#pub;k4-D$ -dL7ihBfOE-=xnIKEuy#pgzIH&mJxwz;<3DJJQp7VzSgjG8r17ty*T3}cfpbdi8;QQi$Ca4E=t3IW_DfDO6_;Ej5}0Ux0DG>u*GZ)CPg -;)kFmVA+5@QQ5oH{G5dLF2L6HU!d|NT1kwDji|tjGm8g!?%0Pj9{ojZZP;FA`mD-mQAWU>g>fnu5CZK=0QcrR1-CtvF3{VI -phZ^OS^=Z)Xa#nyP`S;vU^2HbhGK2*>)C^~-d8q>B4c~2m>?1c4#j?{t(F; -EG_8Y$EtRdHu$#UW_>C&Zs+j=gnFLkLW`N;nh@)zEVk7-b| -lpBU8DY;#YY135)*1q=4G~GXVr5=8LZ{QO4Fs9Zge{>aY-u?Y6{IlShQ7IS+32@M*6xL|i? --U9fUO=IzXeyHnSAb>>2R}o!Svkts2x|?#J}`*(erlQ?&DWNr36w>IIcPoYO~7lu9i=UfXcqMC}_q^j%yUHsdUF-OrEygs@!L -pdW#>^4Wsuj>I=fUwk8|)}tZH!~THX#V^QQ{9FJJa&qa(b^BHxY8B_<6XtOyL1OrmCzA8ki$P}y{9=l -;;i;VG^Ylo{`TDI4a}&l>FH|7v1tE&h`pQ)Zmh{+E1IWV1whGwqO*JqtZB-~GwA8_V$6ng#gXiDj))5 -!?@8g+W^a=e#)>F-Y2Cb*s-`IT>We{X_^A9oy=?t*p@d4;o@oY9`UKn~)`u<%%cCnyg@MHFR*aPLH!&zKbNdtXW2g2%3u-B@~=G-ZQXljKKb_&19j8BqOi2_f_q>rT4`D1I{CI$pM3)EQ -Zy{YnsBx->=ijNM401)~h(>Si7jiuaGI|f$~F?0cz8*4RYb-ytA^7?!bVm&Im$1=d6%pj~vE2ryI=KF(Zi#%CxS|1OaCSS?$4tTd?KfY -is_z6H&dXou%~8-uuRN1$wgueh_#twQohibi={n=rQBB#&wlMzHXUYZ10_|rO|$}znL9}hfGd4k7mNvmq?;@W%Cyo} -v_CVqUs}oc8vW07(9q?j4sTQ&^l=|j^x~C0Jj)v~I3Ew*Tws&{|8pJGF+W+7L(srUQMQMh<@hvCNOms -iXVCxYZGY(C;8~RwYCL>Jls&+;wFV`xJnc(;o~SP$Xe*u#2QUa~q^be%*=93{nMM5#FoML6ext0nh^l -kci(wb=@zKRo!;`qS+A&)f?!Qm&?PK;7_VS -eQq-l}K;pT$*pol-_AA3(AtZ(FhU2$uR)kKL@t`Yg9kadPV=9N*yC)|nUiUI-DKn1Z^c5V|ckD7ZROc -91%WT!r3D4wC-g|K1-7{DFrd?FJCAG~Mo8X744Pa7H3#H_PX#)#!;zRo9mtSmmZ}fdI&F3Q# -w4mkl&w<9XEx>$OkGtRStnY#O;2J-^T#XQYXxoUfhN`6$mZ1~NmeXMsH0xkl#p4uyYh{0gq8m>|B^dV^e8QD`$%mbj$ -2qh$hfjjy-ckC(~GmaPlOUK0E+t8v(%X2NOxNRfBA$I`)ysrqy%6YV%`IBp>(LdIX?84K!O>r#{)JFg -V0bCK6p29;?o$YGu=g^POorjgj!&qyle{P0PeBP?J6nB0OTKOjoledXOhpF#|sLIDR{>|Dk+$2ip?^eC+he;=x1>*n%)Pm%GK8t4j9~->p@$!1(@CT -)>HWf9eA&j!BBy+i%HRN18GB6F=|bx%9$8|Jm`T%OlwsPr{&8|Nh7!eThLo6b4ig|SxT_IMujPKLbsS -QyIRmg2i)V44#PKHY+}K5Lf$}?OsC~>T;0kjt1L~17vRkdm$bX!xuVZouu&-;YQ2KKFzfU%1kvZIr6E -d5p%{%Y3~)xxrIqXl?*@tF{fjhPhmtXh`5%me%qf_!=@iU8=lhaf%~!xH6LwbdicX@!Wfg3?H1p#;D; -;kK;EkHg&bc@X4cW(QB;>&NGH+P(j_Av*PY)4jB#L_j8cwlC5FSi0$r8XbG3K-Akb6A*4aZP4y1`2b_ -(|&Mj!y>-GN;PapjN2G;vDP%sSj&Fbf#&wE~>0%==sXg%xTA7bF>X2pp~%H(kt!*Kv%RJ5Ss9Izduio+0y%~M$)W^uL)LhbW0kxV-Qn{^a4xn -GV=0tMKnj@!M)Eo-oYB#METI7NV9Bts^^bhV>7J!oTQNef{6MDbFuuK+tw<}o*8<3skh9Zwz>2+ZOOq -VdX5vcx~CepjL8Et10{CgNg)|DrN{@Gb0w?>6|KXwk}C#^e -h3;O80of<5wvRJWhC*m-hBiOicEe6m?_JJlYvT+%t^pTSCsOjI+07Q)^f*spt|HobkTPI3U0fzf1QVU -x~O)?me8;GqeSmRiktR|SeA-fF;Bh&?K;sjAydhRwozSxE1-l7qTsgN_E -458f;b*VI|ld8GHa8wcOsVA%5JdbQqvQ_NT7D<9YK?h|jdlF79qX964|S=>uyPW9%iQ1+cYJ|k#z>dbN~EBZDQg*0gM4#)s+%ZulV8kS@2(n9A1Eb;AY*Viyy4Q}8Ks3 -Y0yYpx)@CsNQAZn$|aQq(j%zG25Py62XKGq4n#5=NB-zevz*92AoNG&#JuIG(tNmzU#T&hVDB%K~0;* -#Q3W;@#oZu>!4}P_B2>>C(!YR~t0rU-gm<&3WaL4E)>TjuiZx#n1pztygQhEg&rwnzY=$Vb@H_rA+Fk -iOf)`@}RM1Bk_-a4u{nSbS!NJnW*Pt;K36j3RvwADeHuFH%FUI)ogsqT$ZE9La8ojjCL2UQbXG$DhRBhb!)0X~7hrjbsuo6GE4#B{aCfCed^FsN -qKro3swr%mgdi(;IAS%qFicTc?`H#EhMokZYE5B)y`NK5Y)OQB;I6)i)b^cn-SP~ORq?gLeOT+{`T>Y -`2BJ4tinDcMK(4yl^^?5K{bl9MC+$z^7^cT -SI`RWT)@-gDy0Q@i1B+&lDHm|L{^*(3Wij6_R$Acw&ZLdvK4QdkM0p&WyS`Ty$6qK8e&f8&sScEp_y} -&y_SgdsH%~@`ZYhL%RyP>gMUCIXw08lASmSZWsga*MUOmc?%C&iCx@Em!%E@jYh_m&DV^tR(@1a2Gr>2 -g2Q!3#1at@6c}gLGu3yAJw!Hh=N&eLW*{pl5U}4UHw<4)FYW -`uw@xrEJ}xsqSrfZ_OTR8N3kJYJ_<$Ui_pc*61pSYbfQqfWkL~;N&X3{Wo<;n-(oxTGaTR=vi61N35v -m$i5ApfrbQ~F`nRzR|4Xlq~Cs%ev2ELdGp_JSZ+j|)ZDEF)8|I^|39r(Ay7 -0F`oCHsMGSKeWg{93!gqoH@!Hz$&~oeTLI1Xrp5btE7P)1YGRrN!&`mpM7wp(VK -H=(txhXKe`6Reldmm0JW}UMk}4@+Mq5^&pn>Q?4A?rFAj)X6|c -K7rIqFY>uHn=T^C-i%$!jK|Lp}-eN1rgnxs_D3-@-Dh4c;-MY6*=9;W|o$S0y;NBvN*d;L@fTq@0Ui!fPHz~l(ygZfmT$GZHVt6ogP&G8Wl^6v7ikL?O743 -Xr(ui0Ml7)XDLT@?sJ-P_!jlRLiPyrV7rb6~0C9gxI=)(1*7|&hJq`JQtPx~}?B&j>r)mV);2Eb5^X9 -;okIG(F}r1;7ybU!P+Tq7X31gM%=$9)jyU$K00arqWAh3)*f#*FzYl6>s8aG(%Jh -|5q;w=<9)JLVYa;mIxLKuDap2MhxnMh`x@LYr%@PN@BoYrjBX}F9KQ%3_>FsH~=Aa+RZPItd;1U#JMYANN!@XzRG&DMhml8m|6dHf1rmc=TIje(b0}Wlo6By41&wLC%XpwBy%mggdytcY$0d2SQ -Cq$DX!KoR5>WX&L@gC-r_OB(K*S=zJ-*0OhvK+k3VpFr2-vNZGT|CARYGp -2T)4`1QY-O00;p4c~(;Z00002000000000o0001RX>c!Jc4cm4Z*nhna%^mAVlyvrVPk7yXJvCQVqs% -zaBp&Sb1z?CX>MtBUtcb8c>@4YO9KQH000080Q-4XQ{K2^vq%B}0Eqblf}u6E?$Ex7o~)jtK{nuv5qk0s~sv -d

ua{Ci^CingE#+3ywNu^|y6mj>~ua;MYkP<6MXIZCiCIsxGv96}j$Px>x=oGeXRiU!o6knylDWC| -}q@_|)>9`Qs)m)%ok?h>_TwCsc@&sfxYJ_h%S(Sgt|4Zf-2x)9@X5u=_;G%#h-yZGUpDq0ps<}roZk_ -zb>K5m&y9wpu7pr;cPX%WRCQ{S~P*dpa`GD26?6AxzoPozrv&_Rf#f`g$!c_q-e7=v^nz)UdXgYd*-CKieYri1yct{**@d5&J%5G=M17ZzKbM_{Bc=F6W*fJJWRrsM_txWLEcZsT4ALf; -9aWHPX%xd?z)flj-di(BMBGqc3jM5{&~LmbpPdUeR;oF%x~7W%kT43Kv4_{aWsOJ$L;Y!Xhpza3EEgg -8x2jXZ8k;#&5pjIX_aucJi7t=ewL>;2XHvXWh`>Dx@KHWEW+D6c$q>OtjF$klw?I1Nhc6Ja6b;*aLQg -{uB_$-rt|xmq0x>Y^t%Rsn?Ss?zm``k#rAs(43guQB7G$K??cw7=W)shFP5+3V&orCO9KQH000080Q- -4XQw0;FcccLT0G|T@06PEx0B~t=FJE?LZe(wAFLiQkY-wUMFK}UFYhh<)b1!pqY+r3*bYo~=Xm4|LZe -eX@FJE72ZfSI1UoLQYjZ;f+n?Ml0^D91)gKZU|L{TqMn?tKs>ZR?iN3>=KdllG0yMrnJzQf`;ekfA+0 -%CT)*9@-fISAquvrNMDltIrOehmSgk$PY4If^$Op&5KFjy+t2>VsZOl%omW`JHlldu{0q7_5abx=4!>RjOH)2MSS9D(4% --K9M<{wS6{+7khdJjCLj4GOMOk?4l%Z`aek#Bu5s#y5=3zoZbpA=>gAke5pbIFlF1h(O@Q2_3{Epiga --Z8J9KP^mQG!%v^_S)QdLz~lRb)D&kQp4^aQu_>gfbApBoDo)l0;;+9tid=k9HN&r<&L!;E62R0^31!?do9$*4@H5Z?XOqec*$$x@6aWAK -2mt$eR#QHNOV+Un001u*002S&003}la4%nWWo~3|axZmqY;0*_GcRyqV{2h&Wpgicb8KI2VRU0?UubW -0bZ%j7WiMZ8ZE$R5ZDnqBVRUJ4ZZ2?ntypbu+c*;b?q4xbP()50rD<-vJ>#-K(5AZqx=FTa_loQG3|g -XMHZmoUbRFNX-)Bg@&`J8Tcb5XGB9b%D%slf#B}wvjA!#XGzL+g)>$F(PbWG(+=T6m{N>eZCa^n_wKF -aWKLeg5Poe~wT7gE#8Dt%2?SFf`qNk*d`IQw_;xtst=0)rN|iUY=itxWK)@lXExIj{7S8#dIycB-x?0 -X@NQ>DsTngnA<`{xA+f>S>W;fd%w*ZVsFf1im5|%n -4SoOO;JP9aciDBVz`Y~YZeTt>-c$U{I?b2kg6$>MWZ9i>?Vz7IM&a4IFb`F31`|~GL5}#{TP$4xcg{8 -VgB+O@|D$O?lREDy#tdXdL1cy9A0?1w;H2_^2alJ&zK^B~lT_LeHEKedY=asSxSwte58ueg9Vp)f(#P -qHt9~T@^OLkGOIU8J?8zMSvq_DMyR1~B|4Y1h|R>8@6kI?p$NYOB94!sO5|j*SP}7f(`+Vx&b=tm=F8FgbS15IKn6*2(TjZ63 -JL&3|i+8VI()q<(5)PXh?@^z;B&b25z%KT5!R?|{h$Nh`UZ{LA>cSmGI$sDQa)(_oxiRoI>K#>LE~`) --13UN(6LsGc-7jaID+d0f80{r7H~-14YIkp!E4t_z#avX*%Bq|&Av|`kRwMV>BCf=);-WZ+>gm|O -6(rjfcxlMnySK0as!<(NPxd{-44W$w7xV@=I}oE9G)7eM -4i|h%N$jvfsummh-NAS@ISIg+}{)6qWi9ju$moD4!V*yVb}{+FUd;?c6!;Tx@0Rr;DzUPJFogkm^2^m -?;cu4+fbGjdi_V?|rYv4Eb7#T%zRuv6oE9#~M|#&m;6l@2pU8xxjT~u^`yBMzy1@^5D;vb*OQi9(}2v -eghOA=oDiI$~V?Hb1-r-z7tX+8hc(i03<)(=TK_wOg{;4$nhcu>l^2|?+yrhia+G>LeP?LZz0MN~AN;kx5Q*jy=Km0W4=%~8T}y|?Y4|X3_1Vu -kxz{P&%R+RYK0kc!S=xmZ+>wV2K_% -FU{$Xn+-07>OL^_@d6PT#zpU0l9ByEs32&FB1!H>bb7J9~5ba@ZV*)L%^gAB^UomW6iJ;`4-`4{oomx4m_^_x9DIDx6)#0^*yiRzqzw-&q~D2y?9RTX{! -t}61S7KI!0CEvxhReW2(NCcsgH16HKKBaxZ^VWWcu_q2FkV${V++W3wj5JniZiE4Qn=m@`GPnl~xUAP -b-QwT_xIDJ4>4ATI`uI2Pbu_x|S=fKn+bhp=CG%5bQ;6Os-IGEA4s+VV6g -++TH=e;o!hY&e6NUZ8R9F)*6YrqZkq5y#*s53V4!vTE#s^Bm~;ciyDd0EIdtIv9%O&p4m)BF@xa@LgJ -JoPC@B3XR7Lnq<@_Y;5mRw~#R0>pfm#y@;W%xDv|r@3ypW+`aqG -_2^#z>E^a@>1lfurm;shp+}SsUzvEO599JuG%4TCCq3%%IKHOy@Ah^V!-jjmje85~HnvCR4x&!AikX_&0RRAl1ON -ae0001RX>c!Jc4cm4Z*nhna%^mAVlyvwbZKlaUtei%X>?y-E^v8mlfiD=Fbsz8ehMMGY=F@>=poBIG- -!|*c6!)}P;9ypVp|#|C+pi!jzvdl>XZ2W6eazn8`7NsXa+YB0tnR^O-{&z)$QOArZ`EyiQk&UK~|@Wq -}qx~cSbsOP_1$wsW7C^s>ZP03U`!F3>ItQv^bzRBH>fgjE6l{y6>@aO80!4vT%b?lQstHkWKh^KvxT*z-(>E7W#H^tIgBnOS^-;oTdK5+ -je-JTJuQS}bldpzFy><#d4PQnN-Bn?rjWZ{dZM!z2NaZR=<7Ias|2zAmOmEMjLP_Q_jTZeB8p{bqLGP -Nvrp;2@a7p?^FtAKSx9>>sl)r#uqpp=8Dmb3FUZARcyR52Nu}h=zlusB1I2pBTn>9ejY-KF044wc0B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%gP -PZf<2`bZKvHE^v9BSlezRHxhlhhK2*A-o>MV%Krt}1Qv#z-la-{ -t&E$!jOHvPsluT{2pDV#e}2W3-9a^Sso`8L>i -?wt7CEO=%p1rAF;S}gtvC^h6oK>UIsNyRSwm<_EFUh*1G8(m487)m_02lBinZqmFZ*9#>u@dD7@Mk<8 -`_JDvMhv~2n}&l}f!lEC#i4MB5`n1->_#`Tf+e1j+VUzNc_Fi!8MgKJW`4mhmXcXJTWp-G^HK_~T>i^ -^{`9evz5u=a3;&E#9<<{#K$oG8CR>o=mBVda-}!h>(LA(hFQfz}_pG&Gy#26Pd}7k_#R8o6!X$Uzmm~ -@{XullzW_a16U1|-^FlEsPG*PyI$Dy(LAWQ@p_yB9}m~+NQcM4-#9Z$ilp7?VZL4Opj^sDDHFza3F!W -A9yw~j?UXcZ*&u|xj}18qzMWVR_cz$`3Vpok-iC;72(O0r*brtK4TaRLI8%$5@4z268FyWxhzmNTjbc -ZoVUR|yx#ItCEU$OFup*mB*t4{!1}AxxpXzK-<&Gi;;L1v_^-mAasZMQc9p6HwxmA}A5x(Q$ZgQVV;o9 -7@M2YD@9_-qbIK -+*~ELBrPFu8dZFJF0TiHY=nc@aZ$zxL%j+n-zJ4_wQ4jGSWLEZN&Q2Ia-;>0#JmNT!2 -{eq0O)Jp^u=9M+lbz0F1FEF@ -XaL<*0}6UEYAj_EPZ?uFZA<3Gleo*KRK59yE*HSYg8jvQtTZV|qNmJ4|4P_ttanbcDblTz<_RjipPR% -|O|n@tk?(BQzoc=xEYpk+d>H1#eUMK%I9@U2?OvN#AZw}G-+M;DUZzcDr=kA4Zy2$qcAGp(n --idoKmtYdoG?PqnuFeI$%P~`ND6*XFwZzQI_)q%eptG?;@MbKij4^LLkh6&3H3TM`B -)6eNlmNnzcZ|vn1BwKO`RAMbf!D$SuR_if*V7SHq -F?rwiQfDc=R1Fo&Hm)1r*J%y4f3^}p~?lFuZ>gz7VF=JrbAd&}%qUP|ColzBiN$lbbgW -UycNnY4*r_TLF4Q_7h$--5F(moe<%f$$+NotlnnFHNIw}8Jml?X8+|E74c`;f1oE)ne($`ny5GQ5Bla -rGm=P?GEWy8$`O+3?k8NnRg;)F(GJwCJ`um(RhI!t;L7(eZuH`@<;+2rizfQ(QiX*w&>R}*cAqv7kJ+ -1dDzjXD-kv7Dr^4huNEKE4izgo9Z!C;==Dv`3ZRS0Emw3XJDxgX$mz&?jzYSNJR0agK?)q2pvGtT;_@ -VV?7BdP2yd&-P)UQJALSAUlmnx1Wlahv%oek71tc8W2qwUV@(vYHs`!gn6pAoEe;{SCN|oW79<9$vp9 -F*StEe?N#1C+|5%^f4O}w?w@|Y>7#V!La=dk0NrpX<#5AnA?n?YXShCW=iY{K=0p;UeO*NdLJ{Rh52l -XEk_udlvpTI_&_oYmr0?cfns=yIlUvaV>jT6}*I=Go5{AM2yU~utQ7cMQ&31`BQm?vue#rLLazEn`O~ -GF@$6y*z6cLh7EDdS+Vo9mL2>p>7I!ipMIL9ZXwX#hO^_g@3+Oo6j>oqpyW{I4!y^jAy=stzD4Y${C- -@m^}c{}|lL5>gke$k@COZ_FAoHWFP@QLJSp2BqjOa*&CFZMc6$AUnUbJ{wRnuCLvQ|jnCnK$>zlW4&)ixzYm3i5ay!> -%9WrwB{Xf2Y_rw3#XoqEZbEa$f-V%p}Hc!Jc4cm4Z*nhna%^mAVlyvwbZ -KlaaB^>Wc`k5yeN;`4n=lZ)^D9R5utY*!dRr-{sNGhova8+nj2sw(C1TUDNjCq!W8;AHwOoMle7yH&# -trE`6vlKV$bGNLLPaPnmIHO2+4p>%5wj}&#W|Ip)AQ`Duk|Z@Yz)=b-%LeB@%ck>fi5Z2T}$$G$6N>DI70x -T;MZpJ2CLWg`p0U+ex!$8>-~N8BJug2dqH9kf8YPiAjgXCac14bK~c<`v#dOa3qo_a_khtlkmm-CYeT -Pfw--w6kc$2kCpDJtNHM3}FqF}D1{Qf2kjVnU$XC8UmC` -gQq_q4$H%8P8cA*`u_rhxXz5NIx!hu*7I52-#F-5}>$(V$#XSC5s2*;X%O~moqT;>37HYh{f0BUb*>n -lxEWHwqYU6?aFyu$CV#JxMdDYKN5_#JBMidE8*_~6IM0+mbIj7i?HJE+PwE8K{ov!fEOCIk> -pX^w?f81-afi>RE_LDpEq_T;ABCu5>iT%?bf!1BSpHuo^1xx90+WzqK{pQ0u`mN)lfHhl&EMRGeiZrR -R4UQ9%E;3R!~#^PQavQENU$|CkTq@5OZ88jZNrHku%iXw_z|17`{;}bb2{{c`-0|XQR000O8`*~JVP6 -PIz`~Uy|@&Nzc!Jc4cm4Z*nhna%^mAVlyvwbZKlaa%FLKWpi{caCxOy>u=LY5dZGKVq}Dph -{S*np;M$HS_nsV6|Ju2zMPP=u_w(^dpGQ^n+C-HelzR0o5DS!s>yoiJ->N)ZFCNCye?c}FpfdyTWuVO -mDO3{FfPwQux7fIu=w&tO|yFW0#%|@tZt(S?-JZPsgTeLy&2rh)RAm|TnMOGl}q}xaZ%jE_|ipDON;5679xg}!ErE^kUgVj00`>U%9sK~%=1bJ -oqHsDFP9`GD_7SmOZ4k7|_V*cVv1RL+iB8Mqal($y7VJLhS}v!=zFfZ7*3oYeDy7p$i)MCwjf -$Q{!R7C#7xDYW@!Q28a0KD&*RMNI=oEU2i{q2CMfLEK=qOTOQCW23zd}(e#Y$;7g>XX(`MY3Mf>ni|C -P8rg=FQ^bDqehebN=@9!*8tb`m5YdQ(R}lVHh~Gp9esnI_gHdOmd9lLXogE&UIR37*`kY2$cRJ=J4*t -v*$sXW;pe{KBJ|uE#)b|%wl;QTJeO;n66A11o6A$)3e3ftHs$F*``;YF>zzEJ0Jrg7dIGJ85ny(H;CI -oQ1 -VEK>E-Kk35543bd|0Xw0{En6fT65lX~+r8UpH7iV($<;4UpG7ajp2jxw -otWv2SS&IH7d&fHBF8zSd#6oWh0Z5^#Jt+bucFM97f78g8a48m;$YT>;QOy8csaBGVMq_^KL#aJ%m|v -!=xk0cVdkF3v?j3f9~UdZ*sg-LhPN@bl;2@VaJa(X8%F@Jn(tq+*}grBQ?AYeelBV -igwT)0E_Nk@;xpGg@s`G&d*$Nj1GiI@grf;wJ@IzS_+JL5=3OWUv?YOM`>iyqG&w)hDf0f&mQb);Kir -Hm6_Wg$FiccJlW+)9~Dq7?npuyE_yuLS2>)x*`_C_GTu53B8^X*C{-Z$**Nwg03qk%{fg3vy+zE$PPy -|YJ_u`(}qH{hMc8_*^$dlvs`x>Ls8EHPK>s!T4@J7_1id*u{ZdDBb_a;2M@Zfpm9H>{6D^rjN@)Xy<#ThqksgB6H+w^E={>GG -8abDGVhlTPI|qT8B8U?g7!Av)5$0!aj>MZ^ft8bC?2y+-e9$=mH@2Y2`m8|Z~E+nOg5*La8ZMg-&2U3 -zsY0KjiY(NdI+p*;6&G1=k!}gtr;Y(?zDwJwowR=J@6aWAK2mt$eR#P@8)w -Ey*006cP001Na003}la4%nWWo~3|axZmqY;0*_GcR>?X>2cYWpi+EZgXWWaCxm(O>g5i5WV|X5Z*&29{kA={vvE(#QdLrWuzjYN7$%3h=BfA1TTl4U2zqCj=AC2~Ia=FJ<)2Ke3u$*BjsL) -Z<6?50W%MJgZ%7to@1=E0udX>}w|6b`C%Gb*IrTMb%!YcK%KP;quN7$=!G+gaEk~nKL8VY`QSz -#BR7}kBujzaw@Qlaf@cXW!{K<)JMZO{{q*$p9X;G@0PHXi<0w%~9ZbbBvje^}AhnEenMt}S~RP@}?8< -BI2tPdOhc)QyhzKLmwN99tY(?@u+4lpV$a_LCi?|fzSg(wU;ed2{9FCOFW$86x~=NobBTub83_uswkz -gt43HRqJ=V}#XM0&(TNRZG-9V7^anEuDKMqt -mrA3ar9n-#3AKG*no9bCyA^qd3~K`1g)kaD -+XIZy#&t&|Ymdl!$J_UKQ4UW^ZG)75$98VL|;f$)1k*`09>me--u)cwg5d?G%;VkXRDBz_o -c!&XsLl?*uz!`m_14jsQHe^+~>+A!fPPDd$7F-OJCVMV1`-4oa^;6Yf*_zZXrQ$7Qs>ka -de-UR5U=U}gpp|vyLCr2ct1HO)Q;f7(ub`jD8Tf=MRfaPa2NlBxZN4DwA?XZK*PAew{$<9b0+s76Hm)i -hpDP?DV{TQ6rel4Fm{11#l`5^=d6X9g~(xf%m9o>{{m1;0|XQR000O8`*~JVmFNkb&=vpyk5d2uApig -XaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLGsca(OOrdF4E7bKAyt-}x(c=w<{a6imrU({L`EC=wlOt{r -(K`lcP%1wnEtq6PsL4^fK8{p~&H?0W%FPDb}bZ}~w2f!(uv9(&&4y2!UIiq>^i7d(nsvfbrH#o|Sor* -*|6@x9lR_xUzXQuQUxZ*P4rxD;}@7y!9r&zvfl(V2yfPYhh^?J|PMO?+KtcrS -xjA84%0Q&9*(iW1u~|J7s&$mT?A~+2(f~Hk -p)6pu$rYXDh;GnZ{+w_DQv2E5sGR@fFJjr^8-!!PhaOHDUSUzhK1xml2a9;^i9v5>yX6J{S{9KkbTq2 -h#LF-&TChE~YQf-o2hhZ>FhIkR^+eD^!CDQ+h77V-K9Wg-ALIP3j -yKweNnz99#Bv~P|?oPu|$ljIQ2Y>eZ_0@0F3%SeNL@}h0-@q1dHel7-3 -9+4TPL@5K|2cti>L8&Bdi>YfsxxCI2}lwWx;`XU;#6}7eJ-m5GJPHt%$a?H(+O(6ErZa4`ixx#7OTLn;A)o|Ix*P%m12zmeRKh|lX`s+r$7}~q%y`_hrI%(-B!)}jtxUcf!lQUKImQ3NmQKDiMBY@@ -4(7ZCXm8>yMpr>Xn#lk50TGS}jK^uA^a_HbU#7F8Av?mG-;Z?yvA05jXEC7! -S$O@uW6v>L0gvOaxg@MuR~+sgp8Zq(JM%1KWY1;E9^gU&d+4=kurA1p$;hAcw -AG>a6XHc()U0ZA`K{p8pRxMrpEmVfmz?<66#Hou#}id$tI~g+XVC-~_doamk6W%eW@=G*G>TB2J7fj( -2epgU6hKlbly^YIM;Izp|{}E_fl_L$Pp`vp7xjeaL3uRhJA;IbZQ?$%)l0ht&%>Wp!4Sp`qvv#}$Cuz -vdYb8?S9o4kQ^|)fo4Inra>6UcvWeuyKNDG)5=nkpR1f_yVUP71l{nR?2^E&XAnPFOrtDI{4afX6JLF -RFr3tK2&l-lX&tZ8uGo$_iP(y59$eU08U0q`hp9YAQF&H$*j%IHsGNXd*v{}FW?-U8Npk1!lVa`fW+- ->4%({Z#hnUK@FN5Ryl0TMddt}c6it@HtRlgv0YKwqF$+O~Td@Rbvse1#)JBOR#-cCXBo}I{7FuD{=g@ -@)N;_~!-w` -JUzG0KORg%hOQg?{pLeCSN373lJ^~@urB8Y)Qqpg~(rsq@Wxa{5>cp?vQR9S4)_w!OZ94X#|X1@)QDN -z2&EqCsL4Kgs@P~jq`M6-L_Q^WdiU!>I?>`N|S^L>dsP!ObWT{z{y8F^&O27m}}F5z0+or>izA^z(yq)rL4Q&&Y|DQY6=umIdVl%{Nejj7 -bhm)^OnFb_Z4oxEgE$S_1Bp$uyDfAy&s=mn@j(b^ayABMy%^6A2IDQKEwu5kGGWewXBR3DhCxYZOSBDL)2_33^39u1czaA+f*#RhQtHdqEpUWGCV2KZhd{ -lCFTQT|#`tGqwyuDTD-+2L~5GNQkWneF-5TDicVOVql4TuyGXKMC^O*9Be6jGz7@uiSB%^fgzG&qBKs -nYE{z@DpXr;i6cl*<@J+9DG$`8+B(evVe~lj^*UO|%PKEyg2^XLJRvFCp=Da|YR`G5!p724YbA$M0r( -UT8cwjyfgHBq!pl4*R&GI5ka!650qwD#P20|EP{bO}3LMD?I0Q0s5Q0&robQlRzL0)$H~*P^&4N?*HT -?Z#+((W9fha#_gnZIh -T#WIP`MLWQO4pKNBoq@1|Kcmn)>10h+3(Hg27ga>SPYY0q;mP}H`zLVArPsPCy7VX9TJ%9z;9NNBgj@S)&lxbY -R(Tp=H2IOr{76ips7(wXTW<%hRnHZB0XzbpEB0wp5qA|ehJkJ6R(gw=;LNl+yEKoo+JzaFE9KklC0x? -DxM12uv-e@HVSVPg|)Kd_q3*kz#C8%GpXY-JN -o8Hy#cU#2ts_x2tAyO_S7&Zc;E^>%i7_3I5@&aThSr~MB!dC=IVYu)|5HqW3)pLKBB+3ovpPasJA_Vk -A#`)>5&Lvv}1k?yM965$c+U}o6bN&l<{S{KRhvWjT-0(v&t)?2OkF*Fi9wX6jIJ2@TobQ(X3(4~nY!; -q4vYopG{W=>8<=8j6pqKh%KWGG%WZpkp#y)F3Q-K3z+yXOhlL{&C@P==)*0M~*~EF7Al_N=IR%X7I|_ -o-C24RpJdl@GjABwA2Z_2CQcF%Olhkgl;F>bu|`w1MR^fsyh@tLRTnG73vh@Yr#9ye1<18%7^w`h&&@ -{K%ru0wjPPd21ZClyvmGBOik!5Y}5^4CLMW)thjr+)b;Hyj+LG -yb)2KsR$YinU*l(xpnx`p{Rj-4yc%%)s;ciiD#05|dMfw%1z^cwd%rfA-HciiC{-BYBU^+LGvKAzo{g -oxVp&gPuDu{zS(20eKJ*e1F_qb${}dkx1aSddmtQ!<-yf?i|lEX%W}q;lnp+7ut?lHiF(wcZTw8Y=QO -Z+3CDIB6dEUPH*XvGcfeU4nMk$4$TZo!2`QZ?jUjk2}VK0kRA>EeiLJ2TRa12-8!Vta58!g;vr1PxXA -TVF9m4Mx}y11wt@&YWWQmtf0?Yf1!O__Xskucz%;K58TevV5<{>Scfjd$5`d^Qw8qN7p3{1=xj=ggEn -rAI_ep7QNMhdb5khc3TKz~^wC{uN6J=l|kdizU51RG7;W)McgMq~5C_-%zMNQ}N5yuKYPv%?^uqSh{@ -be{3)}V1msL1zCdVNryNSD3C4{z9V8>Y{%PLQ6$4n+e{0J_`?U?}^Ar6khzgW&70#b|jVex}KHhx8aH}>f4}yo@aNUbGRWJRf@*!tAZW>EBx(SY_e8s2!i#@I06ufMaraMCPGMx!_k+(r8 -%iE@IWi?V^LyNH9HaEA!HCtD)W|n3CrE-(ru}-a>3ZR -t`)d$eI_4c~J_cV#46BtE2GPHno=S(GSviyI95Se$4K*Q=(hLm4G7ofkR+V0^1-DT+Ly=4Qxs5*y~>= -?4(r`M~29XZK7~EY4xUceWha^QC#uX*e034#7amF-Q_G-5>yI!DwYEv9ZTzuB~QS2)R*NhUV?mJ^IAE -6{+wQs2oqj`>sH)8-&EW5d9hx8fBItdtduQFFT&ICTUSqn)Z-8o{r~)r^6U4b;~zeJ0~i0)?^+x`vVH -{gLM3O9)5Dr7S`0%C8;Pv`M7%r_0zqNpL;~^j_jZta%O0Fo9Y-rtR>pbNBk0SoEtzxB24LL3Vt-`u_d -k5l>my^{K7b*}*lA-g&)Avh*LpWQM?hIlNz30+%~GcXxPZwzu~OA$=2a%)o`&BKnN27uZDIqm%T$8}L -4&ZT1zw_ZL2EYh_mW7DQFX&?KjI@?s>1HsvISf$R`=G$23S1hsg<04L>wB#9Rqd0VZmrPfPamrC!_I) -UWbTz!lkhYd?*w=W7P}%rzfvM_*JyTs!g$^`&sx=l~3~#W>NxLtWZ -1WD%w8i3S`}0#O_a3rc_BWmU}FE@uLbw)lYxrL7)6S)9vOTu$;F8l^6984*ny9W2l1 -Hp(5-zWTIjwtaJ3RdRBRCFNQLiv1gdUf?c8x7oGq)tV9!QvwAZ&R6yzMAypIqSq~?a&4(zhMZlZVl=| -C7{BM5~PsvsH5J}-zo=W|P}unerq++Gd`-96FmKvAFUbnh08YCoz3OZ4t0|L4` -1p8xaICCr7vxK7|+j79?Lbeq)@GRP7||z%ppO)at$oJMeIpK|JvWdjllq;y=1iAPKtC25#IWGH5TU!C?>5dWWZ8=o^_!!52ZWFGT1f@MmBt? -E2Ve-G!fLI=6)hG>pbjiBVFF-iN4lvl+FAEQc-6Yc>TyJjPTn-*kq!u* -Nurjro_tRku0v|j4F)PV*xApEAID?xZW`YLPZuc^;|vLCLzRce?vEkr3frv6*Oi=8cUjsd?77{BsSoX -sEcjXFb7}WTK7k@gZ&l=`x_1{B2~L5irN)lo(rcJ=nh*6GMnjz@u5t@&;Wp@#=R{=I6x_Zvm`Q1@zHO -z=SZ+Flyp~hhE-or>Yrl=g_-C#LQq#n;qMwPB4R#&ji3OR3p<;i7R-KcqWy3k1*fyQn(3TBJX*d;)S| -WK;MmDsH=F$~{TA>|rmbG2WBf#`B;JkFEwLJ1=CP9oLSfp+L3#jtt5wRG1}wL4U->#EYV)#nCX`=l5wp%UfW+2)|hbhs)cMkwc{h%Evfx_YOI&xj{ru -^wSphg^Kkv+Hp-jcNaGxgF-vSM)OZh7PeD&h{V>Vg-|1ApSW2XX4a=*R##-A|9op}b;fHcT`i%kA@En -)Q-(R88K*w}lr>XrK=U5<#q(Z{W=n>*ZL9b4y-)ckAogb({1Rl=bp0Nw-9b0NDl~Kj9$Q9zIX`Y*}5) -biJ^@7ti{iaAjBL_n)&sZIr`hSDG -%j#kN1bkWhw8pD3dn3LuIUBqew}>;r#kCJS#D1UGwZC<&R(0HE|Y0~3-(rc*^W1cZXHOIWZ2D7f5IVoD$=OEbkl`KbP>hJTDl6e1jHN$OG5K4%4r8hzxQR?aqFp4_uY!-rL4*nAUdE9>c|TH+0jNnkCi3f#~WPyHs -|PdS1EMZobwTLMh&9@)6&h`EE1&+`ugg+$)Psx`! -6J06C4eCkgbNSa&&{e-Fv=yJ3YUAsWu&o1_0Y%o_Kq0wqHEXQPaZbbo71Wxxwy>ErAM9!S~RLKsvBZF -&wnEH>nD{UK-o%nnrPQpna~pbM8RX+~IyXi&bq6b1l7cs=ut!TISYQG`gV^ulR8wsa)snvVh!Mo7AY% -9Z3mDe(V?jtd@vN%Z(LIE|w1i^tg|{Dvt(Z(G@}9^(F@mw_iW9-?P9Uri&s!=xPlq`=7ACME%s==Rh# -`lkt{54>eyu*$xlf_kIA0IQ&UFKA)}TkWY@s^H~X{yf40d9vFB7b*f-<6Vv=NB#ET1Jv#*&ffB_xswB -!odJoJ(95d7SX4quXlE?Garbta*TR)~^=it&LE*o8J`1wcA_t_Q2E;SE0v<9q_JwaJxj*f5@oE%7289 -PQ%5Icj>$)c{rF**oSLK9%{Hx4nWyiomH%c!Jc4cm4Z*nhna%^mAVlyvwbZKlab8~E8E^v9xJZp2? -$dTXqD<)Lcnao0vC3&43?{rsOie9rKOFB`$Ehm)&hrp1W2m}}a6vb>dzx}#<9+&|^*;|(nr&3vX^z`) -f^t%UM6#1G((PCR|3lT+3t~Ys6F+MNzbX$o?-FG_r`D)E$bDfvws@&Vl9WOGOEz8aV5zl4D%UDW-b|q -d?x!4D6o9Rmhx8mh0m2(5OPQDLVx#H*a@5T4;I|?ie#Y-$UmCUo!!1g)}ekNHdVpzd_%Bw|QtbbBu1o -ZO0$Y1u;cqP{2b5?FQI8G$$-Sl?yNtPwRdPAnun{iR(MbPP+sW&`+;!9yCF6E1UwJlQf@P~)tXnZrCP -NT{Ff4`50*Vhk!j2^}TgWGp-(dhu@QJz)erD9j?HqQh;c`SHV5(53~<8T@QX>j|2r={pjrqTVEyZiAi -0J{&Tt!ang$$*PrLsmPYqiscK -&St4P56{~W%-sHfQc`9gc*kYT-$ac0V@?;wWpJ1>o^#~;Q2x}YSgU$@qo?y1KN#lw@ZIV3Y1m5T+du0;o`4bo|=Woj1gX*!9wI(gd -H0C2un3X)>~NQT;R}ZzY(mginIB?5@krB4+3-n1njZc3MLoCB|DT?@hV_EOBAD74sVN{EJf&1T8MI+; -%^?L2QN4Iy;=8r -izlgXh&*ormPB#Symjncz$t)2s;i_7v-tg5v6HnnA`$51Pxr8v)18=~mNb8P3iF(5;?I -b9bQnvKi2_`U1b+?gMB=(&f=wY7qR?w)*B{4c;}QJZ4_ -rj_h~+>B!wcl^TvV0Dge=Zt;f>(go(;#-(;r8l$QO-P9DXm}DkSFZ=!ek}WH<=$F}2IE4<2@dnUGo3a -}o1xiSa;@z_P?Ck7JPne;}5^A;2E+yz6zIDP)N(ImQpSpKop$--4gaU>itU0`}?pd-iEKDg$;sohpAy -o|K4^I+!!kM2j$#f@|X?xTTOLxxITF4@mOP!^_5wV59)6g_tdY-6`_zaz!x??h$P#>}s*#v7kw_yaIC -uQ4c`R_iUu~tG5@`Z~8i>uJJ;h6!_PO5(P(o2ar}Vht~=<4f5~O00@g -oSJK3e}{|-0^Jnsh0=cA89_`P`RzG;U)290=H!H>*|j}HOeClAwM%UJ!?ee@fSyYD>_2Twp2ynB!ceg -Ef9{}s#aMABd>Kjn#lzy}_-`+GtWr{JTVfxUc=7N!3chkaqGqqsGanXICySBi8IU_@Nyah^tKM?kopI -E|5prHjzOfEYBvJ>V1exwJNyJ@a&n_7>O8QSMWR^utLY!HJ)uRbEyZUkjTG9KgZEka$IFU>E=aLqm$f -Ztrf#El7D3ivl8FotPW|*9Of3cMrrLAEO1g{4rnhp;N{9>TV|CIBjaA*iUl -o2BrO}(DpOz4mT#*trCN@Qm5HETMtmqCQ0^bVPKq)(8I9U4$2s7Xr_}yplNG0nY;@TEzD(9EV3P3)#k -W27`kbh&U#W!G1HiZNqobJgnnA^VbVVRXi}n`OblJWwSd6Fb>;i?-FpVNgkJ>%+Ex2b{<~54`0rB@{BP|a=I_ro*?&Fv{6*2NoLCA*?pl*>s1 -!df4e`ry4=(hHZcdmaJ>4va=~W=1&$s)jRgc>pVy8H6@6(IvY9`}yrJ-B4Kwk0$LLPS{TZtPt=-WHAV -IuMfNftAc64W`B9{Z)}$ruRYM->&N7vpZR6aO+7)qim(jERjU8AFAg)4XWADS}p -52c!EP}`bT-@DbpK=fmn1+XhIU{kd0p;UPb5HMLq^>39q_8p94$Z7^WWRv_nW>s`4(W{Uc`3C}$za&P -Uen^gx7xE^eK)(a8q|sT2?Nd%MQmOF0&}E@W)$wE^3$WV|xkfb}#?-Evjag5i!au@&jFwE)+XDE~;x4 -H*KMy=|&Xvezn_CNDR@kYq5kF|VmS&J^gx7!GdKRFcmILV#2x1?j>`D8kNFZP=EZ!L!1>G6+Bc%`?-q -FpyPHb;jp&aQ2d1p_;Te&Dmc2U6pz4*~}7#$#4ap5GR`*p;=aMAg&M+6KFQW5M~DQQPiv03B3ffo)n$ -yUadrJf|7Z&l6oIh+z7#iyCLpI_;qgz7PTPYOn$7WeS)_Pt3tArG&>kDiz2`k_=Rt$Yz^!Um;*~ewS| -==rH4cu!P6zGy6n3eW4}o_7PZ|fyM=6Mh2vh2>P0G_#GxAfdS)KhZvP+f(h~K5<7W(FT8T(UglJzWX{ -R9$k4|?S1!M$R?F8XPmNYRb-=@1{R;G?5yf(=!by8w#IPErUDHR -avjV9c??LmkTw<485VVnT|uj~ljRCEUKPF!eq*nad@!g}Ei%Cyxd~xovLr7;6UuKZnL3@I$c~6@Clnu -tNA|nlgttY9f4O4+ppkBso}j;23Tw{S%W!zZdgXRrR1;;GXESZ(S;`?WNM# -15b)G+qz8^n2(phT@^&mwF7B#VDAZC&%AP%xNoe=IMh@~k5X$Ocu&7O*~(*&If@cWnu5_K!NTw&y5r= -jxnJg-(37qLZcJcIUT72}(n_i1RJ^55u?JUt60%s9URSk$!*qyG;Y!)=8b_5DudJjDTQM%^7R_&AqwYD!(vx+>d-$H<( -D215a-5SiOMOr5s%2GOrM(DGPEvfTi!C?nMfsQIL-_b15sId7xXqFU>^EvU`b+k{QGcm6k{ipM{ZV>D -UK88H*?Umn@tC!#bwYL6DbfS%gQ^0k`_C2uBEhU*E5m3}9%-a+LjANyaHPoVlSw?{7et_A9xRg0~o;fgQClfqplYvf>RAEOc1s%e+6bhZfM0=RU#eQRtC@HKJrM!|HxGbMApwDA -T+DJ++@kk3*7W53xSbc~^n-pDy1kb=LmE)9pa=d#Idn`!i8&Lokjzzqh3g+{WY$Y6iBN4vO -uaCC}_`S7eVA)MYb-rDe;YQJf&3A&Yp@D*)W4PKZ7h3Xt8R|7gSk7htam9MDPrnP; -yS{3WnjD}Ru0cY88z0$?k8p3gLEZD|xAzu|T(yh_`-kVVlzd^Xpxe!n-v#SA2@kH*%Ule-HJSckO`%6 -zT?zT>A)gCxGAd$$@xU-E-(}tw2Ovywx=m337H*|S8wpWl0QW2!2o0SO;!JbfZRHEYsY@yBgU)97z-SE4)ej$N$6UqtOxmJ5*@CqB1x -k#p{pZsts(&3iBeS%8>F^8a!M!aneH|@k6OE>?bR5C!l}8`aGLzXsiH&{&QqW~!jdC+yrd1-^wHJhDl -Ne;Mes+pY=cgt;O19)^brwtvdCLi;1a1zDK&kR1K5IevrL2sfzZf<9m&8@;L`zhMkqgY%SVlJflq>$Q -D=CD6$QLLxlVHnQ}(y!+|;8R;e=C?(5tHP57GRdlBFEb;h4d!wn;P7aC4dNgfJV_G -1LJ56o45A%vd&i$hD6o+VGfhf{Gw7Nj<0jUCAwDin&i)_cUFH$p}Q1XRUlwT65 -3i8eho5qN+IZt^~-}a`~>bRh_NZyn8U0weM1qOu!yHAO>O)baSB2+JVcn~;2SZ@bFgi7%hw$DfTRq&; -}|rSkRzsvP2z1GhIk576=Dr4nJv9!k+S|o)CpX@%p+A+$Amkz^ty1a|JFTEdsYdPFvmi=x1Lye$I=~| -j_kQT?%-k%JlwUH)%LtMxl`Zmu)8T;^ik}Z9SgtHD)f>>s$6x{*mPp@VW>bv+loD%)9AbM0jotJL@*% -6)|3d+bgXv*!~`OhpJbNgyOQ-x*;GX^SnfZJz78ku*(%l$;%dX+4otkZokXBGu0M#KV(`we)&%_^(0|cob>kJ -xyy3bg??h(@hqc4uL1VE(M2>J^$-6ok}PL1gki~3=937NYvR|s_%(~0Q>a}CKC+dXcACUI^FTT?z$aL -<2eu`;1d#OE!&N-iL!uoQ -2oCxi&xm|oRrG`-iV0E$QJh_x^qRk7MP!yNU&7}mV9>W_HL_gXk@lki#~UpaBv9cJv~+7x&jn7(b+PvDm3^#9jtNdqKC{t< -}fU!c^XKZO?OYnD3rumT{;D_@SD5VBb*JK=0xW|?fB+D@#yBO$2O0AtE+_5$dpk8zrBQkAE-vuNS5*Q -=F}G_%AD)UWgTlC9u{uXl3G)IoBmZIj?UY6Q-6a-=l-63v4+Lwz1L(WOIy~W&alx>b<)Y0%;$G#7~t= -FV?0BhHJ2uoD81LBR3-I4qMM#vmyLwBD(p-H_R-9V9rzaxk%92qnFH@z+oR?@c>SAO(}9#P;r*8j-H) ->8VCwX~dGHRL%JnT9o`PPBR3GldFnu^3O(s+@1j(PLPAu(MerwYbLB|KX^u2r6+q8L}r#<}vcu0r#>b -*j)ANgp!32M&X9gl}1Cm?dRU*fmj82udNg1Ukk8Wz%y<`C;yY!E6GT?(QX&L^e#?J^#H0JpU8qlOc9l -jlzaabM)|=_B=+-v#5_(ZiSf$MLlX4U~25n=Yb5v@IVOC`Q&5fBit0uN7NfQ7f;a^O{OK-({dQwj`Qb -%G_MD$pQ79_HO;I -D@I^Hjdd{nZ!(1-5HntyxKefd^Qv7mko(V>q$H~8Hl-q3aH{k7e{ufY70|XQR000O8`*~JV`M_-R4F~ -`L6B_^kC;$KeaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLQHjbaG*Cb8v5RbS`jt%~@@4A{Qg+;;|GhIqN+vBS8}!4q{*blBnTPZKqLsT!8^@#`N -&n62;J!<9w)A4d#6;T@#gN`+s8=x -7XYu;2#EcG@}=7w&2d@!|f4=}H(<#b&KJ>C5jds#xqmIK84LE3szzo|kgPoyZ|La?rzxXP2+F$z5r4s -qmjkbTsu&iXDZY2QDpIejsE}W0bQpdM{V-J3?dyS8p6!3bf9uMFbJZ6p0xcQ)mUoG~yq%G~Q?u?7rpB -8zaIF7g{=agAtT3rGC?bd20kyCFUZhteMh|ZMo&nnb-_7Hg3Sikx2p*#C>-E=XdFI{1`OTfQ=>{YLe$ -m?R;TsL68H9^3e!Lwosx@wT -&BHLlp~oTwAPeLkW}h0;BkuRE6Fl?;IJjKZ>0;$IF|$$uyAdAhVhTzT -+@2G06aYrJ+9(1|sGlhUec=5%j~M8(qSRJQBK);1h0-s82dig+Fk*$MTZN0@X#V!%-48Ritk-K{D4GZ -Ga`Fk9fdGKD;YzTJeFuA8@{OHeM7U5F=^^{ooL!{##MhEP@v;_I#%MDcV?zH^7#&!!3R5G-R-&L|lqE -J=x_Uf)rRZlOLjiC)$nABSF||B8VMBiZaw?E0ksff5mxxB2iI)m{+?lKJ6AtHS`$^-Q=1`Q_!V>lYZu -DBO9m#s0tjG5j20+jxm=Wd({!7^{+#Ff0fD(!o=c0XrV-&P_EN*)#4il7b%D83tYes*wf?GEkUykWHs -+r^CR7k!H&}8F=Chd~YvY;1v8QuquZr9vn&7({O@lQ~HCB7g3HtjHYz;EVkpBnvtWbq`~pmiyLnt!2M -9*lIti`q%m2IQpN4r(gauk*&d($sLx8WSW(fnze$K -{RW*M?2}s6JzmZ_6gErepRsqbp|9s54`r_@M-@!_Y&DC}O;=;ncLJ47LOmIk2|Guqg>Jb;AQx=M+n+a -21@RR^7Y!fzaVJu};7{H-abrh9ry0!=1zAhjDymJ1u2?|AcwW$9`Zr8{z^j=A02QmI2_~ZVN&6drSZ6 -c+S2ZV0?dGJYpuPv!AZ6KYY4fhWudg0*-0=H>=AAMWV$Hi@XuXIdt5$9LSqF~TL2}lX7Do>)fUYDW!^xTjAbFYkj4Pwd=TTbt2U45PDW9mdv=j2Yn?ir$EkHu*qxFTXtmV{Z1^YSJa -5yy+>N)%A?b5A67$2v)W=n%(9>6X-PaYM_Y8z>nsCnsVjuu-L>m(KJS;_>m8N<9wZqSGuy2GvDr>?DC -Wzj}DMd&sZuuBn<2zOU$ME?(#hd>H@~v-UxjkiPKZE3)9Z(lCzKs(^)_n~hD!&G6!Z-ca9scgK(i`j< -Nhm?fKc9N)DsXHcut?3N{Od$zhRS9B?q)b^!QP!1l^z<2c3jhEUCjbB=0001RX>c!Jc4cm -4Z*nhna%^mAVlyvwbZKlabZKp6Z*_DoaCy~QTXWmE6@K@xz}VxlG{eyDrs=lqy0dALO*50-WG2q`QG! -TFLPHTO0a{TX`rCWX!G(ZG#ZKDJw0cM+5;!=Q?_A+^r*>?ySofyy#A3nZu2r33e5F<08?msz&(7R)BX -{EL?1J5G1*^oG_qAbKm7j!trFxUGUW;qplEHtFa#?e&y?8Tw|)XY|^=)2!qTTag_>6%_ -+Hc`)i)b?~cIsxIuVJO^Txc(SU&B1OU4gW0OazJrg~)@@)3j&6Xb+wbz5jCF!nx8LVCbH5(J0gK>FY-Cbqu3Lh5N*WxqZwV=>a?XIupma%soYeo0O?O?P0R -+d|)?nT$hN@y}=EHVB@ndgra`dc{5ysB=!qJDETpP#Ud)QAy6ofZZVyJ5VpEt|aKJ7Gkp5xvXhFRBsC -<>UBn)Z({^qvHw``yl*B#wsErLPWwziOeuvj2GRUnwjCW#)85tvALIw -l5M{Gi_^cG?muo+^JlZUquSw)oxi!v8|2n=X8_o^_5RyZZ2!ZNn56*t5ZE=z3TB#Nye*p7deyMD*`PW -7MxwYD0Z$p*9vJ1RZLGxuEjpy9|szzKw$R&gPRZ5^r}zU#g$1&G1k?W9F_f@~|U_s`fG$GUg6)dvmd_ -#GK*JGBA_INWo1)^k=w}>Ss@%{b{+#136T=l?o9FPA(xlH`0i}1*&jRpjVtu%<1j)pg|FG-)_y7Fnb{yJPzLVM&1twAa7F -;Te;1)YX@-`B7$34Tc0v;>Zg&R6K{T$bpxr?%Kux|O{_@9Ic-7+;gI=7Q4Br~iSoeKyfmt_DT1%pG#@A}?~vhW+L4?tQRY6it4DjV_R9>^-UDO0$>0e89!3M)$201Zdz8svU6%krh=PVp -t%j8f!qSI`$5JbU_e`sj)O9iBH)ihiuBauLc`$9R4I -ROhdo1@#tm2Y&)WW)uFyB_3f3bO#RP!5m}>N=L=sM+`8Yum#m6 -bD_0z-_GYpBuHx~_v2gGjQ<_)w0?MUC%uNP^#ABoZFpxQ_J>ShHG~#$8j6H=E`5HfL;Zf)IUV5_tIEr -6e$Fo0ENmkjK@zg$^2MToguGZ}$$~_oQ?E!BXLQG5)OtTfNoGE@DFq{gUYCz)A8oFpbR3nAOhCS9(wV4aTDSn0Mr -Lo9#>}(`PK9VJ-g5h4XU-2;wPzsD_l2QR@O}`VJES&~wU;?2Tc&9@5A8csDL@q5?B0G+jFcBGVmG7HOLmVOV8nY;1N1)+5HVNQiHj(W? -DY)4c5xJa>iVhLg^UG9lxXbQtN-&7AYkkEA^vqL_6E5m&P>Vm#cIX*Y`n -+X{~|VK^VB;gF>JxWRF4_;Tru&_6H_PTG%XzYw1KlHN%Y-*a;Ti9U-SboECjtF(CuScy -kLoXfZgly=QN(-a+cu;X#O>y#Zkh+UXLO-=0D%N4%=^+Mnd?Su4xENKX0t_4#2aeAHNA5XPQP4#iiN? -jjA@tIqdz^OT0Gr%O@Nygai$;hFaj=FuemkP(L7+*G8d3}0X>E0)P&%PF^f@kkh#880ZlY=%Kx%IXnzX~gBxa-yX6O-O@ -&Y^NkT8y$wB;!IPa($+Yo-AcyKrKi1bRoH!&65u=)q%}$bqL(<+_tLI7E43ln}d{lCC-7(CtMvY3>U28|B$9FuuMF~0y-dP{g>1k>%fOIzA4lgRvil)Mmvzw`hDQYr -0r4er9|BmD6A~DDFCgceIzKUo0P*(9(E%g?!YoWr1xL7Rz`IsoV^xihunx^Vgp7Cq!)zgOhPx{K&1$$ -7}WcEoQ?v-~CF0^Cf0?d$|z;d=UI~nNHK%Jt&=l)~~c17c5Q?59@%}#(Hq@FDMBK7%k7yYK8D~%}#j< -prREKlfsO%f!G#c?h9bA;&aj(?urWJA!gI&>LpvT&Ja5y^QpN~lM%)+_WYOf(rS9g~k`Fy={rDcF-z{ -smA=0|XQR000O8`*~JV=c|?Zr4j%D-!=dM9{>OVaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLiQkE^v9R -J9}T-NRt2iQ}l>sEe%E{ye^{~hLGSS>=DQsnB5(4glx63HnODAgWx>wvtLzpx74y^Nbb(R87#Nz)z#J -2_2^8~c+OmRTI5B_U6%#(Bu;bYjk7o`a_)-XosJw&d1s0klQ<0dBoE>!Qz(%)=YC!!A@2wn+vX1wp45 -O&_3}K;*a5pXvdN6kxzS^WSL8GNp2b;?--toqo*4hL;O -PP%rm1%~=TUBqI-NE456Gbkj2=Z#Pfc#0C}U -$*&^X-Um|qg4~DM>XP5490Ai&a0h1u)W@@Zodz;gv&DUF-zl^?Ye>1F(kiP}=gL&ux^yj0^KSuH!ur@ -YFKRaLh7GPctPa0?)1|NZIwjkSpZ8_GVA>jb5-#4#6+}!jJ*Wi`*=K2GTXplFDFt`MD=&YGGj{L>wi7 -{?pot?ZoKOV5J_~ZEe^62!m*MVg?dH?&a`~K(>B}U9^Qo^&|&bJ%e`m*z_`fYUH4*n@Y4IBoIf!^@tF -1$Rw*x6QZfzFp1fA}W;e*F?8_q*RL!=9oM<|YgtNLoFH?+%m{R>#eXF>Zk`20$4OE8&bV+u4Qz#phec -z4y|<6MGK!49#?nJ@VN%+3eXqGaQ3i49O7+O1q9SS(#&f+5fg~;wvkYx?4rjaDdESJ4MH-(cEj&yk4S -)R7Qg4?o(1)hfDN#+vxxNOsAvK-&sjtCwjPw*-k67?G?ijXoT|@boObJ+njH4NhXahSVzAdT6`lTmJN;GU_H}YV2hNkLv*F-kF#LURT>6Tmm#>Zouiv~q`P -+Y;zB@aA|M%g=<<;;1_~*a=@WvCLPw!^IzdwfaC{F%Nv%I+f^sxB6^=x}*cW?hcp8sOIKizEpw}I|wU --KFU+hE35!~S<1MCSDaTW7)_fa72fa3nEp&yOqw24DEc`PsldIo8OmZ|{9wx6qp*!6o4VY4ml;{58Ld0$%NuNeHaeF_!#9JrQ|W`*m+rp -q=Fz=_cL(RL-1joB@j;5ct0s)FG>w&@`wdW`iboJCT}iZ;!=qP&)4|1s`-23ox1G)Hks_mX+rTtiHGe -ZAO}KB*FD~8p=fg{>ec)uSXJ6?*R3V(bq;}jdbT(RGUgB)l*G=pjrM&W&CM}+u8nw@%F9KKdu_47vWKMycu0U=J#UZ0mR4Z*&$WkO%!fOd>;9 -hl+U3a?HGVP0V5*9XAAUk!%;Dj{DFkKVk)U`!2YbP(+jFgl${=w%RrrlDB{+@=A)foK^nTb3gFDwX&XgSj&nmD%!M({3hytUcO7y@S -qMO7s1MLwHR7z@nGAe)E+)B=Zz)EMz5UdA_r2t>((93orhO(t>bqgW714`07x7^&LjeCWY-voOvxNLa -w8EincL0OJw=&uv{Jd1XH3zDvRQre% -M*=rFn)4h6#3}K`XZC()+ma@rO=JEm76uww>593M8yv}(3DNaAKP^RnJ)Wp69Yo(l9o0b -wA$Lo?xL5y{XC4B=Am{RS%3a4p=Ppu6scZ=-h^Q$$z{>EIQcdkb2+F=zb2o8;j__(%^uBzZyt3! -9KKq+bgC6Z?XE;`;H?7h1pOC2)LfY06o>*0M7DBd;&u37d2dRFH -hl;Q_zseq@O}63`$a9_GV+w!hJtMVEL?l(mzYKw!pI=!sW%QeRxgPl4q%*s -ycoDy^i#wV0!nbMQgR7sv!Rd_jvtAVU?@b?5p(Y4V0XTTA%r=Zk`jqz4z2i-&+H%i>$PN4l@HWV;93oJC5b60?dk{plc$Sbedm2Q(>DM+HzWSAI)s1`-V+ -K<2TKJ=+fHgyocpM94`Jzsuk{SLs-YC(1!J(bH8mft>HVq_evO?eK=;POpL>qy5Un>$D>=|=lq~t9(o -6Dm~>~lAW;LHwucQS(`)Wjeb2uE-dcp(Pk@r`24Th&0l|(9;f23oNsWwO5vW@PD6AqaRo2i^2_ZC8Ask>I$A;7nyGuEL$g3GkQ(gm#stdy(f{gO -fW!c7yoJ&hxW0&A{VJaEtC>(BTZSEBJE8U^IE2b-HWicL^6HrU`5rRByyL8vP4A%f6Hth9`$x~psBrr -v52DqkDAZ-aH4_EQW5`5KRv9n?vRVh^H&BwkcJeniE>^lfJ6LSmVkpvSktEXueaiq>5q<2XrK)+fxA) -hJj1*{g7p2m0JxG{J!#9jCvT=!;8 -F+EhYX*W1oW*^6a=o`U+#?b7NjtLEsMq~)ZPxme1@iA=)9K*{yv+|4+JESZTTa-M7Kf{XSjgpGL|hnX -Gld}_z(U4NF(L)DsG%S3;9bI;zkcgdWRj2C{qiM)xFBF_$F*#wVQs+f?Qpg3Yy=H`_Dh2UWD>{F=1GZ -s0<0|)GSgor0mkO#scAvzV;F`@wi7;vNO$tg)Bw948gOE7aKjglw1j -3ItowF(s&_NpXT}0dvA?pFRU236K|VxQE|Tt|U%Kn9$Rr{WTDV*D6UQoZ#Oo6Ob{>v*u)4nS`jaiP#B -3wKB$(<^aNqd?`+~SW>tlNsU7HXp_LyVcAx|Bhn@YsFF626i?S%BW?dniLn@(=IAmF<2xh=y8?Rf4G- -d^MJ0N^&oH{7dPXJTnu^zPI{=8s@)R;|9O=rmTTKF1QcMBnU{lE&mfdQKD;K?#Rh4`CvSlT}N+(g)gI -!GHOj!k@Y*NyeX<)5L8o;3d(~c%wVL)6l4k9mIxcMUCOJ0=H$Xf7$c^{5i^+KCWb|cnyRijQ54u&AP- -v_d_PL54Sx1?QzpDL6(9sCduP^#KGb_VBXD;-Ho1PYBe_#u11kP%Nc5lLi;7n#qRgz)nj>;hs{CKBmu -!gc_8)M*6=-V&o`*X!GR(y|pj^b9!KE@Yjve7fFoMy=31%=nMcdA5R11u4$K_Ap#R^%`BCb_GfcC+#_ -;laZ>V0}I7+LjZtcTVz{3KaF^!JTEWnG@lNrR1JgF)(^u|22WC{t7xLZ -g%dESCk7&Y}|>ApZzamD`ENn>fP+Vqhpdq-ZhFt0H>d$8DV6i=1CZiLZYW}YtUlcsA?Ns!p?J!l?-P; -s9O*;Z!;T})#q^op*bEwxENUEmfo?jpISx|-M6Oyeq2Q34f5v6oh+V(7WMD1o}6Wmvri(R$mc(qZc~r -DBn$ttnGkOpo0BBtN)J3yweh9XAEO84Xy=iXbv4$7e_XKe{N4GhE89R5`I$v<1gfURIDhQF0f -_Tqbv~WOT5?0SSs#!&e|F4(&LyXsk7nEwz&#DNRa2%Fa<$n3U=k08+74oge;RDh;XvV1aGIGI=8mpE= -a0c5dyfG`!XBhT7~U=zWt~+9HUrZ=TZm{bjo93$dLcQ^Bh12b6 -y>ZbK?@Bs7Y`j>jxK|{c=UeSO1*2SupCvFexjX^0_ZJWW0M+|ai;k@UL~W&>an97Lez1Qi{CBX%6j10vfh>Nv}*0KHTGH-Z8Gm8wIa* -IlPz5vf-nH$^OpJuU3wFjY()Waszry8pkTW-z^fhtP$G}{B9>(&S$3pnyjLe7F3*q89X2HM5hbxt*sT -OX#wqtcu9DQ&&xN{Kw#9qszn<D{82l1Ky-P->EP)h>@6aWAK2mt$eR#T!zzj43^000~n001BW003} -la4%nWWo~3|axZmqY;0*_GcR>?X>2cdVQF+OaCy~OTW=f36@KThm{Jd-T#8m}1OZ&LfeSdOkyJ8l1%3 -z&Vz_&joN{(&Gc&6df&Tb>XD-VnMLF(UOW5LN&iUrtFRSX-STD%h#!OWF;z(2Bhi`i~sjR6*)is4TMm -v#CT4}wKdNT2L|9T?u3Upr8lu2sS7;hR?Oz5$o){FPy_jlGB`wUC?EZvB8vRX;4S1Yj;zfNz>XSu(}A -LV~E_LC`g6yc_GG41i|sxo$ENtQ=H^j3MAewj=rg_h2VXJKaVf}qMYS5d$gVBA9faHEl7Q*Ad?~U;LmYgt^c>CeK6MNH%LN=mvBp^`$P|aq9@mlyT&SX>UfmNH5EY7_2RCPL -nXtg6jt%A!Ua@Co1&aB#a@h<|jRt!o;+7xX}ECP;(3vc08nu$ -rbMhmiOen1RLkrD-nxz;z^H)OrriV6YQYu@Pk9JA_&^|JWW2@ySH5M_g7Q8g+Ih;7qpc>}nB$;MVkWu -|=?yfu-0Q1L`&To~Su+v9OWt2XWW1kfq|VlUn@Js7G@1!EvD8A|s$WsG1EhAtc96VVVa>2QFv?OVo0Q -h=ZY=W+5P0C6#8&Xm2(It?fJn-&Sq5*G*Ti5wE4F6zo}&E39h0u`b3)YZR1)*`ky}1)o>y5qNxCFeGk -H(z33trHYQN0o6XMrZhb$d`J)AG$w=2APfZ8B%K!@_I?Y%(vV{(w$?nTlB2J;vTex%j)hnL3c=R-AjP -El;Y}sc``8UyOsYagajompY-cVHOlX*S5hxHzZL(E8y-F+YQC5_WWlq@4YLo -4kJD}?*SngbNZ9j?Ung&gSFWX{yYLAADKhcJQ}YUMZ;7L$ppxJoZ@rGxw{)l^3OeJN(I=Hk0l#V*9ZV -m*mAp2v8x210I-j8V1)2`vRWr+HD4DEyzWokWlkJkk -k5ahBe|L+j@{eqhThq|tECR_A&LlL7N*n2CQRya8`%5L`JTx@-ETQb?rnF1F!a9h>x?< -Z$1umI&}>3DzY=ejqTO?`dm~;gzRQ8Tpn)WvF2!FL-yb2Rt~yJ2$E;V}M(L?^7vho@(r(xzOq{(7HqYH0&k7zpA-!7|9 -Trp35%nQpV4Cloh=2>?_kG<061+?4VNDfKke`c|TQ3ByUu7nQ0)nbYT%wUzRyx-$G3U%&OF&Zzb99-BljEw@%=Hp1M4g0J2 -J+CxW@W0iy+*=`r%Uqx=N|QUGM)e5o+mnLlJor_2KkC%Uno5-2MFX``c&Y{tWHiFZXdBVcVGXzr}DGX -N04}g{6%`;M-(1#=DxNK)}8erU{nM@hFgn3?GKap}{s7FfbZ9;;3^&qb|)V(4Y3@IM(?yUD=BlOYxVR -n?rF;(J)Ax04GmED60_Fubq`7$UV%PJhByaTi^XuQV$z#|x{nn`msd*ie516C5C`|K -}D;_3S+%S<=(ImsD^|nQBfekxMFmx_LX_p%LEskM4q&u!0&hUm4cSR_kGdJ!V3du=|XJ}D&6vo9o!(b+io --S>!t2{VU8OnSS9u8pbQ(-S1JyXpvLwUm$g*$&ETC9Wrv4&&MA%^E;}2p=Dp(Q4sid8SC67e -R>=8;6l6>?K)&$nMw>Od;EAPj5qdtP`E|m?4dhIjPM!i9HdYBI&vIXx#t+l4LY^Oj4(?~gK4|H2R$s> -&t%}#2aMWzVg7!T2xgs&HIDZsBr(wqBJ=NDU&1X8Da&?K_n@K~5H&|2;#Uwt@*r*4}&w!%*AA>+H1a~ -N|hv5WOmcf`i?Dru(GEJ`C^w=t!eGWmsrsb%u#0YKKMn8FJE72ZfSI1UoLQY?OOeA+ -eQ-qUr(_Yqo9OJjFVh@E)g5B9i=hqI6>sJxJv?RMXoH?)KcIsE$fN~d5r_^UM^2^Gy6d6^$5JyY{s}=6Y9Zx=cP$57l|P7hmw~YxyU$4maCL48JAQ -h@HLwgxnN}a)3f8tpS~w!BaR43c}(+I33NJTM7c3wrlP5zWK=S$7>G)#O6s;^V&$ -A^7!+9Z#oP{BtFv}$=cmmq8Fij-OX)1a>vw%+{Vfg%%&R8le)A{kq_~JFAe^mRG&PhL0yaU2Otn&|!zckHagoU6hV6#tJ$3$3ll -f>GD*h#v(E{BLv14Tr4I|%Z1L^@tDoPB%RAfrOA)$bF;08qG01FIW?I5N2>10B#*=C*Pu_#rI*^R$`q -?C(buKzF%zuu%NG8H6{2ki}65p^Xja!UL?^%AAo|u -NCMdT$XIv*PK?m=`V0ahL?##@=yE(e?7Ad~O0~b{0tj8PKr4%{qoX5xXux6NozUp*#V|;rpKUk3u!c3W-rMV`N -T+RDht|<}P1itff>kql%6Ni+u$?*7u+)9C!n(vk%$Ld2B*D&9dua)mJbz!EE(8hb4tQ*to(&%yK}<7{fpQt)e@t+_Qv!<{c64XQm?2Hp@bl8F_KAOXJq -dVhcayP)<# -%QDp@KC48u!m7YH&svamo$Xd!NDO$4ZCJ+Tss36JZBfwn3S?pCrD#NLgdmAE2Xh?`#mcxY4vyuf|&gu -eIs9VhCD -#5qpidu`59ZzKZpRqm0m+S&}Zz(JX|0+}{!1%OuoZRlep-UG^tUF|6ftnY%nzI-}5&~;ozMg)e2NGQ8 -1Raq2bDrRGQ-zRABWL8KP1S$lp@_tI8VMrpH>NH{Q9F_VV0-S76iow1LG%r9-_btXs9F{B;;25XLtOV -bC6Sk-Lb_U|>D8v_DWP*9Y0fmM*XIEHs@@WHTDhNTzM22Bsuyn3-Pc6JF?<0Y4GMBPXZK0i8oYU)#a< -P@W}rjaZwo)29+TTi`vY;P0ucD7F_NEIj!3e -B4-j@NLRqS+Ab7S7)s3O-#d*@Kpg7cuvz4ySFm44GF_o5r-ZaAcdmpH8R^1#gD!Q>a=k+FlkH)$KGJJ -k(R<;Z|%BMzpyRuS+tfBCROi`*+F*~N}VZ*Gia3I=Exvn-QWpMUOe|}*;kz$oVQu2xKZDMm;7o^pCI;tge$1r3%?Lz~r+-5lj(dov6fXLN(Ht5qu-%lE_Qx9)2e0 -ZYx1(jtk$}8(2UW -xB_9AYpeYpD5Pt_{KqG+Aah80_{+sHl9Wc3z*DM*snv0b6fU)19mSK|*0yvV*+@qkoWA529;P_wkS-;!mX&@AX!OTGw644b(J$UBvpA9BfIK)V3}8Y0Da7sm~hwl9C?l1^(80~H4yz-33t -VIeWq|%Ev#&z53LEyD&}CQF@GC27;6Pk&GYI7RQK($He;wWc6Cn3nO;%>8!Fcsk8<5ar@Dp8+qJQbHN -|_yIH)Q!~973HDNU}fGE-=^(l#&R9Y?E3n~6gQO$|6Hh( ->t>z=e4ew8`gbWyd|%K=0?wd&`CS{nWYZSWhy!r<_W!@~X$@>x-#M`s{yY1`KYOS<1L -RXfwB1|x&kY-Vsrgv=+4F)*gw_pls6I4SX9fNUi-Q8sYb;f*Sgll4#La7EVH>X+>r#q^j(3Av2*LUqcsCKoDYz^ICn|X>Q+If&>bld)$i6K -ZCH*DlFd8EdNcnIw$3nhcqm`Yj)cMXSCMAmg~n8Arwbt3F;^+pKp7MFwWEs3SWS6$=4q^d;L6KV1n%7auce-0N! -e`4gumjNS(;>ucFI?|{{|it{0|XQR000O8`*~JV$E^s91qJ{B6C(fsA^-pYaA|NaUv_0~WN&gWcV%K_ -Zewp`X>Mn8FKl6AWo&aUaCwcIO>?t05P56$CCXS}KJ1wu`hFZbllGDx-W!9lVk^er0o*w4w7vX%dbKeroxkI(FW2fN+Cs+?5^sS1|GC;5 -EiHc5W|EY$u`ZP?fQpUhbbhiyOEt2*TwdyUU3yV0piWLC-z`||qjiIq~am*2jA|Ks&Ld&>*9lU1zJvR -3`r82&T+@#6>kma{CcnB1xAr&{#qL6z)HrIOXZ<4cvF;z}iPntrmq%w!Q)a@Q<}!;4TWdOFQG -DfUl=PtZ)Q$Z2@;t9B0_rfla_e6!0EQ-SL3CM!*qFfJ4AnWWZM_;49jh6%4poPJ>wC2)MEZe1+mT!^R -Gz1Kxw_YCPa8Bj5-oz#-sC{SZj&gTA?G0KFd6E>jvklo^lq+Jl){S;FJN%y^c}K+v0GV>H*s4x}^FgK --gWVQ_S?Kf~kll=EO2+H5or`$ptTaw`y^ngO=C)~zK#nVjN!$5<=eN4fUtI1(V&ycq}8B-m7s6HG6SO -&kHybD9F9xn6f5{p2DtvoEAPA}|1%u~yFjrg;^uooP^Ca2iBy69H-xGCToFp(Tz1OkrS2Aq0a-ulvl* -u(1Q_6!u_w$s&1)KsX7o->xCOuR~lE2e~ce2i&ebs9&a>2~lQ3+G_%&w`&Mf=+4lHR~`Yag&U&G%vib -u=`*|s|5D{8PG`h%NFjaVhV|KnBT1fT8k*+&=8XufPohI<_@n`Kvjf*R`X3wFq|&o&34_Ec$4C<-$&` -BAqDkoX4BzSQU7%)LSXyI53R_z)@GF6!TUmiRPhn|<5Giausv!EhHN86S*Mv7sVsMNUwvJ8U25WNlbU -jfagh!U$p*?qxxe5!o)dI>vibL8c? -#gQsFY;5Ijjp@POCARBEoJecvR(dhyM!{T&nYY<2FFMfB*{vZ+;^5U?9@f$b|z=xcu?g)wikg?^9B;V -a!N0WwxAd@KoAB9+5iCz5OxE4G|oyDUESvu7i+)YJ388rVd64Tcn^|jyF4E4&}b;|nL?xeX-d|gjy4l -Jtd>UmswQg{qV3pdIS0i`N70dN;UU~CTRYTfnJF|y1!zZM^zG=SU9{ac~kXjPT&wuqK -1sDffc9$IGG_6+km46c!U8SZGZ<1fFm{vZ9&|qYp72c;L`^9zyLU4aec -x0lXAz?sB8xR5m;2ScHHk}w8-9oVeUyenrGS#BI5rGf{56wrXLEr(HNp!~Q=#KF3|{ENUTtKi{G58(hkSuAIUE)RvaBbR2_g^Vmo=QWuSrNm( -A$TlKEYaJIszk=|dE%IFwp@38$qTr1!}osElTa1gO@_k+R}@4Cl`yK+h+=GxRA(MxobkYh1Gbsc*CQf -Mt9g1T>1{s*XUEzBpo2_5r78gHK@R7OeE*u-_C#-OzoYJD3IK@CNi0?<7wC -!2qM5dIdN=!4k6P+)K36ksW>ZNRSlQp$qwO0z2{d-HSc&#fdO%`PWRd~Ab!^u*0#voXef^OHu~d7kBPEXg=8fk)&tNdfGVtSoYm}9$x!l^K`6<{Mg^f -FZ|20z3cif_6hB*Yc!%L3D0FA2m0*iznS4y7@-|pNP?z<`6e)@b1>l4g*qa@6k<}#rLuTWYX!`=L01Q -roykN4AW^C@hUff^m-!g}gKD1tXy83)f+$;_G4I8tnkYG7}K;A?0B{!eL?86GRt4j$v`)EH&1LJEVw_ -GsPf&@8KQ1I$`9t`zEkhFWKQ)p?>wJCN33GHGW;g60up(Cv+EcUnd-CI&I7Pt;OrW}JlyED0e`sn9F8 --u5a5d}SKC2xzTwD!9fCNY?xhhu*_tv495!QaQkyyLW!+$BtXraDm{zZ0}UV9Iu!Vr(TXar0%X{p-)* -h`*Js(&AlwYGq1}ko~13yOBH{^pa!*}7d}N)o);nruPND+eAMG1;yam^B7DUB9p{qt4!0F{=vYI5o==*JC#wW(*s&BSy%v5({j4qf96 -gGj2aXh(pD$a1M~AYeeM%BuLpvT?{~kR_0z;rJ3-Qkfpk2a`a88z|9QpOp-%8H34#~+Ma?vJ)6wiuF8 -bP^l7rueA3^ZjK8jO@>!?-L<(zcE7Do6W%B0J~w_~xb)tgsrZ;;>`xuq~9?-kh5$*cj}20Z>Z=1QY-O -00;p4c~(c!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZbY*jNb1raswO31T+eQ$ -+>sKri1f&87o52y%s3Y-g|TVOZrPX`y!Vw>tRQ*Oma -EjH;mWu5nw@`~T+Anudr3H^Ap>e@^eTz(k -ms#Zo+iZe5WQQ+2}fx}!35%kU{ZoZrUiTEy&J~y*L2-Mv(xcpHtztfknbSrJzNFOGmG*;zmYatpYte4 -$Q_9|4$q``(y`ZMD8$;A%jt2u)oLxt3Sq`b!o37jMwm-B0aN?DB_G>qjiFB?(hgjF=WO!lH|g|Msz9-29`;Z#EI*i(Itp-{q_y(Ip| -HU%D5@A!V9qvT3qdb@*Jt?yN=90r`?P29YH52NH5fhe%V9xa=NQ1y!kGYXg5)`NEVThU+^8d!3IPbx` --np_U*?9eha5Z2D#T5;m6;KD7bb{=O53{(fzGBvuuq6q7Q31mUAU(n|Dv6Wqi-NgQIsxwN_FVcaM+X4 -ZDNAU?Ju-+7B{2y*$4-K#Q=<<*MAczTbQK6c}DPO0&gFJL%*FX0 -PsIJjl&0usCpeB5jaaYku8?LS!rL3CjUe)oJ36wDj*j6pMZpoxVK$Bh`SV?LHrcn@TF^zC|a+##03{q -B?EYXF&!4eJWWjj^bq9(zLAqE8b7>3AnYa|W;LpUMm8xEm7tMo%Wj`IYRhXfLU5$>uS?169!J*eHTWT -Q?wfys}TxY~(MBdJX#d$Guy_BN9|vT@{X)^`@0$#k(9kKqf~;D^Nq55ZYSGdk^URHjoq|NOoeuF%=9u -J?u-z57Ay#)N~=*+5~Ttqsu`Tx_>lS}!)oeGx2EX;pps@0UOF5|h1aKgCcHK7=XXbk$rHhuqF$*WF4W -hpWTk4rT$RnE}5(+8p{_6G?p*7YS=j66aUWDd?W^TQvUssnwuhI)N{c_xp>p5#g -zbWD{VG8B4t(Giw(%(m;$el+MW0WPFiOjtf}K+-zs8eoac&^Wy;MZAr-;Hmh_=$~$dh -|^K;+9|nr$O{%EL0*Zu^&)Jl!gbCC~Iv)g0`4r-I=$3+KhiDiSX#p806)%Qk@UdQXUNL3E4@t=pRLbn -L`CYuV$m$9MwMg-atj8LnWk^>OgkGh>G}He}OdHj)&l$VQ6N4^x=5dOyW9GIONuX1#H;6Wd$tI6HvEP -u4C1Ds>;c9tD21iU#&!-7PN%TCLEa$5c5sh?5j+!=lV1%=DD$+U~eLnTg_fddzH5GQe -NT<{3*`6vKGc3!S{GT1JXS-DLc!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZcwcpM -WpZC+WoBt^Wn?aJd8L#?GDJy{lHCz?!}ldi_!-v6r?ILt*lTVy -n&yp&}+&&Aw`a!TTQXSLW4FDCphy?WY6%@;Q|y_}XKapXfW=tl#Q+=z>G<`%k;i!`tA&Qe8rK_l__;< -zgcBJt$x=;Z0ei7<03G?g;V3b%CSW$b(=CKIzPmzE}zRef_?ie;X<5@?d8C0UsjjRp^_t}2%{wzjrhF -)k5@=V$JM%4AX0SK9SWy(;O9Y6<67dr`Zf%ydPXFP3FjkhODLj9W!%Q&qOy?Sg-nTunLe*i%z!XL2#& -H8y5<*BzUbGRdeDwsdp<_m;fItu3poxFxsXELXOE!r4hxtM&c3JHlJv*Bf^lWHvr^{ipZr%W9e4Ji#6{j$FL@lcQWg!pi)T+Jn9Q& -PXh5nwRa?YJ+Xt65B#e-MJH~akoAa(O@XZog$Y!5dG`ZgMPo$X>6Vg*Y>*2tykCmerp{@AsmEoH-ryE -xEI3x5FUhZ=)=+nc^FJWsC;O{H433whf%0B3YA8o(kN6Kg-WAPX%s4rLZ#6#gmDOE2t&1z3ZV{Rs5kQ -U3g!pQCUcAVA#se8B87cbGlqCrsZg-zncF-y`23-yPo?-xuE$-;ojXQ|2S)nE4 -s=bLJP!L*`@V5%ZXN!aQYu$^43W#{8Q34f6@}Tjo>dcg%C<_snO^3+8j?56mB#FPJ|ue`fx|eCZ$cD- -K>W-!Lc4Uzz?K`9tJSkUu{D?D&Io$(%A%=5Ng3nHh7&^l#UkxnTam^siUJ^snWT`6u&=dCk0G{>A*8` -4977=G*l8%pWFM0QOoo`mm`F?#OcW*>6T$?2V)Tj8Cq|zbePZ;9(I-Zq -7=2>&iP0xUpBQ~&^oh|YMxPjcB>G77k?14QN1~5JABjE^eI)uw^pWTz(MO_>L?4Mh5`7Z%Nzf-jp9Fm -p^hwYsL7xPD67)&XCqbVCeG>FZ&?iBk1br0xDD+Y2qtHj8k3t`XJ_>yl`Y7~K=%dg_p^rizg+2;>H2P -@t(deVmN28BMAB{d5eKh)L^wH>}(MO|?Mjwqn8hr?T2z>~B2z>~B2z>~B2z>~B2z>~B2z>~B2z>~B2z -?Ct81ymdW6;N-k3k=UJ_daZ`WW;v=wr~wppQWxgFeRjbn1VvPxu2wRR8xjHoxV<*N6YIN|tG++qb_>{ -{v7<0Rj{Q6aWAK2mt$eR#TliG(h+O003nH000jF0000000000005+c00000aA|NaUtei%X>?y-E^v8J -O928D0~7!N00;p4c~(;+8{@xi0ssK61ONaJ00000000000001_fh7R|0B~t=FJE76VQFq(UoLQYP)h* -<6ay3h000O8`*~JV&BwqszyJUM9svLV3;+NC0000000000q=CN!003}la4&FqE_8WtWn@rG0Rj{Q6aW -AK2mt$eR#TR>aG_BF002D#000>P0000000000005+csRRH3aA|NaUukZ1WpZv|Y%gD5X>MtBUtcb8c~ -DCM0u%!j000080Q-4XQ(rU*uN({j0Ny4502%-Q00000000000HlF21^@tXX>c!JX>N37a&BR4FJg6RY --C?$Zgwtkc~DCM0u%!j000080Q-4XQw7u2l@$vB0O2G602TlM00000000000HlG15&!^jX>c!JX>N37 -a&BR4FJob2Xk{*Nc~DCM0u%!j000080Q-4XQ=-7pTFMUq0AVu#03HAU00000000000HlG=9RL7uX>c! -JX>N37a&BR4FJo_RW@%@2a$$67Z*DGdc~DCM0u%!j000080Q-4XQ{;JOYzzc!JX>N37a&BR4FJ*XRWpH$9Z*FrgaCuNm0Rj{Q6aWAK2mt$eR#PVcqo?Qq002}000 -0#L0000000000005+c89o32aA|NaUukZ1WpZv|Y%gtLX>KlXc~DCM0u%!j000080Q-4XQ#;j?=0FJm0 -52Q>02%-Q00000000000HlF5KL7x5X>c!JX>N37a&BR4FK~Hqa&Ky7V{|TXc~DCM0u%!j000080Q-4X -Q;uiz4&(>`0QndI03-ka00000000000HlGeNB{tEX>c!JX>N37a&BR4FLPyVW?yf0bYx+4Wn^DtXk}w --E^v8JO928D0~7!N00;p4c~(=hw_o*?3;+PvF8}}@00000000000001_fznX`0B~t=FJEbHbY*gGVQe -pVXk}$=Ut)D>Y-D9}E^v8JO928D0~7!N00;p4c~(=-cj`1~0001l0000T00000000000001_fuddj0B -~t=FJEbHbY*gGVQepBY-ulFUukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#VjdaF^N#0093O001KZ000000 -0000005+cMPC2_aA|NaUukZ1WpZv|Y%gPMX)j@QbZ=vCZE$R5bZKvHE^v8JO928D0~7!N00;p4c~(<) -AAbh82><|Y9smF#00000000000001_fna9<0B~t=FJEbHbY*gGVQepBY-ulIVRL0)V{dJ3VQyqDaCuN -m0Rj{Q6aWAK2mt$eR#VV?GE^v8JO928D0~7!N00;p4c~(;(W`@cs0RRB_0ssIc00000000000001_f&GsF0B~t=FJ -EbHbY*gGVQepBY-ulJZ*6U1Ze(9$Z*FvDcyumsc~DCM0u%!j000080Q-4XQ)*dce2@eH0H_H702u%P0 -0000000000HlFvkpKX2X>c!JX>N37a&BR4FJo+JFKuCIZZ2?nP)h*<6ay3h000O8`*~JVm>Usq2Lu2B -HVOa$AOHXW0000000000q=7G%003}la4%nJZggdGZeeUMV{Bc!JX>N37a&BR4FJo+JFLQ8dZf<3Ab1rasP)h*<6ay3h000O8`*~JV{dxd -PSO5S3bN~PVApigX0000000000q=DJX003}la4%nJZggdGZeeUMV{B6he1Pj1ONb-4gdfm00000000000001_fpE+K0B~t=FJEbHbY*gGVQepBZ*6U1Ze -(*WUtei%X>?y-E^v8JO928D0~7!N00;p4c~(<6gYW%*2mkOV00000 -00000q=Dht003}la4%nJZggdGZeeUMV{dJ3VQyq|FJowBV{0yOc~DCM0u%!j000080Q-4XQ|fU;s?`G -k0FDa)03-ka00000000000HlFW+yDS@X>c!JX>N37a&BR4FJo_QZDDR?b1!3WZE$R5bZKvHE^v8JO92 -8D0~7!N00;p4c~(=T(w7#`2><}_A^-p<00000000000001_fo9+U0B~t=FJEbHbY*gGVQepBZ*6U1Ze -(*WV{dL|X=inEVRUJ4ZZ2?nP)h*<6ay3h000O8`*~JVT3!3Cp$Gr~OV0000000000q=9c!JX>N37a&BR4FJo_QZDDR?b1!6XcW!KNVPr0Fc~DCM0u%!j000080Q-4 -XQ~$H#<-r300EY_z03ZMW00000000000HlE_`2YZLX>c!JX>N37a&BR4FJo_QZDDR?b1!CcWo3G0E^v -8JO928D0~7!N00;p4c~(=u$ot^q0ssJ~1^@sa00000000000001_fhhd|0B~t=FJEbHbY*gGVQepBZ* -6U1Ze(*WXkl|`E^v8JO928D0~7!N00;p4c~(=2!<0Pg0RRAO1ONaY00000000000001_fkyxV0B~t=F -JEbHbY*gGVQepBZ*6U1Ze(*WXk~10E^v8JO928D0~7!N00;p4c~(;`3unU81pok=5&!@n0000000000 -0001_fo%c-0B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WX>Md?crI{xP)h*<6ay3h000O8`*~JV9AJnmU>g7 -c%WMDuApigX0000000000q=9@00RV7ma4%nJZggdGZeeUMV{dJ3VQyq|FKKRbbYX04E^v8JO928D0~7 -!N00;p4c~(>7Sb)bq3;+PDF8}}@00000000000001_fg2c!JX>N37a&BR4FJo_QZDDR?b1!pfZ+9+mc~DCM0u%!j000080Q-4XQ!gWy@Rc!JX>N37a&BR4FJo_QZDDR?b1!vnX>N0LVQg$JaCuNm0Rj{Q6aWAK2mt -$eR#OXid4LQD000;m0018V0000000000005+cK1TrnaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZfXk}$=E^ -v8JO928D0~7!N00;p4c~(=?bqy{%0RRA60{{Rg00000000000001_frm~30B~t=FJEbHbY*gGVQepCX ->)XPX<~JBX>V?GFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVXbO;_`~d&}lmq|(BLDyZ0000000000 -q=5%e0RV7ma4%nJZggdGZeeUMWNCABa%p09bZKvHb1!0Hb7d}Yc~DCM0u%!j000080Q-4XQ;*`ja$Nx -c0RI9204M+e00000000000HlFLQUL&PX>c!JX>N37a&BR4FJx(RbaH88b#!TOZgVebZgX^DY;0v@E^v -8JO928D0~7!N00;p4c~(;vfl9b{1^@uw6#xJv00000000000001_f#*{J0B~t=FJEbHbY*gGVQepCX> -)XPX<~JBX>V?GFLPvRb963nc~DCM0u%!j000080Q-4XQvgu2Xxae)09ynA03-ka00000000000HlGSS -^)rXX>c!JX>N37a&BR4FJx(RbaH88b#!TOZgVepXk}$=E^v8JO928D0~7!N00;p4c~(Md?crRmbY;0v?bZ> -GlaCuNm0Rj{Q6aWAK2mt$eR#Ts`S07&@008)n001Qb0000000000005+cA9Dc!aA|NaUukZ1WpZv|Y% -ghUWMz0SaA9L>VP|DuW@&C@WpXZXc~DCM0u%!j000080Q-4XQ`;-Z>hA>r0G$~C03HAU00000000000 -HlGzl>q>7X>c!JX>N37a&BR4FKKRMWq2=hZ*_8GWpgfYc~DCM0u%!j000080Q-4XQ^k>Mqx%p50Bkq_ -03!eZ00000000000HlHJn*jiDX>c!JX>N37a&BR4FKlmPVRUJ4ZgVeRUukY>bYEXCaCuNm0Rj{Q6aWA -K2mt$eR#Wqj+MRm{008e6001Qb0000000000005+cD6IhiaA|NaUukZ1WpZv|Y%gqYV_|e@Z*FrhUu0 -=>baixTY;!Jfc~DCM0u%!j000080Q-4XQ#cE&dK(G=0PY?D03`qb00000000000HlHDwE+NdX>c!JX> -N37a&BR4FKlmPVRUJ4ZgVeRb9r-PZ*FF3XD)DgP)h*<6ay3h000O8`*~JVv-DZ*7XttQD+T}n9{>OV0 -000000000q=7`h0RV7ma4%nJZggdGZeeUMY;R*>bZKvHb1!0Hb7d}Yc~DCM0u%!j000080Q-4XQw(z& -$U*`D0DJ}j03rYY00000000000HlGK!vO$rX>c!JX>N37a&BR4FKuOXVPs)+VJ}}_X>MtBUtcb8c~DC -M0u%!j000080Q-4XQ!azP_Xi9B0ADKr03HAU00000000000HlE$#sL6uX>c!JX>N37a&BR4FKuOXVPs -)+VJ~7~b7d}Yc~DCM0u%!j000080Q-4XQwc9KIy?pd0O1n=04D$d00000000000HlFk(g6T)X>c!JX> -N37a&BR4FKuOXVPs)+VJ~oNXJ2wPD002J#001BW0 -000000000005+c-q-;EaA|NaUukZ1WpZv|Y%gtZWMyn~FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV -6)>Prc>w?b-U9#tApigX0000000000q=8r20RV7ma4%nJZggdGZeeUMZEs{{Y;!MTVQyq;WMOn=E^v8 -JO928D0~7!N00;p4c~(<{l00000000000001_fe+#V0B -~t=FJEbHbY*gGVQepLZ)9a`b1!CZa&2LBUt@1>baHQOE^v8JO928D0~7!N00;p4c~(>3m#NLd0RR971 -ONaX00000000000001_fxG1a0B~t=FJEbHbY*gGVQepLZ)9a`b1!LbWMz0RaCuNm0Rj{Q6aWAK2mt$e -R#RFqbX4mM003Dg000~S0000000000005+cxaR=?aA|NaUukZ1WpZv|Y%gtZWMyn~FKlUUYc6nkP)h* -<6ay3h000O8`*~JV8yxcLYykiO;sO8w9smFU0000000000q=DV^0RV7ma4%nJZggdGZeeUMZEs{{Y;! -MjV`ybc!JX>N37a&BR4FK%UYcW-iQFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVt357KN(}%2o-Y6Z9 -RL6T0000000000q=BRg0swGna4%nJZggdGZeeUMZe?_LZ*prdVRdw9E^v8JO928D0~7!N00;p4c~(z?C000000 -00000001_fzm7j0B~t=FJEbHbY*gGVQepMWpsCMa%(ShWpi_BZ*DGdc~DCM0u%!j000080Q-4XQ`3)D -F}no-0NW1$03HAU00000000000HlGgLIMDAX>c!JX>N37a&BR4FK%UYcW-iQFLiWjY;!Jfc~DCM0u%! -j000080Q-4XQ!TH1U%CPS0RIL603QGV00000000000HlGXNCE(GX>c!JX>N37a&BR4FK%UYcW-iQFL- -Tia&TiVaCuNm0Rj{Q6aWAK2mt$eR#N}~0006200000001Na0000000000005+coJ#@#aA|NaUukZ1Wp -Zv|Y%gzcWpZJ3X>V?GFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV>CLabA_f2e^%DR9ApigX000000 -0000q=Dc|0swGna4%nJZggdGZeeUMZ*XODVRUJ4ZgVeVXk}w-E^v8JO928D0~7!N00;p4c~(=zz6u&A -3IG5qCIA2;00000000000001_fk9FN0B~t=FJEbHbY*gGVQepNaAk5~bZKvHb1!CcWo3G0E^v8JO928 -D0~7!N00;p4c~(>WI%Nj382|ttT>tia -P)h*<6ay3h000O8`*~JVz2<6y83F(RnFIg;GXMYp0000000000q=7Jb0swGna4%nJZggdGZeeUMZ*XO -DVRUJ4ZgVeUb!lv5FKuOXVPs)+VP9orX>?&?Y-KKRc~DCM0u%!j000080Q-4XQ>U8^`|<(+0GS5>05J -dn00000000000HlGMdjbG(X>c!JX>N37a&BR4FK=*Va$$67Z*FrhVs&Y3WG`)HbYWy+bYWj?WoKbyc` -k5yP)h*<6ay3h000O8`*~JV;ye;Y=m7u#Cjc!JX>N37a&BR4FK=*Va$$67Z*FrhVs&Y3WG{DUWo2w%Wn^h|VPb4$E^v8JO928D0~7! -N00;p4c~(<_$cJSK1ONc%3jhEv00000000000001_ftP~<0B~t=FJEbHbY*gGVQepNaAk5~bZKvHb1! -0bX>4RKcW7m0Y+r0;XJKP`E^v8JO928D0~7!N00;p4c~(>5-4uH@0000p0000i00000000000001_f$ -WC@0B~t=FJEbHbY*gGVQepNaAk5~bZKvHb1!Lbb97;BY%gD5X>MtBUtcb8c~DCM0u%!j000080Q-4XQ -{h^v-UR{x01^cN05bpp00000000000HlFyhynm`X>c!JX>N37a&BR4FK=*Va$$67Z*FrhX>N0LVQg$K -Wn^h|VPb4$UuV?GFKKRbbYX04FKlIJVPknNaCuNm0Rj{Q6aWAK2mt$eR#T6qSSr -FG000zg001cf0000000000005+ce2@YFaA|NaUukZ1WpZv|Y%gzcWpZJ3X>V?GFKKRbbYX04FL!8VWo -#~Rc~DCM0u%!j000080Q-4XQwXG`bdLi70O<+<0384T00000000000HlG1u>t^aX>c!JX>N37a&BR4F -LGsZFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVWn*>Aln?*_wL1U+ApigX0000000000q=8Sh0swGn -a4%nJZggdGZeeUMa%FKZV{dMAbaHiLbZ>HVE^v8JO928D0~7!N00;p4c~(;=qt)2!6951WL;wIC0000 -0000000001_fg;8N0B~t=FJEbHbY*gGVQepQWpOWZWpQ6-X>4UKaCuNm0Rj{Q6aWAK2mt$eR#R!EM9= -9W000bx001BW0000000000005+cNZJAbaA|NaUukZ1WpZv|Y%g+UaW8UZabIa}b97;BY%XwlP)h*<6a -y3h000O8`*~JVBLu(1a|i$cpdA1J8~^|S0000000000q=9e!0swGna4%nJZggdGZeeUMa%FKZa%FK}b -7gccaCuNm0Rj{Q6aWAK2mt$eR#O+blrnz>000#b001BW0000000000005+c90mgbaA|NaUukZ1WpZv| -Y%g+UaW8UZabI+DVPk7$axQRrP)h*<6ay3h000O8`*~JVbYEXCaCuNm0Rj{Q6a -WAK2mt$eR#W90%c=hW002h<001BW0000000000005+c@+JcSaA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHF -JfVHWiD`eP)h*<6ay3h000O8`*~JV000000ssI200000D*ylh0000000000q=7Fe0|0Poa4%nJZggdG -ZeeUMa%FRGY;|;LZ*DJaWoKbyc`sjIX>MtBUtcb8c~DCM0u%!j000080Q-4XQ}{OHU%wOp0EkBb04o3 -h00000000000HlF>C<6d+X>c!JX>N37a&BR4FLGsbZ)|mRX>V>XY-ML*V|g!fWpi(Ac4cxdaCuNm0Rj -{Q6aWAK2mt$eR#N}~0006200000001ul0000000000005+cf;|HOaA|NaUukZ1WpZv|Y%g+Ub8l>QbZ -KvHFLGsbZ)|pDY-wUIUtei%X>?y-E^v8JO928D0~7!N00;p4c~(=y03zQ=1pokK6aWA#00000000000 -001_fzdq!0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RWo&6;FJfVHWiD`eP)h*<6ay3h000O8 -`*~JVYKm(DsSp4FB1ZrKF#rGn0000000000q=8~X0|0Poa4%nJZggdGZeeUMa%FRGY;|;LZ*DJgWpi( -Ac4cg7VlQK1Ze(d>VRU74E^v8JO928D0~7!N00;p4c~(dXaE2%00000000000001_fm& -1p0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RWo&6;FJ@t5bZ>HbE^v8JO928D0~7!N00;p4c~ -(;?)mp#21^@s_761S@00000000000001_fy{3M0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RW -o&6;FJ^CbZe(9$VQyq;WMOn=b1rasP)h*<6ay3h000O8`*~JVRpR%rEerqv^&c!JX>N37a&BR4FLGsbZ)|mRX>V>Xa%F -RGY<6XAX<{#OWpHnDbY*fbaCuNm0Rj{Q6aWAK2mt$eR#RNV>E!MN002)F001)p0000000000005+cw} -t}%aA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHFLGsbZ)|pDY-wUIa%FLKX>w(4Wo~qHE^v8JO928D0~7!N0 -0;p4c~(;z7guBI3jhFYB>(^~00000000000001_f%c070B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3W -b8l>RWo&6;FLGsbZ)|pDaxQRrP)h*<6ay3h000O8`*~JV000000ssI2000009{>OV0000000000q=7A -%0|0Poa4%nJZggdGZeeUMb#!TLb1z?CX>MtBUtcb8c~DCM0u%!j000080Q-4XQ%ZO_Kh^;N0QUm`02= -@R00000000000HlFzm;(TCX>c!JX>N37a&BR4FLiWjY;!MPY;R{SaCuNm0Rj{Q6aWAK2mt$eR#O9VLc -*5<004mo0015U0000000000005+cdzu3PaA|NaUukZ1WpZv|Y%g_mX>4;ZVQ_F{X>xNeaCuNm0Rj{Q6 -aWAK2mt$eR#W6-jcert003ME0012T0000000000005+cPMre)aA|NaUukZ1WpZv|Y%g_mX>4;ZV{dJ6 -VRSBVc~DCM0u%!j000080Q-4XQ?|5;v2z9h009*M04V?f00000000000HlF#p#uPLX>c!JX>N37a&BR -4FLiWjY;!MTZ*6d4bZKH~Y-x0PUvyz-b1rasP)h*<6ay3h000O8`*~JVY!f}Mm;e9(@&Et;9{>OV000 -0000000q=6`?0|0Poa4%nJZggdGZeeUMb#!TLb1!6JbY*mDZDlTSc~DCM0u%!j000080Q-4XQ_!}>j! -Xpr04ojv03rYY00000000000HlHar~?3SX>c!JX>N37a&BR4FLiWjY;!MUWpHw3V_|e@Z*DGdc~DCM0 -u%!j000080Q-4XQznk}UF`z^0EP?z04V?f00000000000HlG5t^)vYX>c!JX>N37a&BR4FLiWjY;!MU -X>w&_bYFFHY+q<)Y;a|Ab1rasP)h*<6ay3h000O8`*~JVxg+o|3jzQD;RFBxB>(^b0000000000q=CJ -%0|0Poa4%nJZggdGZeeUMb#!TLb1!6Rb98ldX>4;}VRC14E^v8JO928D0~7!N00;p4c~(OV0000000000q=AOG0|0Poa4%nJZggdGZeeUMb#!TLb1!9XV{c?>Z -f7oVc~DCM0u%!j000080Q-4XQ{g5JlS2Xk03QSZ03rYY00000000000HlG@x&r`kX>c!JX>N37a&BR4 -FLiWjY;!MVZgg^aaBpdDbaO6nc~DCM0u%!j000080Q-4XQ^z}7-{%Mb00kES03iSX00000000000HlF -by#oMnX>c!JX>N37a&BR4FLiWjY;!MWX>4V4d2@7SZ7y(mP)h*<6ay3h000O8`*~JVqt#C>SOEY4%mM -%aAOHXW0000000000q=94;ZXKZO=V=i!cP -)h*<6ay3h000O8`*~JV5m0CS*#-ar%Mt(p9RL6T0000000000q=9+O0|0Poa4%nJZggdGZeeUMb#!TL -b1!INb7*CAE^v8JO928D0~7!N00;p4c~(=`e-Wdi0RR9S0{{Rm00000000000001_fsNDy0B~t=FJEb -HbY*gGVQepTbZKmJFKKRSWn*+-b7f<7a%FUKVQzD9Z*p`laCuNm0Rj{Q6aWAK2mt$eR#O=t$&+0T000 -av0015U0000000000005+cde#E~aA|NaUukZ1WpZv|Y%g_mX>4;ZY;R|0X>MmOaCuNm0Rj{Q6aWAK2m -t$eR#V=;&!#;a007WW000{R0000000000005+c6XXK`aA|NaUukZ1WpZv|Y%g_mX>4;ZZE163E^v8JO -928D0~7!N00;p4c~(<9v<$F!0RRB01ONaX00000000000001_fr4;ZaA9L>VP|P>XD)DgP)h*<6ay3h000O8`*~JV$Uak^jsySzd<*~p9{>OV00000000 -00q=EMZ1ORYpa4%nJZggdGZeeUMb#!TLb1!gVa$#(2Wo#~Rc~DCM0u%!j000080Q-4XQc!JX>N37a&BR4FLiWjY;!MgYiD0_Wpi(Ja${w4E^v8JO928D0 -~7!N00;p4c~(<7l-3zA1pok95&!@v00000000000001_ftL&f0B~t=FJEbHbY*gGVQepTbZKmJFLPyd -b#QcVZ)|g4Vs&Y3WG--dP)h*<6ay3h000O8`*~JV$(R6($qWDhN+$pSApigX0000000000q=5_)1ORY -pa4%nJZggdGZeeUMb#!TLb1!psVsLVAV`X!5E^v8JO928D0~7!N00;p4c~(=8MVb@a2><}@9RL6y000 -00000000001_ffOGE0B~t=FJEbHbY*gGVQepTbZKmJFLY&Xa9?C;axQRrP)h*<6ay3h000O8`*~JVJg -Ie^3<>}M$|3*&AOHXW0000000000q=76c1ORYpa4%nJZggdGZeeUMb#!TLb1!vnaA9L>X>MmOaCuNm0 -Rj{Q6aWAK2mt$eR#SeVju$xt007?x000{R0000000000005+cb~6M3aA|NaUukZ1WpZv|Y%g_mX>4;Z -b#iQTE^v8JO928D0~7!N00;p4c~(4;ZcW7m0Y%XwlP)h*<6ay3h000O8`*~JVFRk9iF984mR00419R -L6T0000000000q=9lo1ORYpa4%nJZggdGZeeUMc4KodUtei%X>?y-E^v8JO928D0~7!N00;p4c~($_h7T@?TTj70zd7ytkO0000000000q=ENI1ORYpa4%nJZggdGZeeUMc4KodXK8dUaCuN -m0Rj{Q6aWAK2mt$eR#TU6$*GwI002=F0015U0000000000005+cieCf(aA|NaUukZ1WpZv|Y%g|Wb1! -XWa$|LJX<=+GaCuNm0Rj{Q6aWAK2mt$eR#R%uV*EA^002xa0018V0000000000005+cUu6UUaA|NaUu -kZ1WpZv|Y%g|Wb1!psVs>S6b7^mGE^v8JO928D0~7!N00;p4c~(<%F(Ir$7ytl{R{#Jb00000000000 -001_fzopX0B~t=FJEbHbY*gGVQepUV{MtBUtcb8c~DCM0u%!j000080Q-4 -XQ>83)(Elp{03N*n02KfL00000000000HlGik^}&7X>c!Jc4cm4Z*nhWX>)XPZ!U0oP)h*<6ay3h000 -O8`*~JV1`i|L^ZNh*@+$-Y7ytkO0000000000q=DkT1ORYpa4%nWWo~3|axZXsaA9(DX>MmOaCuNm0R -j{Q6aWAK2mt$eR#VdT6U!aA|NaUv_0~WN&gWV_{=xWn*t{baHQOFJWY1aCBvIE^v8JO928D0~7!N0 -0;p4c~(=#V!TZ<0RR9c0{{Ra00000000000001_fq&)&0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gPB -V`ybAaCuNm0Rj{Q6aWAK2mt$eR#SzRWD^nr006fF001HY0000000000005+c@aF{paA|NaUv_0~WN&g -WV_{=xWn*t{baHQOFJo_QaA9;VaCuNm0Rj{Q6aWAK2mt$eR#TS#Z*33|002cd001Tc0000000000005 -+cLg@tnaA|NaUv_0~WN&gWV_{=xWn*t{baHQOFJo_RbaHQOY-MsTaCuNm0Rj{Q6aWAK2mt$eR#WrQ{u -=rN0089)001Wd0000000000005+cmiYw$aA|NaUv_0~WN&gWV_{=xWn*t{baHQOFJ@_MWp{F6aByXEE -^v8JO928D0~7!N00;p4c~(=DC2d}>1pol%4*&or00000000000001_fz|y50B~t=FJE?LZe(wAFJob2 -Xk}w>Zgg^QY%geKb#iHQbZKLAE^v8JO928D0~7!N00;p4c~(=m$h}$^2><}I8vp<$00000000000001 -_fye^}0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%g|z0B~t=FJE?LZe(wAFJob2Xk}w>Z -gg^QY%gPBV`yb_FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV{LU}Q2?hWFIS>EZgg^QY%gPBV`y -b_FLGsMX>(s=VPj}zE^v8JO928D0~7!N00;p4c~(=ukX5ii0000!0000V00000000000001_f!P)Y0B -~t=FJE?LZe(wAFJonLbZKU3FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVFCeYqo&W#<{{R309{>OV0 -000000000q=8l!1^{qra4%nWWo~3|axY_La&&2CX)j-2ZDDC{Utcb8c~DCM0u%!j000080Q-4XQx_bf -z0f5B0EzVj03HAU00000000000HlF27zO}vX>c!Jc4cm4Z*nhVWpZ?BW@#^DVPj=-bS`jZZBR=A0u%! -j000080Q-4XQc!Jc4cm4Z*nhVWpZ?BW@#^DZ*p -ZWaCuNm0Rj{Q6aWAK2mt$eR#T(rX47{B0074f0018V0000000000005+c8bb&GaA|NaUv_0~WN&gWV` -yP=WMy?y-E^v8JO928D0~7!N00;p4c~(<7zl0F)IRF3_dH?_)00000000000001_fzC$=0 -B~t=FJE?LZe(wAFJow7a%5$6FJftDHD+>UaV~IqP)h*<6ay3h000O8`*~JV%jaiiJOcm#-39;vApigX -0000000000q=EW@2mo+ta4%nWWo~3|axY_OVRB?;bT49QXEktgZ(?O~E^v8JO928D0~7!N00;p4c~(> -4Stva{2><}YBme*>00000000000001_fpvul0B~t=FJE?LZe(wAFJow7a%5$6FJow7a%5?9baH88b#! -TOZZ2?nP)h*<6ay3h000O8`*~JV#p&<`cLV?c{|*2EDF6Tf0000000000q=EO22mo+ta4%nWWo~3|ax -Y_OVRB?;bT4CQVRCb2bZ2sJb#QQUZ(?O~E^v8JO928D0~7!N00;p4c~(>4mtk7J2LJ%}6951t000000 -00000001_fwhwe0B~t=FJE?LZe(wAFJow7a%5$6FJow7a&u*LaB^>AWpXZXc~DCM0u%!j000080Q-4X -Q(XHuNTdY-00s^K04V?f00000000000HlGon+O1KX>c!Jc4cm4Z*nhVXkl_>WppoNZ)9n1XLEF6bY*Q -}V`yn^WiD`eP)h*<6ay3h000O8`*~JV*bE&BS^@w7umk`A9RL6T0000000000q=BKK2mo+ta4%nWWo~ -3|axY_OVRB?;bT4CXZE#_9E^v8JO928D0~7!N00;p4c~(=sLYgsX0{{R&2LJ#f00000000000001_fi -|QF0B~t=FJE?LZe(wAFJow7a%5$6FJo{yG&yi`Z(?O~E^v8JO928D0~7!N00;p4c~(c!Jc4cm4Z*nhVXkl_>WppoPb7OFFZ(?O~E^v8 -JO928D0~7!N00;p4c~(=w3SgoX1^@sKDF6T*00000000000001_flIIm0B~t=FJE?LZe(wAFJow7a%5 -$6FJ*IMb8Rkgc~DCM0u%!j000080Q-4XQy@3Q&?*H00HqE903rYY00000000000HlGLwg>=lX>c!Jc4 -cm4Z*nhVXkl_>WppoPbz^F9aB^>AWpXZXc~DCM0u%!j000080Q-4XQ}jUtd0`j;0O~XV03ZMW000000 -00000HlEfya)hrX>c!Jc4cm4Z*nhVXkl_>WppoPbz^ICW^!e5E^v8JO928D0~7!N00;p4c~(;<1(*0Z -0{{Tj1^@se00000000000001_fuht10B~t=FJE?LZe(wAFJow7a%5$6FJ*OOYjSXMZ(?O~E^v8JO928 -D0~7!N00;p4c~(=*+ey_kIsgELdjJ3+00000000000001_fg0Ed0B~t=FJE?LZe(wAFJow7a%5$6FJ* -OOba!TQWpOTWc~DCM0u%!j000080Q-4XQ=mbO7&rp}0MiBl03rYY00000000000HlG75(xlsX>c!Jc4 -cm4Z*nhVXkl_>WppoPbz^jQaB^>AWpXZXc~DCM0u%!j000080Q-4XQyE3l*JCFD0P9cy03iSX000000 -00000HlEf76|}wX>c!Jc4cm4Z*nhVXkl_>WppoRVlp!^GG=mRaV~IqP)h*<6ay3h000O8`*~JVeyjeE -HUj_v+6DjsBLDyZ0000000000q=BV92>@_ua4%nWWo~3|axY_OVRB?;bT4OOGBYtUaB^>AWpXZXc~DC -M0u%!j000080Q-4XQw=H8vY8S901h?)03!eZ00000000000HlE&K?wkGX>c!Jc4cm4Z*nhVXkl_>Wpp -oSWnyw=cW`oVVr6nJaCuNm0Rj{Q6aWAK2mt$eR#Uc!Jc4cm4Z*nhVXkl_>WppoWVQyz)b!=y0a%o|1ZEs{{Y%Xw -lP)h*<6ay3h000O8`*~JVq|tlo_ZI*F^MnBaB>(^b0000000000q=C(~2>@_ua4%nWWo~3|axY_OVRB -?;bT4dSZf9q5Wo2t^Z)9a`E^v8JO928D0~7!N00;p4c~(>R2%Q%i7XSd*fdK#}00000000000001_fd -|eB0B~t=FJE?LZe(wAFJow7a%5$6FKl6MXJ}<&a%FdIZ)9a`E^v8JO928D0~7!N00;p4c~(>Ngpv<-8 -vp=ekO2TG00000000000001_fo0_h0B~t=FJE?LZe(wAFJow7a%5$6FKl6MXJ~b9XJK+_VQy`2WMynF -aCuNm0Rj{Q6aWAK2mt$eR#V+O6}2NM003+N0stof0000000000005+cA^{2jaA|NaUv_0~WN&gWV`yP -=WMycaX<=?{Z)9a`E^v8JO928D0~7!N00;p4c~(<35Ym-B8vp>1lK}uE0000000000000 -1_fr=>#0B~t=FJE?LZe(wAFJow7a%5$6FKl6MXLM*`X>D(0Wo#~Rc~DCM0u%!j000080Q-4XQyVYVGZ --8I0Lpd&04D$d00000000000HlElMG63LX>c!Jc4cm4Z*nhVXkl_>WppoWVQy!1b#iNIb7*aEWMynFa -CuNm0Rj{Q6aWAK2mt$eR#W8c@T8Xp0082daA|NaUv_0~WN&gWV`yP= -WMyJaA|NaUv_0~ -WN&gWV`yP=WMyAWp -XZXc~DCM0u%!j000080Q-4XQ^$+OgLe%80M{@804M+e00000000000HlGBkqQ8CX>c!Jc4cm4Z*nhVX -kl_>WppofZfSO9a&uv9WMy<^V{~tFE^v8JO928D0~7!N00;p4c~(>Pzq~y~1ONce3IG5h0000000000 -0001_flQwY0B~t=FJE?LZe(wAFJow7a%5$6FLiWgIB;@rVr6nJaCuNm0Rj{Q6aWAK2mt$eR#S&;{S~< -Y008m;0015U0000000000005+c(4z_faA|NaUv_0~WN&gWV`yP=WMyV>WaCuNm0Rj{Q6aW -AK2mt$eR#TG(*D?bD00031001KZ0000000000005+c#iR-VaA|NaUv_0~WN&gWV`yP=WMy?y-E^v8JO928D0~7!N00;p4c~(MtBUtcb8c~DCM0u%!j000080Q-4XQ&QQjhKd6K0NM!v02}}S000000 -00000HlE#z6tc!Jc4cm4Z*nhVZ)|UJVQpbAVQzD2E^v8JO928D0~7!N00;p4c~(=)#1YeR3jhEW -DF6T?00000000000001_f!)Ch0B~t=FJE?LZe(wAFJo_PZ*pO6VJ~5Bb7^#McWG`jGA?j=P)h*<6ay3 -h000O8`*~JV8*fE&g8~2mdj|jjA^-pY0000000000q=Apk3IK3va4%nWWo~3|axY_VY;SU5ZDB8IZfS -IBVQgu0WiD`eP)h*<6ay3h000O8`*~JV2pbF$Pz3-092Ecn9RL6T0000000000q=8b<3IK3va4%nWWo -~3|axY_VY;SU5ZDB8WX>KzzE^v8JO928D0~7!N00;p4c~(=RDAwlg1pojh82|tu00000000000001_f -!);#0B~t=FJE?LZe(wAFJo_PZ*pO6VJ~-SZggdGZ7y(mP)h*<6ay3h000O8`*~JVU2hWoT>$_9MFIc- -9{>OV0000000000q=5+B3IK3va4%nWWo~3|axY|Qb98KJVlQ7`X>MtBUtcb8c~DCM0u%!j000080Q-4 -XQ!LZH4qz()02iVF0384T00000000000HlGU-3kD3X>c!Jc4cm4Z*nhWX>)XJX<{#9Z*6d4bS`jtP)h -*<6ay3h000O8`*~JVhCpNd_%8qebH@Mx9{>OV0000000000q=7vN3jlCwa4%nWWo~3|axY|Qb98KJVl -QN2bYWs)b7d}Yc~DCM0u%!j000080Q-4XQ!?pD%+?eD00U6~02}}S00000000000HlF(IST-AX>c!Jc -4cm4Z*nhWX>)XJX<{#FZe(S6E^v8JO928D0~7!N00;p4c~(>1NpBz`GXMbn$^ZZ#00000000000001_ -fr3s80B~t=FJE?LZe(wAFJx(RbZlv2FKlmPVRUbDb1rasP)h*<6ay3h000O8`*~JVc&0UHY!Cnd+c^L -L9{>OV0000000000q=Dgq3jlCwa4%nWWo~3|axY|Qb98KJVlQoBZfRy^b963nc~DCM0u%!j000080Q- -4XQ}nBO_}2yi0DThx03HAU00000000000HlG6k_!NEX>c!Jc4cm4Z*nhWX>)XJX<{#JVRCC_a&sc!Jc4cm4Z*nhWX>)XJX -<{#JWprU=VRT_GaCuNm0Rj{Q6aWAK2mt$eR#Tn5)qt=I002ZP001BW0000000000005+cy}kc!Jc4cm4Z*nhWX>)XJX<{#QHZ(0^a&0bUc -x6ya0Rj{Q6aWAK2mt$eR#SN)bMBJK0001<0RS5S0000000000005+c>eLMYaA|NaUv_0~WN&gWWNCAB -Y-wUIbT%|DWq4&!O928D0~7!N00;p4c~(=jFEei;A&*;@br9smFU0000000000q=B -mE4ghdza4%nWWo~3|axY|Qb98KJVlQ@Oa&u{KZZ2?nP)h*<6ay3h000O8`*~JV92gG9H?RNz0AK+C8v -pt03QGV00000000000HlG`u@3-nX>c!Jc4cm4Z*nhWX>)XJX<{#THZ(0^a&0bUcx6ya0Rj{Q -6aWAK2mt$eR#W>LvixVj0001n0RS5S0000000000005+cSlbW)aA|NaUv_0~WN&gWWNCABY-wUIcQ!O -GWq4&!O928D0~7!N00;p4c~(=j{uZzcDF6V!rvLyP00000000000001_f%uyd0B~t=FJE?LZe(wAFJx -(RbZlv2FL!8VWo#~Rc~DCM0u%!j000080Q-4XQzP$Z{KEhM01^QJ04V?f00000000000HlFE#Ss8-X> -c!Jc4cm4Z*nhWX>)XJX<{#5Vqs%zaBp&SFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVuZk~a&H(@b% -L4!aB>(^b0000000000q=84q5dd&$a4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mZE163E^v8JO928D -0~7!N00;p4c~(=#IR?{F8~^}oWB>ps00000000000001_fmp~90B~t=FJE?LZe(wAFJx(RbZlv2FJEF -|V{344a&#|qXmxaHY%XwlP)h*<6ay3h000O8`*~JVZx#Tp_5lC@ISK#(D*ylh0000000000q=D|_5dd -&$a4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mb9r-PZ*FF3XD(xAXHZK40u%!j000080Q-4XQ~r7Dqp -2PM0On`_04e|g00000000000HlE}=MeyKX>c!Jc4cm4Z*nhWX>)XJX<{#5Vqs%zaBp&SFLQZwV{dL|X -=g5Qc~DCM0u%!j000080Q-4XQ$#`^Ltc!Jc4cm4Z*nhW -X>)XJX<{#5Vqs%zaBp&SFLYsYW@&6?E^v8JO928D0~7!N00;p4c~(=2mv5*<0ssJr1ONaa000000000 -00001_fyQ4F0B~t=FJE?LZe(wAFKBdaY&C3YVlQ7`X>MtBUtcb8c~DCM0u%!j000080Q-4XQ_80;;Vl -#Z09Zi)03iSX00000000000HlFPViEvwX>c!Jc4cm4Z*nhabZu-kY-wUIUukGzbY*yLY%XwlP)h*<6a -y3h000O8`*~JViZ&5F4j%vjVSWGrBme*a0000000000q=B?{5&&>%a4%nWWo~3|axZ9fZEQ7cX<{#5X ->M?JbaQlaWnpbDaCuNm0Rj{Q6aWAK2mt$eR#P<&;B8MG008hT0RSQZ0000000000005+c1eOv2aA|Na -Uv_0~WN&gWXmo9CHEd~OFJE+TYh`X}dS!AhaCuNm0Rj{Q6aWAK2mt$eR#T4CiAmiC002W10015U0000 -000000005+cld}>4aA|NaUv_0~WN&gWXmo9CHEd~OFJE4;YaCuNm0Rj{Q6aWAK2mt$eR#RIt113 -O7000O^0RSNY0000000000005+cthy2aaA|NaUv_0~WN&gWXmo9CHEd~OFJo_Rb97;DbaO6nc~DCM0u -%!j000080Q-4XQ?aYPpWj3P0K&-u03!eZ00000000000HlE{0}}vnX>c!Jc4cm4Z*nhabZu-kY-wUIX -mo9CHE>~ab7gWaaCuNm0Rj{Q6aWAK2mt$eR#S*`+2Ohn0056Y001HY0000000000005+cOGpy{aA|Na -Uv_0~WN&gWXmo9CHEd~OFLPybX<=+>dS!AhaCuNm0Rj{Q6aWAK2mt$eR#VdDT~t~C003bYEXCaCuNm0Rj{Q6aWAK2mt -$eR#O=o=YggH008v^001KZ0000000000005+c<5?2`aA|NaUv_0~WN&gWXmo9CHEd~OFJE+WX=N{8Vq -tS-E^v8JO928D0~7!N00;p4c~( -UK0RtX>c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bUtei%X>?y-E^v8JO928D0~7!N00;p4c~(<^umlj -o0RRA(0{{Rv00000000000001_fo)zB0B~t=FJE?LZe(wAFKBdaY&C3YVlQTCY;c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bX>Mv|V{~6_WprU*V`yP= -b7gccaCuNm0Rj{Q6aWAK2mt$eR#TGEWT0#V0027<001Na0000000000005+c!DJHvaA|NaUv_0~WN&g -WXmo9CHEd~OFJ@_MbY*gLFKlUUbS`jtP)h*<6ay3h000O8`*~JV7j>E{4?5a&s?laCB*JZeeV6VP|tLaCuNm0Rj{Q6aWAK2m -t$eR#Q>FVND4b000qb001cf0000000000005+co^KNXaA|NaUv_0~WN&gWXmo9CHEd~OFJ@_MbY*gLF -LPmTX>@6NWpXZXc~DCM0u%!j000080Q-4XQ($uc5A^{60KNnO04e|g00000000000HlHLhZ6vBX>c!J -c4cm4Z*nhabZu-kY-wUIW@&76WpZ;bcW7yJWpi+0V`VOIc~DCM0u%!j000080Q-4XQ=U0-T4wc!Jc4cm4Z*nhabZu-kY-wUIbaG{7VPs)&bY*gLFJE72ZfSI1UoL -QYP)h*<6ay3h000O8`*~JVQj8HPR0041vjzYFD*ylh0000000000q=DUw698~&a4%nWWo~3|axZ9fZE -Q7cX<{#Qa%E*p0PqF?04M+e000000 -00000HlF>juQZIX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7VPs)&bY*gLFLPmdE^v8JO928D0~7!N00;p4 -c~(4R -=a&s?VUukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#QGcX#DaH008AU001cf0000000000005+cqLvc?aA| -NaUv_0~WN&gWXmo9CHEd~OFLZKcWny({Y-D9}b1!0Hb7d}Yc~DCM0u%!j000080Q-4XQ&0q_vHu4E0N -o-004M+e00000000000HlH2r4s;fX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7Vs&Y3WMy)5FJy0RE^v8JO -928D0~7!N00;p4c~(4R=a&s?bbaG{7E^v8JO928D0~7!N00;p4c~(=g8MlBL4gdhIIRF4J00000000000001 -_fx5U80B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZWo2S@X>4R=a&s?bbaG{7Uu<}7Y%XwlP)h*<6ay3h000 -O8`*~JV+jeS&o(2E_R~7&OEC2ui0000000000q=6vE698~&a4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQ -gzbYEXCaCuNm0Rj{Q6aWAK2mt$eR#PD@+Cmox001-{001Ze0000000000005+c2+k7#a -A|NaUv_0~WN&gWXmo9CHEd~OFLZKcWp`n0Yh`kCFJfVHWiD`eP)h*<6ay3h000O8`*~JVWr}a9_W=L^ -g#`crCjbBd0000000000q=9AC698~&a4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQgzc!Jc4cm4Z*nhabZu-kY-w -UIbaG{7cVTR6WpZ;bWpr|7WiD`eP)h*<6ay3h000O8`*~JVFatz%c?JLg)ffN(E&u=k0000000000q= -D1i698~&a4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQgzbYEXCaCuNm0Rj{Q6aWAK2mt$eR#RmaV{t13004ar000>P0000000000005 -+c{^t__aA|NaUv_0~WN&gWX=H9;FJo_HWn(UIc~DCM0u%!j000080Q-4XQ!S&#-OB&~0B8XK02%-Q00 -000000000HlFn>k|NQX>c!Jc4cm4Z*nhbWNu+EV{dJ6VRSBVc~DCM0u%!j000080Q-4XQ&gZqI-Lsu0 -2(p?02lxO00000000000HlFq>=OWRX>c!Jc4cm4Z*nhbWNu+EV{dY0E^v8JO928D0~7!N00;p4c~(<) -7#cEfBLDzyr2qgN00000000000001_fj0OP0B~t=FJE?LZe(wAFKJ|MVJ~T9Zee6$bYU)Vc~DCM0u%! -j000080Q-4XQ!L0`TPFhm0F4I#0384T00000000000HlH68x#O=X>c!Jc4cm4Z*nhbWNu+EX>N3KVQy -z-b1rasP)h*<6ay3h000O8`*~JV>7zsD7XSbN6#xJLAOHXW0000000000q=7*n6aa8(a4%nWWo~3|ax -ZCQZecHQVPk7yXJubxVRT_GaCuNm0Rj{Q6aWAK2mt$eR#Uvk5`-O?004Ou0{|TW0000000000005+cm -LC)VaA|NaUv_0~WN&gWX=H9;FLiWtG&W>mbYU)Vc~DCM0u%!j000080Q-4XQ;jEASl|Hw0A2(D03QGV -00000000000HlHLw-f+yX>c!Jc4cm4Z*nhfb7yd2V{0#8UukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#P$ -fug4Yu000yK0018V0000000000005+c3%V2laA|NaUv_0~WN&gWZF6UEVPk7AUv_13b7^mGE^v8JO92 -8D0~7!N00;p4c~(<9JDt<50RR9w1ONab00000000000001_fnK^40B~t=FJE?LZe(wAFKu&YaA9L>FJ -*XRWpH$9Z*FrgaCuNm0Rj{Q6aWAK2mt$eR#S=pt2_V)0077r000^Q0000000000005+cO1u;RaA|NaU -v_0~WN&gWZF6UEVPk7AWq5QhaCuNm0Rj{Q6aWAK2mt$eR#U62=1n9W004@V0018V0000000000005+c -g2NO5aA|NaUv_0~WN&gWZF6UEVPk7AW?^h>Vqs%zE^v8JO928D0~7!N00;p4c~(;<3Bjgi0RRA%0ssI -a00000000000001_f#cv50B~t=FJE?LZe(wAFK}UFYhh<;Zf7rFUtwZzb#z}}E^v8JO928D0~7!N00; -p4c~(;ut&*Vh0002-0RR9Y00000000000001_fr#Q10B~t=FJE?LZe(wAFK}UFYhh<;Zf7rFUukY>bY -EXCaCuNm0Rj{Q6aWAK2mt$eR#O`)I%9|q007`D001KZ0000000000005+cyWVP|P>XD?rEVQzVBX>N6RE^v8JO928D0~7!N00;p4c~(=Z>u=dG2LJ#X5dZ)q00000000000001_ -frRoD0B~t=FJE?LZe(wAFK}UFYhh<;Zf7rFZFO^OY-w(FcrI{xP)h*<6ay3h000O8`*~JVaRPJMmPUvqSFbz^jOa%FQaaCuNm0Rj -{Q6aWAK2mt$eR#R>K>OXr5001W;001BW0000000000005+cp!*a6aA|NaUv_0~WN&gWaA9L>VP|P>XD -@AGa%*LBb1rasP)h*<6ay3h000O8`*~JVZj(V;@&*6^L=pf1B>(^b0000000000q=8um6##H)a4%nWW -o~3|axZXUV{2h&X>MmPa%FLKX>w(4Wo~qHE^v8JO928D0~7!N00;p4c~(<2WSxN$8vp?GcmMz+00000 -000000001_fsPFo0B~t=FJE?LZe(wAFK}UFYhh<;Zf7rZaAjj@W@%+|b1rasP)h*<6ay3h000O8`*~J -V5aMk*l@R~{Vm$x=9RL6T0000000000q=Dfm6##H)a4%nWWo~3|axZXUV{2h&X>MmPbYW+6E^v8JO92 -8D0~7!N00;p4c~(URJD0D=Gj03HAU0000000000 -0HlHFP!#}hX>c!Jc4cm4Z*nhiWpFhyH!ojbX>MtBUtcb8c~DCM0u%!j000080Q-4XQ~N##QYZxg0D%n -v02=@R00000000000HlGNQ567iX>c!Jc4cm4Z*nhiWpFhyH!os!X>4RJaCuNm0Rj{Q6aWAK2mt$eR#S -7On+w7P006`n000{R0000000000005+c{8kkJaA|NaUv_0~WN&gWaAj~cF*h$`Xk}w-E^v8JO928D0~ -7!N00;p4c~(;mu~yKE1^@s85C8xk00000000000001_f%jY$0B~t=FJE?LZe(wAFK}gWH8D3YV{dG4a -%^vBE^v8JO928D0~7!N00;p4c~(=83#T>70RRBy1ONaW00000000000001_fxTlD0B~t=FJE?LZe(wA -FK}gWH8D3YV{dJ6VRSBVc~DCM0u%!j000080Q-4XQ#;SNM$Z8N0BHmO03HAU00000000000HlGyWfcH -$X>c!Jc4cm4Z*nhiWpFhyH!oyqa&&KRY;!Jfc~DCM0u%!j000080Q-4XQxxc!Jc4cm4Z*nhiWpFhyH!o#wc4BpDY-BEQc~DCM0u%!j000080Q-4XQX>c!Jc4cm4Z*nhiWpFhyH!p2vbYU)Vc~DCM0u%!j000080 -Q-4XQx;WthTRMR0Ch9~03HAU00000000000HlE^bQJ(_X>c!Jc4cm4Z*nhiWpFhyH!pW`VQ_F|a&sc!Jc4cm4Z*nhiWpFh -yH!o>!UvP47V`X!5FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVv=buUNDBY}!7Bg&EC2ui00000000 -00q=Br06##H)a4%nWWo~3|axZXYa5XVEFKKRHaB^>BWpi^cUukY%aB^>BWpi^baCuNm0Rj{Q6aWAK2m -t$eR#U)|DV@Y~0094{0RSZc0000000000005+cK8_UtaA|NaUv_0~WN&gWaBF8@a%FRGb#h~6b1z?CX ->MtBUtcb8c~DCM0u%!j000080Q-4XQ(+_n)=L2Z05Spq04D$d00000000000HlFM0u}&pX>c!Jc4cm4 -Z*nhiYiD0_Wpi(Ja${w4FK~G?F=KCSaA9;VaCuNm0Rj{Q6aWAK2mt$eR#WeURQTcq0028O001Na0000 -000000005+c)dLm)aA|NaUv_0~WN&gWaBN|8W^ZzBWNC79FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~ -JVR)sLnTmb+8bOZnZBme*a0000000000q=ESe765Q*a4%nWWo~3|axZXfVRUA1a&2U3a&s?VUu|J&Ze -L$6aCuNm0Rj{Q6aWAK2mt$eR#WZ?Ez&*&005c~001KZ0000000000005+cmkJgDaA|NaUv_0~WN&gWa -BN|8W^ZzBWNC79FJW$Ea&Kv5E^v8JO928D0~7!N00;p4c~(4IUfGI1^@v08UO$w00000000000001_f%p~{0B~t=FJE?LZe(wAFK}#ObY^dIZDeV3b1!vnX? -QMhc~DCM0u%!j000080Q-4XQ-k>iYCQk|08jt`03!eZ00000000000HlHO9Tos^X>c!Jc4cm4Z*nhiY -+-a}Z*py9X>xNfc4cyNX>V>WaCuNm0Rj{Q6aWAK2mt$eR#S!>I518J000mf001KZ0000000000005+c -Zypu^aA|NaUv_0~WN&gWaBN|8W^ZzBWNC79FL!BfWN&wKE^v8JO928D0~7!N00;p4c~(;r+t4op2LJ% -B6aWAq00000000000001_f&L{H0B~t=FJE?LZe(wAFK}{iXL4n8b1z?CX>MtBUtcb8c~DCM0u%!j000 -080Q-4XQ|?3ZU&aIg0DcPq02=@R00000000000HlFFEfxT9X>c!Jc4cm4Z*nhia&KpHWpi^cVqtPFaC -uNm0Rj{Q6aWAK2mt$eR#TCk?2lvw003VK0015U0000000000005+cJu(&maA|NaUv_0~WN&gWaB^>Fa -%FRKFJo_PZ*p@kaCuNm0Rj{Q6aWAK2mt$eR#Q%e)_ffU002z}0018V0000000000005+c-8L2gaA|Na -Uv_0~WN&gWaB^>Fa%FRKFJo_YZggdGE^v8JO928D0~7!N00;p4c~(>8*Y@fz0{{TE1poja000000000 -00001_fj2r90B~t=FJE?LZe(wAFK}{iXL4n8b1!pnX>M+1axQRrP)h*<6ay3h000O8`*~JVxf(FQYzF -`U`4a#DAOHXW0000000000q=BM6765Q*a4%nWWo~3|axZdaadl;LbaO9XUukY>bYEXCaCuNm0Rj{Q6a -WAK2mt$eR#VF+@nr@9006lG001KZ0000000000005+cOhpy|aA|NaUv_0~WN&gWa%FLKWpi|MFJE7FW -pZEK~phAOHX -W0000000000q=D>6765Q*a4%nWWo~3|axZdaadl;LbaO9ZWMOc0WpZ;aaCuNm0Rj{Q6aWAK2mt$eR#O -71vL9&%0006R000{R0000000000005+cwp|tgaA|NaUv_0~WN&gWa%FLKWpi|MFJW+LE^v8JO928D0~ -7!N00;p4c~(=qw23Qo3jhG$CjbB(00000000000001_fmmb~0B~t=FJE?LZe(wAFLGsZb!BsOb1z|ab -Z9Pcc~DCM0u%!j000080Q-4XQ-ZRh&n^J~0MP*e0384T00000000000HlEha25b?X>c!Jc4cm4Z*nhk -WpQ<7b98erV`Xx5b1rasP)h*<6ay3h000O8`*~JVuV#XDOV000 -0000000q=Alf765Q*a4%nWWo~3|axZdaadl;LbaO9bZ*Oa9WpgfYc~DCM0u%!j000080Q-4XQ)i?&gh -vDb0J01K03rYY00000000000HlG7h!y~FX>c!Jc4cm4Z*nhkWpQ<7b98erWq4y{aCB*JZgVbhc~DCM0 -u%!j000080Q-4XQ(+b-xsC(?0E7c!Jc4cm4Z*nhkWpQ<7b98er -Xk~10E^v8JO928D0~7!N00;p4c~(;!N^{|P0RRB?0ssIV00000000000001_f!dK40B~t=FJE?LZe(w -AFLGsZb!BsOb1!IbZ)?y-E^v8J -O928D0~7!N00;p4c~(;&WxEeF2LJ%@761Sv00000000000001_fx8hG0B~t=FJE?LZe(wAFLGsbZ)|p -DY-wUIaB^>UX=G(`b1rasP)h*<6ay3h000O8`*~JV%K=~A>Hz=%R0RM4BLDyZ0000000000q=7IQ7XW -Z+a4%nWWo~3|axZdab8l>RWo&6;FLGsYZ*p{Ha&ss60E9#U03 -!eZ00000000000HlFi8y5g@X>c!Jc4cm4Z*nhkWpi(Ac4cg7VlQ%Kb8l>RWpZ;aaCuNm0Rj{Q6aWAK2 -mt$eR#Q~m&~Q@)006oY001EX0000000000005+c&Mg-JaA|NaUv_0~WN&gWa%FRGY<6XAX<{#PbaHiL -baO6nc~DCM0u%!j000080Q-4XQvd(}00IC20000004V?f00000000000HlFnGZz4GX>c!Jc4cm4Z*nh -kWpi(Ac4cg7VlQKFZE#_9FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV8`mypV*mgEoB#j-FaQ7m000 -0000000q=Bh37XWZ+a4%nWWo~3|axZdab8l>RWo&6;FJo_QaA9;WV{dG1Wn*+{Z*Fs6VPa!0aCuNm0R -j{Q6aWAK2mt$eR#WC47Qf{a002=(001BW0000000000005+cS~M2`aA|NaUv_0~WN&gWbY*T~V`+4GF -JE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV8Qg7btpor7@(cg~AOHXW0000000000q=9`%7XWZ+a4%nW -Wo~3|axZjcZee3-ba^jdVRLzIV`*4;YaCuNm0Rj{Q6aWAK2mt$eR#T|q81C`{007 -tp0012T0000000000005+cAyF3qaA|NaUv_0~WN&gWbY*T~V`+4GFJWeMWpXZXc~DCM0u%!j000080Q --4XQ_y|osl5UK0AK|G03HAU00000000000HlFVR2KknX>c!Jc4cm4Z*nhmWo}_(X>@rnVr6D;a%C=Xc -~DCM0u%!j000080Q-4XQv?g`YhnWc0CWcc03-ka00000000000HlFOR~Gc!Jc4cm4Z*nhmWo}_( -X>@rnVr6D;a%Eq0Y-MF|E^v8JO928D0~7!N00;p4c~(=VY_ikb0ssJK1pojW00000000000001_f$Lf -q0B~t=FJE?LZe(wAFLY&YVPk1@c`t5Za4v9pP)h*<6ay3h000O8`*~JVm{Kjfv;_bF^%(#F9RL6T000 -0000000q=5il7XWZ+a4%nWWo~3|axZjcZee3-ba^jwWpr|RE^v8JO928D0~7!N00;p4c~(>PR?T&(0{ -{T#3IG5c00000000000001_f$w7%0B~t=FJE?LZe(wAFLY&YVPk1@c`tKxZ*VSfc~DCM0u%!j000080 -Q-4XQytzSIjjQ!0AUCK03rYY00000000000HlG^XBPl)X>c!Jc4cm4Z*nhmWo}_(X>@rnbZ>HQVPtQ2 -WnwOHc~DCM0u%!j000080Q-4XQ(9$!S(69=03#Xz02}}S00000000000HlGwYZm};X>c!Jc4cm4Z*nh -mWo}_(X>@rncVTICE^v8JO928D0~7!N00;p4c~(=s(5w5a0002y0000T00000000000001_fs1q(0B~ -t=FJE?LZe(wAFLZBhY-ulFUukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#V0R{PsW<0056y000~S0000000 -000005+cadj5}aA|NaUv_0~WN&gWbZ>2JX)j-JVRCb2axQRrP)h*<6ay3h000O8`*~JVg4{8H&I14dc -?tjk7ytkO0000000000q=D;-7XWZ+a4%nWWo~3|axZjmZER^TUvgzGaCuNm0Rj{Q6aWAK2mt$eR#PEK -Z5&Yq007Gh0018V0000000000005+c?~WG$aA|NaUv_0~WN&gWb#iQMX<{=kUtei%X>?y-E^v8JO928 -D0~7!N00;p4c~(;$KNE5S4FCW;DgXc@00000000000001_fqjz~0B~t=FJE?LZe(wAFLiQkY-wUMFJE -JCY;0v?bZKvHb1rasP)h*<6ay3h000O8`*~JVTd^H-KL7v#KL7v#9{>OV0000000000q=CSo7XWZ+a4 -%nWWo~3|axZmqY;0*_GcR9uWpZc!Jc4cm4Z*nhna%^mAVlyveZ*Fd7V{~b6ZZ2?nP)h*<6ay3h000O8`*~JVo&cQ- -MkoLP(~(^b0000000000q=AOG7XWZ+a4%nWWo~3|axZmqY;0*_GcRLrZf<2`bZKvHaBpvHE^v8 -JO928D0~7!N00;p4c~(<{YAOHX%00000000000001_fe+yq0B~t=FJE?LZe(wAFLiQkY-w -UMFJ*XRWpH$9Z*FrgaCuNm0Rj{Q6aWAK2mt$eR#V3)ODrG?004s_0012T0000000000005+cIqMeyaA -|NaUv_0~WN&gWb#iQMX<{=kW@%+?WOFWXc~DCM0u%!j000080Q-4XQ-`a+qkaPb0Eh_y03QGV000000 -00000HlGG^%nqeX>c!Jc4cm4Z*nhna%^mAVlyvhX>4V1Z*z1maCuNm0Rj{Q6aWAK2mt$eR#WJrk&9>+ -001*h001HY0000000000005+cPx%)BaA|NaUv_0~WN&gWb#iQMX<{=kaBpvHZDDR001j)0018V0000000000005+c+7}oAaA|NaUv_0~WN -&gWb#iQMX<{=ka%FRHZ*FsCE^v8JO928D0~7!N00;p4c~(;Z00002000000000d00000000000001_f -qFF<0B~t=FJE?LZe(wAFLiQkY-wUMFJo_RbaH88FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVDud}2 -wE+MCy#oLMF#rGn0000000000q=CUT7yxi-a4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ7|aByXAXK8L -_UuAA~X>xCFE^v8JO928D0~7!N00;p4c~(<6WeLe=3;+NcD*yl}00000000000001_fyFl%0B~t=FJE -?LZe(wAFLiQkY-wUMFJo_RbaH88FJW+SWo~C_Ze=cTc~DCM0u%!j000080Q-4XQ@F=N{!|740J;$X04 -D$d00000000000HlF(L>K^YX>c!Jc4cm4Z*nhna%^mAVlyveZ*FvQX<{#KbZl*KZ*OcaaCuNm0Rj{Q6 -aWAK2mt$eR#RZPnqGkv000C+001Ze0000000000005+c3riRPaA|NaUv_0~WN&gWb#iQMX<{=kV{dMB -a%o~OaCvWVWo~nGY%XwlP)h*<6ay3h000O8`*~JV!0)=_!6g6yk%j;OE&u=k0000000000q=C|37yxi --a4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ)LV|8+6baG*Cb8v5RbS`jtP)h*<6ay3h000O8`*~JV%Tm -ZcSqK0Cxf=igBme*a0000000000q=Das7yxi-a4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ)VV{3CRaC -uNm0Rj{Q6aWAK2mt$eR#N}~0006200000001}u0000000000005+cdX5+XaA|NaUv_0~WN&gWb#iQMX -<{=kV{dMBa%o~OUvp(+b#i5Na$#Md`ZfA2YaCuNm0Rj{Q6aWAK2mt$eR#UDjr@UDa003e(0021v0000000000005+cAfOlkaA| -NaUv_0~WN&gWb#iQMX<{=kV{dMBa%o~OUvp(+b#i5Na$#;0RtWW>|0BisN04M+e00000000000HlG?u^0ewX>c!Jc4cm4Z*nhna%^mAVlyvrVPk7yX -JvCQUtei%X>?y-E^v8JO928D0~7!N00;p4c~(>3X^-9}ApihshX4R000000000000001_fo8H80B~t= -FJE?LZe(wAFLiQkY-wUMFK}UFYhh<)b1!pgcrI{xP)h*<6ay3h000O8`*~JV000000ssI200000G5`P -o0000000000q=C)T7yxi-a4%nWWo~3|axZmqY;0*_GcRyqV{2h&WpgiLVPk7>Z*p{VFJE72ZfSI1UoL -QYP)h*<6ay3h000O8`*~JV-ne74NCE%=i3I=vG5`Po0000000000q=6sQ7yxi-a4%nWWo~3|axZmqY; -0*_GcRyqV{2h&WpgiLVPk7>Z*p{VFKuCKWoBt?WiD`eP)h*<6ay3h000O8`*~JV1rwupqyYc`p925@I -{*Lx0000000000q=C2A7yxi-a4%nWWo~3|axZmqY;0*_GcRyqV{2h&Wpgicb8KI2VRU0?UubW0bZ%j7 -WiMY}X>MtBUtcb8c~DCM0u%!j000080Q-4XQ$B=C*0Bfx0528*073u&00000000000HlGm*cbqCX>c! -Jc4cm4Z*nhna%^mAVlyvrVPk7yXJvCQb8~E8ZDDj{XkTb=b98QDZDlWCX>D+9Wo>0{bYXO9Z*DGdc~D -CM0u%!j000080Q-4XQ|5}9n%DsV0D}Yo03-ka00000000000HlG%;TQmLX>c!Jc4cm4Z*nhna%^mAVl -yvwbZKlaUtei%X>?y-E^v8JO928D0~7!N00;p4c~(<%5t5pw2LJ##6951v00000000000001_f#2g80 -B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%gPPZf<2`bZKvHE^v8JO928D0~7!N00;p4c~(;j&s+yy0ssI- -1^@sd00000000000001_fywI_0B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%g$fZ+LkwaCuNm0Rj{Q6aWA -K2mt$eR#Q#`_MiL!008m<001EX0000000000005+cX6_gOaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFL8 -Bcb!9Gac~DCM0u%!j000080Q-4XQ<7w_RO$r)02>eh03!eZ00000000000HlGT?-&4ZX>c!Jc4cm4Z* -nhna%^mAVlyvwbZKlaa%FLKWpi{caCuNm0Rj{Q6aWAK2mt$eR#P@8)wEy*006cP001Na00000000000 -05+c%=H)maA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLGsbaBpsNWiD`eP)h*<6ay3h000O8`*~JVmFNkb -&=vpyk5d2uApigX0000000000q=9bx7yxi-a4%nWWo~3|axZmqY;0*_GcR>?X>2cYWpr|RE^v8JO928 -D0~7!N00;p4c~(=TlKaP}761SlLjV9E00000000000001_fqfDg0B~t=FJE?LZe(wAFLiQkY-wUMFLi -WjY%gc!Jc4 -cm4Z*nhna%^mAVlyvwbZKlab8~ETa$#<2c3jhEUCjbB=0 -0000000000001_fweIi0B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%g?aZDntDbS`jtP)h*<6ay3h000O8 -`*~JV=c|?Zr4j%D-!=dM9{>OV0000000000q=B|Q831r;a4%nWWo~3|axZmqY;0*_GcR>?X>2cba%?V -ec~DCM0u%!j000080Q-4XQ=&${ali)v02~zn03ZMW00000000000HlGKP#FMlX>c!Jc4cm4Z*nhna%^ -mAVlyvwbZKlacVTICE^v8JO928D0~7!N00;p4c~(<+&>wL!3jhF9DF6T@00000000000001_ftFYq0B -~t=FJE?LZe(wAFLz~PWo~0{WNB_^b1z?CX>MtBUtcb8c~DCM0u%!j000080Q-4XQ^&0civc!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZY++($Y;!Jfc~DCM0u%!j00008 -0Q-4XQ&4{~A4CEG02u`U03-ka00000000000HlFWY8e1c!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZZEI{ -{Vr6V|E^v8JO928D0~7!N00;p4c~(Mn8FL+;db7gX0WMyV)Ze?UHaCuNm1qJ{B005Z*nE_CM0 -06po82|tP -""" - - -if __name__ == "__main__": - main() diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts index f719844ef80b..2644fd0d00ff 100644 --- a/src/client/common/process/internal/scripts/index.ts +++ b/src/client/common/process/internal/scripts/index.ts @@ -25,9 +25,6 @@ const SCRIPTS_DIR = _SCRIPTS_DIR; // In some cases one or more types related to a script are exported // from the same module in which the script's function is located. // These types typically relate to the return type of "parse()". -// -// ignored scripts: -// * install_debugpy.py (used only for extension development) export * as testingTools from './testing_tools'; // interpreterInfo.py From d63fb1825fa03297581fffd06829dd513ee42651 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:42:40 -0700 Subject: [PATCH 162/336] Bump brettcannon/check-for-changed-files from 1.1.0 to 1.1.1 (#21607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [brettcannon/check-for-changed-files](https://github.com/brettcannon/check-for-changed-files) from 1.1.0 to 1.1.1.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=brettcannon/check-for-changed-files&package-manager=github_actions&previous-version=1.1.0&new-version=1.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-file-check.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index c0bf09f2cd24..2d227ea00399 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'package-lock.json matches package.json' - uses: brettcannon/check-for-changed-files@v1.1.0 + uses: brettcannon/check-for-changed-files@v1.1.1 with: prereq-pattern: 'package.json' file-pattern: 'package-lock.json' @@ -25,7 +25,7 @@ jobs: failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - name: 'package.json matches package-lock.json' - uses: brettcannon/check-for-changed-files@v1.1.0 + uses: brettcannon/check-for-changed-files@v1.1.1 with: prereq-pattern: 'package-lock.json' file-pattern: 'package.json' @@ -33,7 +33,7 @@ jobs: failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - name: 'Tests' - uses: brettcannon/check-for-changed-files@v1.1.0 + uses: brettcannon/check-for-changed-files@v1.1.1 with: prereq-pattern: src/**/*.ts file-pattern: | From 0c4a156c6f88710a35142d3fe0463ab84ec0463a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:43:06 -0700 Subject: [PATCH 163/336] Bump mheap/github-action-required-labels from 4 to 5 (#21606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mheap/github-action-required-labels](https://github.com/mheap/github-action-required-labels) from 4 to 5.
Release notes

Sourced from mheap/github-action-required-labels's releases.

v5

Tag that always points to the latest commit in the v5.x.x series of releases

v4.1.1

  • Fix build step by switching to ubuntu-latest

Full Changelog: https://github.com/mheap/github-action-required-labels/compare/v4.1.0...v4.1.1

v4.1.0

What's Changed

New Contributors

Full Changelog: https://github.com/mheap/github-action-required-labels/compare/v4.0.0...v4.1.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mheap/github-action-required-labels&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index e953f62d2011..730b8e5c5832 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'PR impact specified' - uses: mheap/github-action-required-labels@v4 + uses: mheap/github-action-required-labels@v5 with: mode: exactly count: 1 From 8099383e6c7905bc6a4ee61e26061a62283a1844 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:43:33 -0700 Subject: [PATCH 164/336] Bump actions/checkout from 2 to 3 (#21577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
Release notes

Sourced from actions/checkout's releases.

v3.0.0

  • Updated to the node16 runtime by default
    • This requires a minimum Actions Runner version of v2.285.0 to run, which is by default available in GHES 3.4 or later.

v2.7.0

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2.6.0...v2.7.0

v2.6.0

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2.5.0...v2.6.0

v2.5.0

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2...v2.5.0

v2.4.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2...v2.4.2

v2.4.1

  • Fixed an issue where checkout failed to run in container jobs due to the new git setting safe.directory

v2.4.0

  • Convert SSH URLs like org-<ORG_ID>@github.com: to https://github.com/ - pr

v2.3.5

Update dependencies

v2.3.4

v2.3.3

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

v3.5.3

v3.5.2

v3.5.1

v3.5.0

v3.4.0

v3.3.0

v3.2.0

v3.1.0

v3.0.2

v3.0.1

v3.0.0

v2.3.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/info-needed-closer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml index 07104d6bc4be..c0b130be803b 100644 --- a/.github/workflows/info-needed-closer.yml +++ b/.github/workflows/info-needed-closer.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions From 0cc5b18b7fba25302bcea9ecc68ec9d70cdc792c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 12 Jul 2023 09:01:22 -0700 Subject: [PATCH 165/336] handle skipped marker on class level (#21612) fixes https://github.com/microsoft/vscode-python/issues/21579 --- .../tests/pytestadapter/.data/skip_tests.py | 10 +++ .../expected_execution_test_output.py | 17 ++++ .../tests/pytestadapter/test_execution.py | 2 + pythonFiles/vscode_pytest/__init__.py | 82 +++++++++++++------ 4 files changed, 86 insertions(+), 25 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/.data/skip_tests.py b/pythonFiles/tests/pytestadapter/.data/skip_tests.py index 113e3506932a..871b0e7bf5c3 100644 --- a/pythonFiles/tests/pytestadapter/.data/skip_tests.py +++ b/pythonFiles/tests/pytestadapter/.data/skip_tests.py @@ -28,3 +28,13 @@ def test_decorator_thing(): def test_decorator_thing_2(): # Skip this test as well, with a reason. This one uses a decorator with a condition. assert True + + +# With this test, the entire class is skipped. +@pytest.mark.skip(reason="Skip TestClass") +class TestClass: + def test_class_function_a(self): # test_marker--test_class_function_a + assert True + + def test_class_function_b(self): # test_marker--test_class_function_b + assert False diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index 3d24f036fe2a..92b881fdc8b8 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -169,6 +169,9 @@ # └── test_another_thing: skipped # └── test_decorator_thing: skipped # └── test_decorator_thing_2: skipped +# ├── TestClass +# │ └── test_class_function_a: skipped +# │ └── test_class_function_b: skipped skip_tests_execution_expected_output = { "skip_tests.py::test_something": { "test": "skip_tests.py::test_something", @@ -198,6 +201,20 @@ "traceback": None, "subtest": None, }, + "skip_tests.py::TestClass::test_class_function_a": { + "test": "skip_tests.py::TestClass::test_class_function_a", + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + "skip_tests.py::TestClass::test_class_function_b": { + "test": "skip_tests.py::TestClass::test_class_function_b", + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, } diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index f147a0462f38..8f5fa191e38c 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -62,6 +62,8 @@ def test_bad_id_error_execution(): "skip_tests.py::test_another_thing", "skip_tests.py::test_decorator_thing", "skip_tests.py::test_decorator_thing_2", + "skip_tests.py::TestClass::test_class_function_a", + "skip_tests.py::TestClass::test_class_function_b", ], expected_execution_test_output.skip_tests_execution_expected_output, ), diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index b14a79aef7fd..20b3dba325ef 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -207,32 +207,64 @@ def pytest_report_teststatus(report, config): def pytest_runtest_protocol(item, nextitem): + skipped = check_skipped_wrapper(item) + if skipped: + node_id = str(item.nodeid) + report_value = "skipped" + cwd = pathlib.Path.cwd() + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) + item_result = create_test_outcome( + node_id, + report_value, + None, + None, + ) + collected_test = testRunResultDict() + collected_test[node_id] = item_result + execution_post( + os.fsdecode(cwd), + "success", + collected_test if collected_test else None, + ) + + +def check_skipped_wrapper(item): + """A function that checks if a test is skipped or not by check its markers and its parent markers. + + Returns True if the test is marked as skipped at any level, False otherwise. + + Keyword arguments: + item -- the pytest item object. + """ if item.own_markers: - for marker in item.own_markers: - # If the test is marked with skip then it will not hit the pytest_report_teststatus hook, - # therefore we need to handle it as skipped here. - skip_condition = False - if marker.name == "skipif": - skip_condition = any(marker.args) - if marker.name == "skip" or skip_condition: - node_id = str(item.nodeid) - report_value = "skipped" - cwd = pathlib.Path.cwd() - if node_id not in collected_tests_so_far: - collected_tests_so_far.append(node_id) - item_result = create_test_outcome( - node_id, - report_value, - None, - None, - ) - collected_test = testRunResultDict() - collected_test[node_id] = item_result - execution_post( - os.fsdecode(cwd), - "success", - collected_test if collected_test else None, - ) + if check_skipped_condition(item): + return True + parent = item.parent + while isinstance(parent, pytest.Class): + if parent.own_markers: + if check_skipped_condition(parent): + return True + parent = parent.parent + return False + + +def check_skipped_condition(item): + """A helper function that checks if a item has a skip or a true skip condition. + + Keyword arguments: + item -- the pytest item object. + """ + + for marker in item.own_markers: + # If the test is marked with skip then it will not hit the pytest_report_teststatus hook, + # therefore we need to handle it as skipped here. + skip_condition = False + if marker.name == "skipif": + skip_condition = any(marker.args) + if marker.name == "skip" or skip_condition: + return True + return False def pytest_sessionfinish(session, exitstatus): From 0beefa093c49e7321ac1efad1d39746d49e3b47a Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 12 Jul 2023 12:34:12 -0700 Subject: [PATCH 166/336] Bump semver (#21618) --- package-lock.json | 144 +++++++++++++++++++++++----------------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index e6042d7638d8..3678852d1f65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1851,9 +1851,9 @@ } }, "node_modules/@vscode/vsce/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -2694,9 +2694,9 @@ } }, "node_modules/async-listener/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -3953,9 +3953,9 @@ } }, "node_modules/cls-hooked/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -4275,9 +4275,9 @@ } }, "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -4787,9 +4787,9 @@ } }, "node_modules/diagnostic-channel/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -4911,9 +4911,9 @@ } }, "node_modules/download/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -5600,9 +5600,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -8526,9 +8526,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -9259,9 +9259,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -10120,9 +10120,9 @@ } }, "node_modules/nock/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -10436,9 +10436,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -11145,9 +11145,9 @@ } }, "node_modules/parse-semver/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -16751,9 +16751,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "tmp": { @@ -17421,9 +17421,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" } } }, @@ -18405,9 +18405,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" } } }, @@ -18685,9 +18685,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "which": { @@ -19094,9 +19094,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" } } }, @@ -19201,9 +19201,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -19935,9 +19935,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -21994,9 +21994,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -22584,9 +22584,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -23259,9 +23259,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -23518,9 +23518,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -24062,9 +24062,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } From 5ccb73187ad24f2c29a55f20033c9c813c1ed606 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 12 Jul 2023 15:12:41 -0700 Subject: [PATCH 167/336] delete run messages (#21620) remove some printing during test run. --- src/client/testing/testController/common/resultResolver.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 8e08ffbeedf5..d9b0a0bde015 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -139,7 +139,6 @@ export class PythonResultResolver implements ITestResultResolver { if (indiItem.id === grabVSid) { if (indiItem.uri && indiItem.range) { runInstance.passed(grabTestItem); - runInstance.appendOutput('Passed here'); } } }); @@ -152,7 +151,6 @@ export class PythonResultResolver implements ITestResultResolver { if (indiItem.id === grabVSid) { if (indiItem.uri && indiItem.range) { runInstance.skipped(grabTestItem); - runInstance.appendOutput('Skipped here'); } } }); From 0b30aaa2e44478bb69ed5c4c2a7d857106417c35 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 13 Jul 2023 12:56:54 -0700 Subject: [PATCH 168/336] display errors and tests on discovery (#21629) fixes https://github.com/microsoft/vscode-python/issues/13301 --- .../testController/common/resultResolver.ts | 21 +++---- .../resultResolver.unit.test.ts | 58 ++++++++++++++++++- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index d9b0a0bde015..320c00bf6ad0 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -65,18 +65,19 @@ export class PythonResultResolver implements ITestResultResolver { } errorNode.error = message; } else { - // Remove the error node if necessary, - // then parse and insert test data. + // remove error node only if no errors exist. this.testController.items.delete(`DiscoveryError:${workspacePath}`); + } + if (rawTestData.tests) { + // if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not. + // parse and insert test data. - if (rawTestData.tests) { - // If the test root for this folder exists: Workspace refresh, update its children. - // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. - populateTestTree(this.testController, rawTestData.tests, undefined, this, token); - } else { - // Delete everything from the test controller. - this.testController.items.replace([]); - } + // If the test root for this folder exists: Workspace refresh, update its children. + // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. + populateTestTree(this.testController, rawTestData.tests, undefined, this, token); + } else { + // Delete everything from the test controller. + this.testController.items.replace([]); } sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 0fc7eb3ab353..30b2ccecf513 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -101,8 +101,7 @@ suite('Result Resolver tests', () => { cancelationToken, // token ); }); - // what about if the error node already exists: this.testController.items.get(`DiscoveryError:${workspacePath}`); - test('resolveDiscovery should create error node on error with correct params', async () => { + test('resolveDiscovery should create error node on error with correct params and no root node with tests in payload', async () => { // test specific constants used expected values testProvider = 'pytest'; workspaceUri = Uri.file('/foo/bar'); @@ -136,6 +135,61 @@ suite('Result Resolver tests', () => { // header of createErrorTestItem is (options: ErrorTestItemOptions, testController: TestController, uri: Uri) sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); }); + test('resolveDiscovery should create error and root node when error and tests exist on payload', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // create test result node + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + // stub out return values of functions called in resolveDiscovery + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + tests, + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // builds an error node root + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // builds an error item + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + + // also calls populateTestTree with the discovery test results + sinon.assert.calledWithMatch( + populateTestTreeStub, + testController, // testController + tests, // testTreeData + undefined, // testRoot + resultResolver, // resultResolver + cancelationToken, // token + ); + }); }); suite('Test execution result resolver', () => { let resultResolver: ResultResolver.PythonResultResolver; From b454189cad5d24a8c93a4bc220f1d4c070b0c827 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 13 Jul 2023 15:33:00 -0700 Subject: [PATCH 169/336] Fix for CodeQL errors (#21613) --- src/test/mocks/vsc/extHostedTypes.ts | 2 +- src/test/mocks/vsc/index.ts | 2 +- .../common/environmentIdentifier.unit.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/mocks/vsc/extHostedTypes.ts b/src/test/mocks/vsc/extHostedTypes.ts index 4921b24629d1..f87b50174150 100644 --- a/src/test/mocks/vsc/extHostedTypes.ts +++ b/src/test/mocks/vsc/extHostedTypes.ts @@ -775,7 +775,7 @@ export class SnippetString { this._tabstop = nested._tabstop; defaultValue = nested.value; } else if (typeof defaultValue === 'string') { - defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); + defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); // CodeQL [SM02383] don't escape backslashes here (by design) } this.value += '${'; diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index 092fc67da6c6..7678bef4e53c 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -285,7 +285,7 @@ export class MarkdownString { // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash this.value += (this.supportThemeIcons ? escapeCodicons(value) : value) .replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') - .replace(/\n/, '\n\n'); + .replace(/\n/g, '\n\n'); return this; } diff --git a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts index 6aeb320a0b11..af719c3e40ed 100644 --- a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts @@ -148,7 +148,7 @@ suite('Environment Identifier', () => { test(`Path using forward slashes (${exe})`, async () => { const interpreterPath = path .join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe) - .replace('\\', '/'); + .replace(/\\/g, '/'); const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); }); From 65a761f36990eb2397453e106f6cff2d075dd8e6 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Thu, 13 Jul 2023 16:12:22 -0700 Subject: [PATCH 170/336] Add deprecation warning for linting and formatting settings (#21585) For https://github.com/microsoft/vscode-python/issues/21390 --- package.json | 194 +++++++++++++++++++++++++++++++++++------------ package.nls.json | 96 +++++++++++++++++++++++ 2 files changed, 241 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index dfd6a640b3c4..0ef0ff4d79b6 100644 --- a/package.json +++ b/package.json @@ -588,13 +588,17 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.formatting.autopep8Args.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.autopep8Args.deprecationMessage%" }, "python.formatting.autopep8Path": { "default": "autopep8", "description": "%python.formatting.autopep8Path.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.formatting.autopep8Path.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.autopep8Path.deprecationMessage%" }, "python.formatting.blackArgs": { "default": [], @@ -603,13 +607,17 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.formatting.blackArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.blackArgs.deprecationMessage%" }, "python.formatting.blackPath": { "default": "black", "description": "%python.formatting.blackPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.formatting.blackPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.blackPath.deprecationMessage%" }, "python.formatting.provider": { "default": "autopep8", @@ -621,7 +629,9 @@ "yapf" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.formatting.provider.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.provider.deprecationMessage%" }, "python.formatting.yapfArgs": { "default": [], @@ -630,13 +640,17 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.formatting.yapfArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.yapfArgs.deprecationMessage%" }, "python.formatting.yapfPath": { "default": "yapf", "description": "%python.formatting.yapfPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.formatting.yapfPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.yapfPath.deprecationMessage%" }, "python.globalModuleInstallation": { "default": false, @@ -669,31 +683,41 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.banditArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.banditArgs.deprecationMessage%" }, "python.linting.banditEnabled": { "default": false, "description": "%python.linting.banditEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.banditArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.banditArgs.deprecationMessage%" }, "python.linting.banditPath": { "default": "bandit", "description": "%python.linting.banditPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.banditPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.banditPath.deprecationMessage%" }, "python.linting.cwd": { "default": null, "description": "%python.linting.cwd.description%", "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.cwd.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.cwd.deprecationMessage%" }, "python.linting.enabled": { "default": true, "description": "%python.linting.enabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.enabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.enabled.deprecationMessage%" }, "python.linting.flake8Args": { "default": [], @@ -702,7 +726,9 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.flake8Args.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8Args.deprecationMessage%" }, "python.linting.flake8CategorySeverity.E": { "default": "Error", @@ -714,7 +740,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.E.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8CategorySeverity.E.deprecationMessage%" }, "python.linting.flake8CategorySeverity.F": { "default": "Error", @@ -726,7 +754,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.F.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8CategorySeverity.F.deprecationMessage%" }, "python.interpreter.infoVisibility": { "default": "onPythonRelated", @@ -754,19 +784,25 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.W.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8CategorySeverity.W.deprecationMessage%" }, "python.linting.flake8Enabled": { "default": false, "description": "%python.linting.flake8Enabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.flake8Enabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8Enabled.deprecationMessage%" }, "python.linting.flake8Path": { "default": "flake8", "description": "%python.linting.flake8Path.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.flake8Path.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8Path.deprecationMessage%" }, "python.linting.ignorePatterns": { "default": [ @@ -779,19 +815,25 @@ }, "scope": "resource", "type": "array", - "uniqueItems": true + "uniqueItems": true, + "markdownDeprecationMessage": "%python.linting.ignorePatterns.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.ignorePatterns.deprecationMessage%" }, "python.linting.lintOnSave": { "default": true, "description": "%python.linting.lintOnSave.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.lintOnSave.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.lintOnSave.deprecationMessage%" }, "python.linting.maxNumberOfProblems": { "default": 100, "description": "%python.linting.maxNumberOfProblems.description%", "scope": "resource", - "type": "number" + "type": "number", + "markdownDeprecationMessage": "%python.linting.maxNumberOfProblems.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.maxNumberOfProblems.deprecationMessage%" }, "python.linting.mypyArgs": { "default": [ @@ -805,7 +847,9 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.mypyArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.mypyArgs.deprecationMessage%" }, "python.linting.mypyCategorySeverity.error": { "default": "Error", @@ -817,11 +861,13 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.mypyCategorySeverity.error.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.mypyCategorySeverity.error.deprecationMessage%" }, "python.linting.mypyCategorySeverity.note": { "default": "Information", - "description": "%python.linting.mypyCategorySeverity.note.description%.", + "description": "%python.linting.mypyCategorySeverity.note.description%", "enum": [ "Error", "Hint", @@ -829,19 +875,25 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.mypyCategorySeverity.note.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.mypyCategorySeverity.note.deprecationMessage%" }, "python.linting.mypyEnabled": { "default": false, "description": "%python.linting.mypyEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.mypyEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.mypyEnabled.deprecationMessage%" }, "python.linting.mypyPath": { "default": "mypy", "description": "%python.linting.mypyPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.mypyPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.mypyPath.deprecationMessage%" }, "python.linting.prospectorArgs": { "default": [], @@ -850,19 +902,25 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.prospectorArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.prospectorArgs.deprecationMessage%" }, "python.linting.prospectorEnabled": { "default": false, "description": "%python.linting.prospectorEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.prospectorEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.prospectorEnabled.deprecationMessage%" }, "python.linting.prospectorPath": { "default": "prospector", "description": "%python.linting.prospectorPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.prospectorPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.prospectorPath.deprecationMessage%" }, "python.linting.pycodestyleArgs": { "default": [], @@ -871,7 +929,9 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.pycodestyleArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pycodestyleArgs.deprecationMessage%" }, "python.linting.pycodestyleCategorySeverity.E": { "default": "Error", @@ -883,7 +943,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pycodestyleCategorySeverity.E.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pycodestyleCategorySeverity.E.deprecationMessage%" }, "python.linting.pycodestyleCategorySeverity.W": { "default": "Warning", @@ -895,19 +957,25 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pycodestyleCategorySeverity.W.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pycodestyleCategorySeverity.W.deprecationMessage%" }, "python.linting.pycodestyleEnabled": { "default": false, "description": "%python.linting.pycodestyleEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.pycodestyleEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pycodestyleEnabled.deprecationMessage%" }, "python.linting.pycodestylePath": { "default": "pycodestyle", "description": "%python.linting.pycodestylePath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pycodestylePath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pycodestylePath.deprecationMessage%" }, "python.linting.pydocstyleArgs": { "default": [], @@ -916,19 +984,25 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.pydocstyleArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pydocstyleArgs.deprecationMessage%" }, "python.linting.pydocstyleEnabled": { "default": false, "description": "%python.linting.pydocstyleEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.pydocstyleEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pydocstyleEnabled.deprecationMessage%" }, "python.linting.pydocstylePath": { "default": "pydocstyle", "description": "%python.linting.pydocstylePath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pydocstylePath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pydocstylePath.deprecationMessage%" }, "python.linting.pylamaArgs": { "default": [], @@ -937,19 +1011,25 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.pylamaArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylamaArgs.deprecationMessage%" }, "python.linting.pylamaEnabled": { "default": false, "description": "%python.linting.pylamaEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.pylamaEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylamaEnabled.deprecationMessage%" }, "python.linting.pylamaPath": { "default": "pylama", "description": "%python.linting.pylamaPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylamaPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylamaPath.deprecationMessage%" }, "python.linting.pylintArgs": { "default": [], @@ -958,7 +1038,9 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.pylintArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintArgs.deprecationMessage%" }, "python.linting.pylintCategorySeverity.convention": { "default": "Information", @@ -970,7 +1052,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.convention.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintCategorySeverity.convention.deprecationMessage%" }, "python.linting.pylintCategorySeverity.error": { "default": "Error", @@ -982,7 +1066,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.error.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintCategorySeverity.error.deprecationMessage%" }, "python.linting.pylintCategorySeverity.fatal": { "default": "Error", @@ -994,7 +1080,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.fatal.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintCategorySeverity.fatal.deprecationMessage%" }, "python.linting.pylintCategorySeverity.refactor": { "default": "Hint", @@ -1006,7 +1094,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.refactor.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintCategorySeverity.refactor.deprecationMessage%" }, "python.linting.pylintCategorySeverity.warning": { "default": "Warning", @@ -1018,19 +1108,25 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.warning.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintCategorySeverity.warning.deprecationMessage%" }, "python.linting.pylintEnabled": { "default": false, "description": "%python.linting.pylintEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.pylintEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintEnabled.deprecationMessage%" }, "python.linting.pylintPath": { "default": "pylint", "description": "%python.linting.pylintPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintPath.deprecationMessage%" }, "python.logging.level": { "default": "error", diff --git a/package.nls.json b/package.nls.json index e228e6a19552..84c920b7c22e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -44,12 +44,26 @@ "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.formatting.autopep8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.autopep8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.autopep8Path.description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", + "python.formatting.autopep8Path.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.autopep8Path.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.formatting.blackArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.blackArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.blackPath.description": "Path to Black, you can use a custom version of Black by modifying this setting to include the full path.", + "python.formatting.blackPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.blackPath.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.provider.description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", + "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8) or the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension or the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.formatting.yapfArgs.markdownDeprecationMessage": "Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.yapfArgs.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfPath.description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", + "python.formatting.yapfPath.markdownDeprecationMessage": "Yapf support will soon be deprecated.
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.yapfPath.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServer.description": "Defines type of the language server.", "python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.", @@ -57,50 +71,132 @@ "python.languageServer.pylanceDescription": "Use Pylance as a language server.", "python.languageServer.noneDescription": "Disable language server capabilities.", "python.linting.banditArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.banditArgs.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.banditArgs.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.banditEnabled.description": "Whether to lint Python files using bandit.", + "python.linting.banditEnabled.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.banditEnabled.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.banditPath.description": "Path to bandit, you can use a custom version of bandit by modifying this setting to include the full path.", + "python.linting.banditPath.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.banditPath.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.cwd.description": "Optional working directory for linters.", + "python.linting.cwd.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.cwd.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.enabled.description": "Whether to lint Python files.", + "python.linting.enabled.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.enabled.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8Args.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.flake8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8CategorySeverity.E.description": "Severity of Flake8 message type 'E'.", + "python.linting.flake8CategorySeverity.E.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8CategorySeverity.E.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8CategorySeverity.F.description": "Severity of Flake8 message type 'F'.", + "python.linting.flake8CategorySeverity.F.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8CategorySeverity.F.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8CategorySeverity.W.description": "Severity of Flake8 message type 'W'.", + "python.linting.flake8CategorySeverity.W.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8CategorySeverity.W.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8Enabled.description": "Whether to lint Python files using flake8.", + "python.linting.flake8Enabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8Enabled.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8Path.description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path.", + "python.linting.flake8Path.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8Path.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.ignorePatterns.description": "Patterns used to exclude files or folders from being linted.", + "python.linting.ignorePatterns.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.ignorePatterns.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.interpreter.infoVisibility.description": "Controls when to display information of selected interpreter in the status bar.", "python.interpreter.infoVisibility.never.description": "Never display information.", "python.interpreter.infoVisibility.onPythonRelated.description": "Only display information if Python-related files are opened.", "python.interpreter.infoVisibility.always.description": "Always display information.", "python.linting.lintOnSave.description": "Whether to lint Python files when saved.", + "python.linting.lintOnSave.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.lintOnSave.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.maxNumberOfProblems.description": "Controls the maximum number of problems produced by the server.", + "python.linting.maxNumberOfProblems.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.maxNumberOfProblems.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.mypyArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.mypyArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.mypyArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.mypyCategorySeverity.error.description": "Severity of Mypy message type 'Error'.", + "python.linting.mypyCategorySeverity.error.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.mypyCategorySeverity.error.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.mypyCategorySeverity.note.description": "Severity of Mypy message type 'Note'.", + "python.linting.mypyCategorySeverity.note.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.mypyCategorySeverity.note.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.mypyEnabled.description": "Whether to lint Python files using mypy.", + "python.linting.mypyEnabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.mypyEnabled.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.mypyPath.description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path.", + "python.linting.mypyPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.mypyPath.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.prospectorArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.prospectorArgs.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.prospectorArgs.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.prospectorEnabled.description": "Whether to lint Python files using prospector.", + "python.linting.prospectorEnabled.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.prospectorEnabled.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.prospectorPath.description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path.", + "python.linting.prospectorPath.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.prospectorPath.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pycodestyleArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.pycodestyleArgs.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pycodestyleArgs.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pycodestyleCategorySeverity.E.description": "Severity of pycodestyle message type 'E'.", + "python.linting.pycodestyleCategorySeverity.E.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pycodestyleCategorySeverity.E.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pycodestyleCategorySeverity.W.description": "Severity of pycodestyle message type 'W'.", + "python.linting.pycodestyleCategorySeverity.W.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pycodestyleCategorySeverity.W.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pycodestyleEnabled.description": "Whether to lint Python files using pycodestyle.", + "python.linting.pycodestyleEnabled.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pycodestyleEnabled.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pycodestylePath.description": "Path to pycodestyle, you can use a custom version of pycodestyle by modifying this setting to include the full path.", + "python.linting.pycodestylePath.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pycodestylePath.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pydocstyleArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.pydocstyleArgs.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pydocstyleArgs.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pydocstyleEnabled.description": "Whether to lint Python files using pydocstyle.", + "python.linting.pydocstyleEnabled.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pydocstyleEnabled.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pydocstylePath.description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path.", + "python.linting.pydocstylePath.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pydocstylePath.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylamaArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.pylamaArgs.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylamaArgs.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylamaEnabled.description": "Whether to lint Python files using pylama.", + "python.linting.pylamaEnabled.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylamaEnabled.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylamaPath.description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path.", + "python.linting.pylamaPath.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylamaPath.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.pylintArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintCategorySeverity.convention.description": "Severity of Pylint message type 'Convention/C'.", + "python.linting.pylintCategorySeverity.convention.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintCategorySeverity.convention.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintCategorySeverity.error.description": "Severity of Pylint message type 'Error/E'.", + "python.linting.pylintCategorySeverity.error.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintCategorySeverity.error.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintCategorySeverity.fatal.description": "Severity of Pylint message type 'Error/F'.", + "python.linting.pylintCategorySeverity.fatal.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintCategorySeverity.fatal.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintCategorySeverity.refactor.description": "Severity of Pylint message type 'Refactor/R'.", + "python.linting.pylintCategorySeverity.refactor.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintCategorySeverity.refactor.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintCategorySeverity.warning.description": "Severity of Pylint message type 'Warning/W'.", + "python.linting.pylintCategorySeverity.warning.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintCategorySeverity.warning.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintEnabled.description": "Whether to lint Python files using pylint.", + "python.linting.pylintEnabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintEnabled.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintPath.description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", + "python.linting.pylintPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintPath.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.logging.level.description": "The logging level the extension logs at, defaults to 'error'", "python.logging.level.deprecation": "This setting is deprecated. Please use command `Developer: Set Log Level...` to set logging level.", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", From e07aa5ed30e0f54e5b1ee718ee6ef409db4dc946 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 13 Jul 2023 18:20:35 -0700 Subject: [PATCH 171/336] Update writing style based on recommendation (#21525) Closes https://github.com/microsoft/vscode-python/issues/21521 Closes https://github.com/microsoft/vscode-python/issues/21519 --------- Co-authored-by: Brett Cannon Co-authored-by: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> Co-authored-by: Luciana Abud <45497113+luabud@users.noreply.github.com> --- src/client/common/utils/localize.ts | 27 +++++++++---------- .../debugger/extension/adapter/factory.ts | 2 +- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index f32c4fec0ac9..b3af0c476957 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -53,7 +53,6 @@ export namespace Common { export const close = l10n.t('Close'); export const bannerLabelYes = l10n.t('Yes'); export const bannerLabelNo = l10n.t('No'); - export const yesPlease = l10n.t('Yes, please'); export const canceled = l10n.t('Canceled'); export const cancel = l10n.t('Cancel'); export const ok = l10n.t('Ok'); @@ -143,7 +142,7 @@ export namespace TensorBoard { export const launchNativeTensorBoardSessionCodeLens = l10n.t('▶ Launch TensorBoard Session'); export const launchNativeTensorBoardSessionCodeAction = l10n.t('Launch TensorBoard session'); export const missingSourceFile = l10n.t( - 'We could not locate the requested source file on disk. Please manually specify the file.', + 'The Python extension could not locate the requested source file on disk. Please manually specify the file.', ); export const selectMissingSourceFile = l10n.t('Choose File'); export const selectMissingSourceFileDescription = l10n.t( @@ -167,7 +166,7 @@ export namespace LanguageService { ); export const reloadAfterLanguageServerChange = l10n.t( - 'Please reload the window switching between language servers.', + 'Reload the window after switching between language servers.', ); export const lsFailedToStart = l10n.t( @@ -184,7 +183,7 @@ export namespace LanguageService { export const extractionCompletedOutputMessage = l10n.t('Language server download complete.'); export const extractionDoneOutputMessage = l10n.t('done.'); export const reloadVSCodeIfSeachPathHasChanged = l10n.t( - 'Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.', + 'Search paths have changed for this Python interpreter. Reload the extension to ensure that the IntelliSense works correctly.', ); } export namespace Interpreters { @@ -211,11 +210,11 @@ export namespace Interpreters { 'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar', ); export const installPythonTerminalMessageLinux = l10n.t( - '💡 Please try installing the Python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads', + '💡 Try installing the Python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads', ); export const installPythonTerminalMacMessage = l10n.t( - '💡 Brew does not seem to be available. Please try to download Python from https://www.python.org/downloads. Alternatively, you can install the Python package using some other available package manager.', + '💡 Brew does not seem to be available. You can download Python from https://www.python.org/downloads. Alternatively, you can install the Python package using some other available package manager.', ); export const changePythonInterpreter = l10n.t('Change Python Interpreter'); export const selectedPythonInterpreter = l10n.t('Selected Python Interpreter'); @@ -225,7 +224,7 @@ export namespace InterpreterQuickPickList { export const condaEnvWithoutPythonTooltip = l10n.t( 'Python is not available in this environment, it will automatically be installed upon selecting it', ); - export const noPythonInstalled = l10n.t('Python is not installed, please download and install it'); + export const noPythonInstalled = l10n.t('Python is not installed'); export const clickForInstructions = l10n.t('Click for instructions...'); export const globalGroupName = l10n.t('Global'); export const workspaceGroupName = l10n.t('Workspace'); @@ -266,7 +265,7 @@ export namespace Installer { export namespace ExtensionSurveyBanner { export const bannerMessage = l10n.t( - 'Can you please take 2 minutes to tell us how the Python extension is working for you?', + 'Can you take 2 minutes to tell us how the Python extension is working for you?', ); export const bannerLabelYes = l10n.t('Yes, take survey now'); export const bannerLabelNo = l10n.t('No, thanks'); @@ -418,7 +417,7 @@ export namespace Testing { export namespace OutdatedDebugger { export const outdatedDebuggerMessage = l10n.t( - 'We noticed you are attaching to ptvsd (Python debugger), which was deprecated on May 1st, 2020. Please switch to [debugpy](https://aka.ms/migrateToDebugpy).', + 'We noticed you are attaching to ptvsd (Python debugger), which was deprecated on May 1st, 2020. Use [debugpy](https://aka.ms/migrateToDebugpy) instead.', ); } @@ -435,13 +434,13 @@ export namespace SwitchToDefaultLS { } export namespace CreateEnv { - export const informEnvCreation = l10n.t('We have selected the following environment:'); + export const informEnvCreation = l10n.t('The following environment is selected:'); export const statusTitle = l10n.t('Creating environment'); export const statusStarting = l10n.t('Starting...'); export const hasVirtualEnv = l10n.t('Workspace folder contains a virtual environment'); - export const noWorkspace = l10n.t('Please open a folder when creating an environment using venv.'); + export const noWorkspace = l10n.t('A workspace is required when creating an environment using venv.'); export const pickWorkspacePlaceholder = l10n.t('Select a workspace to create environment'); @@ -465,12 +464,12 @@ export namespace CreateEnv { } export namespace Conda { - export const condaMissing = l10n.t('Please install `conda` to create conda environments.'); + export const condaMissing = l10n.t('Install `conda` to create conda environments.'); export const created = l10n.t('Environment created...'); export const installingPackages = l10n.t('Installing packages...'); export const errorCreatingEnvironment = l10n.t('Error while creating conda environment.'); export const selectPythonQuickPickPlaceholder = l10n.t( - 'Please select the version of Python to install in the environment', + 'Select the version of Python to install in the environment', ); export const creating = l10n.t('Creating conda environment...'); export const providerDescription = l10n.t('Creates a `.conda` Conda environment in the current workspace'); @@ -485,7 +484,7 @@ export namespace ToolsExtensions { 'Use the Pylint extension to enable easier configuration and new features such as quick fixes.', ); export const isortPromptMessage = l10n.t( - 'To use sort imports, please install the isort extension. It provides easier configuration and new features such as code actions.', + 'To use sort imports, install the isort extension. It provides easier configuration and new features such as code actions.', ); export const installPylintExtension = l10n.t('Install Pylint extension'); export const installFlake8Extension = l10n.t('Install Flake8 extension'); diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts index 067c2e405ea0..ecbd8afcc287 100644 --- a/src/client/debugger/extension/adapter/factory.ts +++ b/src/client/debugger/extension/adapter/factory.ts @@ -203,6 +203,6 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac * @memberof DebugAdapterDescriptorFactory */ private async notifySelectInterpreter() { - await showErrorMessage(l10n.t('Please install Python or select a Python Interpreter to use the debugger.')); + await showErrorMessage(l10n.t('Install Python or select a Python Interpreter to use the debugger.')); } } From ea768589cfaa255757dc83eee0ab021a5bef9f78 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 14 Jul 2023 09:39:34 -0700 Subject: [PATCH 172/336] Handle error tests as a different test icon (#21630) fixes https://github.com/microsoft/vscode-python/issues/21625 adds a few things: - returns error on any caught errors in pytest that aren't assertion errors - add error node type handling to result resolver - update tests for both files --- .../expected_execution_test_output.py | 2 +- .../tests/pytestadapter/test_execution.py | 5 ++- pythonFiles/vscode_pytest/__init__.py | 6 ++- .../testController/common/resultResolver.ts | 24 +++++++++++- .../resultResolver.unit.test.ts | 39 +++++++++++++++++++ 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index 92b881fdc8b8..fe1d40a55b43 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -157,7 +157,7 @@ error_raised_exception_execution_expected_output = { "error_raise_exception.py::TestSomething::test_a": { "test": "error_raise_exception.py::TestSomething::test_a", - "outcome": "failure", + "outcome": "error", "message": "ERROR MESSAGE", "traceback": "TRACEBACK", "subtest": None, diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 8f5fa191e38c..ffc84955bf54 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -174,7 +174,10 @@ def test_pytest_execution(test_ids, expected_const): assert a["cwd"] == os.fspath(TEST_DATA_PATH) actual_result_dict.update(a["result"]) for key in actual_result_dict: - if actual_result_dict[key]["outcome"] == "failure": + if ( + actual_result_dict[key]["outcome"] == "failure" + or actual_result_dict[key]["outcome"] == "error" + ): actual_result_dict[key]["message"] = "ERROR MESSAGE" if actual_result_dict[key]["traceback"] != None: actual_result_dict[key]["traceback"] = "TRACEBACK" diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 20b3dba325ef..072c5ef5d3ad 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -82,7 +82,9 @@ def pytest_exception_interact(node, call, report): ) else: # if execution, send this data that the given node failed. - report_value = "failure" + report_value = "error" + if call.excinfo.typename == "AssertionError": + report_value = "failure" node_id = str(node.nodeid) if node_id not in collected_tests_so_far: collected_tests_so_far.append(node_id) @@ -119,7 +121,7 @@ class TestOutcome(Dict): """ test: str - outcome: Literal["success", "failure", "skipped"] + outcome: Literal["success", "failure", "skipped", "error"] message: Union[str, None] traceback: Union[str, None] subtest: Optional[str] diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 320c00bf6ad0..8baf4d0d7ae7 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -102,7 +102,29 @@ export class PythonResultResolver implements ITestResultResolver { testCases.push(...tempArr); }); - if ( + if (rawTestExecData.result[keyTemp].outcome === 'error') { + const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + const text = `${rawTestExecData.result[keyTemp].test} failed with error: ${ + rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome + }\r\n${traceback}\r\n`; + const message = new TestMessage(text); + + const grabVSid = this.runIdToVSid.get(keyTemp); + // search through freshly built array of testItem to find the failed test and update UI. + testCases.forEach((indiItem) => { + if (indiItem.id === grabVSid) { + if (indiItem.uri && indiItem.range) { + message.location = new Location(indiItem.uri, indiItem.range); + runInstance.errored(indiItem, message); + runInstance.appendOutput(fixLogLines(text)); + } + } + }); + } else if ( rawTestExecData.result[keyTemp].outcome === 'failure' || rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' ) { diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 30b2ccecf513..09a68128167d 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -390,6 +390,45 @@ suite('Result Resolver tests', () => { // verify that the passed function was called for the single test item runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.once()); }); + test('resolveExecution handles error correctly as test outcome', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'error', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.errored(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); + }); test('resolveExecution handles success correctly', async () => { // test specific constants used expected values testProvider = 'pytest'; From f38f6e558f3fb3f024b4cfa6b0ebae182be94f94 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 14 Jul 2023 15:25:02 -0700 Subject: [PATCH 173/336] Add npm project for Python API (#21631) For https://github.com/microsoft/vscode-python/issues/20949 --- .eslintignore | 2 + .gitignore | 1 + build/azure-pipelines/pipeline.yml | 56 +++ .../azure-pipelines/templates/pack-steps.yml | 14 + .../azure-pipelines/templates/test-steps.yml | 23 ++ build/fail.js | 6 + gulpfile.js | 18 +- package.json | 4 +- pythonExtensionApi/.npmignore | 8 + pythonExtensionApi/LICENSE.md | 21 + pythonExtensionApi/README.md | 31 ++ pythonExtensionApi/SECURITY.md | 41 ++ pythonExtensionApi/example/todo.txt | 0 pythonExtensionApi/package-lock.json | 32 ++ pythonExtensionApi/package.json | 40 ++ .../api => pythonExtensionApi/src}/main.ts | 0 pythonExtensionApi/tsconfig.json | 34 ++ src/client/api.ts | 2 +- src/client/api/package-lock.json | 63 --- src/client/api/package.json | 26 -- src/client/api/types.ts | 389 ++++++++++++++++++ src/client/deprecatedProposedApiTypes.ts | 2 +- src/client/environmentApi.ts | 2 +- src/client/extension.ts | 2 +- .../creation/proposed.createEnvApis.ts | 2 +- src/test/api.test.ts | 2 +- src/test/common.ts | 2 +- src/test/environmentApi.unit.test.ts | 2 +- src/test/initialize.ts | 2 +- tsconfig.json | 3 +- 30 files changed, 728 insertions(+), 102 deletions(-) create mode 100644 build/azure-pipelines/pipeline.yml create mode 100644 build/azure-pipelines/templates/pack-steps.yml create mode 100644 build/azure-pipelines/templates/test-steps.yml create mode 100644 build/fail.js create mode 100644 pythonExtensionApi/.npmignore create mode 100644 pythonExtensionApi/LICENSE.md create mode 100644 pythonExtensionApi/README.md create mode 100644 pythonExtensionApi/SECURITY.md create mode 100644 pythonExtensionApi/example/todo.txt create mode 100644 pythonExtensionApi/package-lock.json create mode 100644 pythonExtensionApi/package.json rename {src/client/api => pythonExtensionApi/src}/main.ts (100%) create mode 100644 pythonExtensionApi/tsconfig.json delete mode 100644 src/client/api/package-lock.json delete mode 100644 src/client/api/package.json create mode 100644 src/client/api/types.ts diff --git a/.eslintignore b/.eslintignore index ad69cab31ea7..7f6bb48d6c8e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,5 @@ +pythonExtensionApi/out/ + # The following files were grandfathered out of eslint. They can be removed as time permits. src/test/analysisEngineTest.ts diff --git a/.gitignore b/.gitignore index 0fc4c34d7127..ec46f481e79b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ cucumber-report.json port.txt precommit.hook pythonFiles/lib/** +pythonFiles/get_pip.py debug_coverage*/** languageServer/** languageServer.*/** diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml new file mode 100644 index 000000000000..9cab7dedf9b1 --- /dev/null +++ b/build/azure-pipelines/pipeline.yml @@ -0,0 +1,56 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +name: $(Date:yyyyMMdd)$(Rev:.r) + +pr: none + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + ref: main + endpoint: Monaco + +parameters: + - name: quality + displayName: Quality + type: string + default: latest + values: + - latest + - next + - name: publishPythonApi + displayName: 🚀 Publish pythonExtensionApi + type: boolean + default: false + +extends: + template: azure-pipelines/npm-package/pipeline.yml@templates + parameters: + npmPackages: + - name: pythonExtensionApi + testPlatforms: + - name: Linux + nodeVersions: + - 16.17.1 + - name: MacOS + nodeVersions: + - 16.17.1 + - name: Windows + nodeVersions: + - 16.17.1 + testSteps: + - template: /build/azure-pipelines/templates/test-steps.yml@self + parameters: + package: pythonExtensionApi + buildSteps: + - template: /build/azure-pipelines/templates/pack-steps.yml@self + parameters: + package: pythonExtensionApi + ghTagPrefix: release/pythonExtensionApi/ + tag: ${{ parameters.quality }} + publishPackage: ${{ parameters.publishPythonApi }} + workingDirectory: $(Build.SourcesDirectory)/pythonExtensionApi diff --git a/build/azure-pipelines/templates/pack-steps.yml b/build/azure-pipelines/templates/pack-steps.yml new file mode 100644 index 000000000000..97037efb59ba --- /dev/null +++ b/build/azure-pipelines/templates/pack-steps.yml @@ -0,0 +1,14 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +parameters: +- name: package + +steps: + - script: npm install --root-only + workingDirectory: $(Build.SourcesDirectory) + displayName: Install root dependencies + - script: npm install + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.package }} + displayName: Install package dependencies diff --git a/build/azure-pipelines/templates/test-steps.yml b/build/azure-pipelines/templates/test-steps.yml new file mode 100644 index 000000000000..15eb3db6384d --- /dev/null +++ b/build/azure-pipelines/templates/test-steps.yml @@ -0,0 +1,23 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +parameters: +- name: package + type: string +- name: script + type: string + default: 'all:publish' + +steps: + - script: npm install --root-only + workingDirectory: $(Build.SourcesDirectory) + displayName: Install root dependencies + - bash: | + /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + echo ">>> Started xvfb" + displayName: Start xvfb + condition: eq(variables['Agent.OS'], 'Linux') + - script: npm run ${{ parameters.script }} + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.package }} + displayName: Verify package diff --git a/build/fail.js b/build/fail.js new file mode 100644 index 000000000000..2adc808d8da9 --- /dev/null +++ b/build/fail.js @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +process.exitCode = 1; diff --git a/gulpfile.js b/gulpfile.js index 0d47127f187e..66f96bf48ec0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -27,7 +27,7 @@ const tsProject = ts.createProject('./tsconfig.json', { typescript }); const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; -gulp.task('compile', (done) => { +gulp.task('compileCore', (done) => { let failed = false; tsProject .src() @@ -39,6 +39,22 @@ gulp.task('compile', (done) => { .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); }); +const apiTsProject = ts.createProject('./pythonExtensionApi/tsconfig.json', { typescript }); + +gulp.task('compileApi', (done) => { + let failed = false; + apiTsProject + .src() + .pipe(apiTsProject()) + .on('error', () => { + failed = true; + }) + .js.pipe(gulp.dest('./pythonExtensionApi/out')) + .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); +}); + +gulp.task('compile', gulp.series('compileCore', 'compileApi')); + gulp.task('precommit', (done) => run({ exitOnError: true, mode: 'staged' }, done)); gulp.task('output:clean', () => del(['coverage'])); diff --git a/package.json b/package.json index 0ef0ff4d79b6..e38e6921e50a 100644 --- a/package.json +++ b/package.json @@ -2064,8 +2064,8 @@ "testSmoke": "cross-env INSTALL_JUPYTER_EXTENSION=true \"node ./out/test/smokeTest.js\"", "testInsiders": "cross-env VSC_PYTHON_CI_TEST_VSC_CHANNEL=insiders INSTALL_PYLANCE_EXTENSION=true TEST_FILES_SUFFIX=insiders.test CODE_TESTS_WORKSPACE=src/testMultiRootWkspc/smokeTests \"node ./out/test/standardTest.js\"", "lint-staged": "node gulpfile.js", - "lint": "eslint --ext .ts,.js src build", - "lint-fix": "eslint --fix --ext .ts,.js src build gulpfile.js", + "lint": "eslint --ext .ts,.js src build pythonExtensionApi", + "lint-fix": "eslint --fix --ext .ts,.js src build pythonExtensionApi gulpfile.js", "format-check": "prettier --check 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", "format-fix": "prettier --write 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", "clean": "gulp clean", diff --git a/pythonExtensionApi/.npmignore b/pythonExtensionApi/.npmignore new file mode 100644 index 000000000000..283d589ea5fe --- /dev/null +++ b/pythonExtensionApi/.npmignore @@ -0,0 +1,8 @@ +example/** +dist/ +out/**/*.map +out/**/*.tsbuildInfo +src/ +.eslintrc* +.eslintignore +tsconfig*.json diff --git a/pythonExtensionApi/LICENSE.md b/pythonExtensionApi/LICENSE.md new file mode 100644 index 000000000000..767f4076ba05 --- /dev/null +++ b/pythonExtensionApi/LICENSE.md @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md new file mode 100644 index 000000000000..d7ca4ccfdae4 --- /dev/null +++ b/pythonExtensionApi/README.md @@ -0,0 +1,31 @@ +# Python extension's API + +This npm module implements an API facade for the Python extension in VS Code. + +## Example + +The source code of the example can be found [here](TODO Update example extension link here) + +First we need to define a `package.json` for the extension that wants to use the API: + +```jsonc +{ + "name": "...", + ... + // depend on the Python extension + "extensionDependencies": [ + "ms-python.python" + ], + // Depend on the Python extension facade npm module to get easier API access to the + // core extension. + "dependencies": { + "@vscode/python-extension": "..." + }, +} +``` + +TODO insert example here + +```typescript +TODO +``` diff --git a/pythonExtensionApi/SECURITY.md b/pythonExtensionApi/SECURITY.md new file mode 100644 index 000000000000..a050f362c152 --- /dev/null +++ b/pythonExtensionApi/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + diff --git a/pythonExtensionApi/example/todo.txt b/pythonExtensionApi/example/todo.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json new file mode 100644 index 000000000000..9c7c4046870e --- /dev/null +++ b/pythonExtensionApi/package-lock.json @@ -0,0 +1,32 @@ +{ + "name": "@vscode/python-extension", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@vscode/python-extension", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/vscode": "^1.78.0" + }, + "engines": { + "node": ">=16.17.1", + "vscode": "^1.78.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", + "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + } + }, + "dependencies": { + "@types/vscode": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", + "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + } + } +} diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json new file mode 100644 index 000000000000..43b4c19877c3 --- /dev/null +++ b/pythonExtensionApi/package.json @@ -0,0 +1,40 @@ +{ + "name": "@vscode/python-extension", + "description": "An API facade for the Python extension in VS Code", + "version": "1.0.0", + "author": { + "name": "Microsoft Corporation" + }, + "keywords": [ + "Python", + "VSCode", + "API" + ], + "main": "./out/main.js", + "types": "./out/main.d.ts", + "engines": { + "node": ">=16.17.1", + "vscode": "^1.78.0" + }, + "license": "MIT", + "homepage": "https://github.com/Microsoft/vscode-python", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-python" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-python/issues" + }, + "dependencies": { + "@types/vscode": "^1.78.0" + }, + "scripts": { + "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", + "prepack": "npm run all:publish", + "compile": "node ../node_modules/typescript/lib/tsc.js -b ./tsconfig.json", + "clean": "node ../node_modules/rimraf/bin.js out", + "lint": "node ../node_modules/eslint/bin/eslint.js --ext ts src", + "all": "npm run clean && npm run compile", + "all:publish": "git clean -xfd . && npm install && npm run compile" + } +} diff --git a/src/client/api/main.ts b/pythonExtensionApi/src/main.ts similarity index 100% rename from src/client/api/main.ts rename to pythonExtensionApi/src/main.ts diff --git a/pythonExtensionApi/tsconfig.json b/pythonExtensionApi/tsconfig.json new file mode 100644 index 000000000000..d90209b3a4b6 --- /dev/null +++ b/pythonExtensionApi/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": ["types/*"] + }, + "module": "commonjs", + "target": "es2018", + "outDir": "./out", + "lib": [ + "es6", + "es2018", + "dom", + "ES2019", + "ES2020" + ], + "sourceMap": true, + "rootDir": "src", + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "removeComments": true + }, + "exclude": [ + "node_modules", + "out" + ] +} diff --git a/src/client/api.ts b/src/client/api.ts index 7cf580d9f78f..32ce68f6a28b 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -10,7 +10,7 @@ import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient import { LanguageClient } from 'vscode-languageclient/node'; import { PYLANCE_NAME } from './activation/node/languageClientFactory'; import { ILanguageServerOutputChannel } from './activation/types'; -import { PythonExtension } from './api/main'; +import { PythonExtension } from './api/types'; import { isTestExecution, PYTHON_LANGUAGE } from './common/constants'; import { IConfigurationService, Resource } from './common/types'; import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers'; diff --git a/src/client/api/package-lock.json b/src/client/api/package-lock.json deleted file mode 100644 index 137262c6523b..000000000000 --- a/src/client/api/package-lock.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@vscode/python-extension", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "@vscode/python-extension", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@types/vscode": "^1.78.0" - }, - "devDependencies": { - "@types/node": "^16.11.7", - "typescript": "^4.7.2" - } - }, - "node_modules/@types/node": { - "version": "16.18.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.27.tgz", - "integrity": "sha512-GFfndd/RINWD19W+xNJ9Qh/sOZ5ieTiOSagA86ER/12i/l+MEnQxsbldGRF23azWjRfe7zUlAldyrwN84a1E5w==", - "dev": true - }, - "node_modules/@types/vscode": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.78.0.tgz", - "integrity": "sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==" - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - } - }, - "dependencies": { - "@types/node": { - "version": "16.18.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.27.tgz", - "integrity": "sha512-GFfndd/RINWD19W+xNJ9Qh/sOZ5ieTiOSagA86ER/12i/l+MEnQxsbldGRF23azWjRfe7zUlAldyrwN84a1E5w==", - "dev": true - }, - "@types/vscode": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.78.0.tgz", - "integrity": "sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==" - }, - "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true - } - } -} diff --git a/src/client/api/package.json b/src/client/api/package.json deleted file mode 100644 index 901ab3f13cfd..000000000000 --- a/src/client/api/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@vscode/python-extension", - "description": "VSCode Python extension's public API", - "version": "1.0.0", - "publisher": "ms-python", - "author": { - "name": "Microsoft Corporation" - }, - "types": "./index.d.ts", - "license": "MIT", - "homepage": "https://github.com/Microsoft/vscode-python", - "repository": { - "type": "git", - "url": "https://github.com/Microsoft/vscode-python" - }, - "bugs": { - "url": "https://github.com/Microsoft/vscode-python/issues" - }, - "dependencies": { - "@types/vscode": "^1.78.0" - }, - "devDependencies": { - "@types/node": "^16.11.7", - "typescript": "^4.7.2" - } -} diff --git a/src/client/api/types.ts b/src/client/api/types.ts new file mode 100644 index 000000000000..4e13ec4853ec --- /dev/null +++ b/src/client/api/types.ts @@ -0,0 +1,389 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem } from 'vscode'; + +/* + * Do not introduce any breaking changes to this API. + * This is the public API for other extensions to interact with this extension. + */ +export interface PythonExtension { + /** + * Promise indicating whether all parts of the extension have completed loading or not. + * @type {Promise} + */ + ready: Promise; + jupyter: { + registerHooks(): void; + }; + debug: { + /** + * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. + * Users can append another array of strings of what they want to execute along with relevant arguments to Python. + * E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` + * @param {string} host + * @param {number} port + * @param {boolean} [waitUntilDebuggerAttaches=true] + * @returns {Promise} + */ + getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; + + /** + * Gets the path to the debugger package used by the extension. + * @returns {Promise} + */ + getDebuggerPackagePath(): Promise; + }; + + datascience: { + /** + * Launches Data Viewer component. + * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. + * @param {string} title Data Viewer title + */ + showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; + /** + * Registers a remote server provider component that's used to pick remote jupyter server URIs + * @param serverProvider object called back when picking jupyter server URI + */ + registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; + }; + + /** + * These APIs provide a way for extensions to work with by python environments available in the user's machine + * as found by the Python extension. See + * https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs for usage examples and more. + */ + readonly environments: { + /** + * Returns the environment configured by user in settings. Note that this can be an invalid environment, use + * {@link resolveEnvironment} to get full details. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getActiveEnvironmentPath(resource?: Resource): EnvironmentPath; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + updateActiveEnvironmentPath( + environment: string | EnvironmentPath | Environment, + resource?: Resource, + ): Promise; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event; + /** + * Carries environments known to the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + * + * Only reports environments in the current workspace. + */ + readonly known: readonly Environment[]; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + readonly onDidChangeEnvironments: Event; + /** + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. + * + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. + * @param options Additional options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. + */ + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + */ + resolveEnvironment( + environment: Environment | EnvironmentPath | string, + ): Promise; + /** + * Returns the environment variables used by the extension for a resource, which includes the custom + * variables configured by user in `.env` files. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getEnvironmentVariables(resource?: Resource): EnvironmentVariables; + /** + * This event is fired when the environment variables for a resource change. Note it's currently not + * possible to detect if environment variables in the system change, so this only fires if custom + * environment variables are updated in `.env` files. + */ + readonly onDidEnvironmentVariablesChange: Event; + }; +} + +interface IJupyterServerUri { + baseUrl: string; + token: string; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + authorizationHeader: any; // JSON object for authorization header. + expiration?: Date; // Date/time when header expires and should be refreshed. + displayName: string; +} + +type JupyterServerUriHandle = string; + +export interface IJupyterUriProvider { + readonly id: string; // Should be a unique string (like a guid) + getQuickPickEntryItems(): QuickPickItem[]; + handleQuickPick(item: QuickPickItem, backEnabled: boolean): Promise; + getServerUri(handle: JupyterServerUriHandle): Promise; +} + +interface IDataFrameInfo { + columns?: { key: string; type: ColumnType }[]; + indexColumn?: string; + rowCount?: number; +} + +export interface IDataViewerDataProvider { + dispose(): void; + getDataFrameInfo(): Promise; + getAllRows(): Promise; + getRows(start: number, end: number): Promise; +} + +enum ColumnType { + String = 'string', + Number = 'number', + Bool = 'bool', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type IRowsResponse = any[]; + +export type RefreshOptions = { + /** + * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. + */ + forceRefresh?: boolean; +}; + +/** + * Details about the environment. Note the environment folder, type and name never changes over time. + */ +export type Environment = EnvironmentPath & { + /** + * Carries details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness if known at this moment. + */ + readonly bitness: Bitness | undefined; + /** + * Value of `sys.prefix` in sys module if known at this moment. + */ + readonly sysPrefix: string | undefined; + }; + /** + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. + */ + readonly environment: + | { + /** + * Type of the environment. + */ + readonly type: EnvironmentType; + /** + * Name to the environment if any. + */ + readonly name: string | undefined; + /** + * Uri of the environment folder. + */ + readonly folderUri: Uri; + /** + * Any specific workspace folder this environment is created for. + */ + readonly workspaceFolder: WorkspaceFolder | undefined; + } + | undefined; + /** + * Carries Python version information known at this moment, carries `undefined` for envs without python. + */ + readonly version: + | (VersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string | undefined; + }) + | undefined; + /** + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary tool which manages the environment, which never changes over time. + * + * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for + * global interpreters. + */ + readonly tools: readonly EnvironmentTools[]; +}; + +/** + * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an + * {@link Environment} with complete information. + */ +export type ResolvedEnvironment = Environment & { + /** + * Carries complete details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness of the environment. + */ + readonly bitness: Bitness; + /** + * Value of `sys.prefix` in sys module. + */ + readonly sysPrefix: string; + }; + /** + * Carries complete Python version information, carries `undefined` for envs without python. + */ + readonly version: + | (ResolvedVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string; + }) + | undefined; +}; + +export type EnvironmentsChangeEvent = { + readonly env: Environment; + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + readonly type: 'add' | 'remove' | 'update'; +}; + +export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { + /** + * Workspace folder the environment changed for. + */ + readonly resource: WorkspaceFolder | undefined; +}; + +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; + +export type EnvironmentPath = { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; +}; + +/** + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which + * was contributed. + */ +export type EnvironmentTools = KnownEnvironmentTools | string; +/** + * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. + */ +export type KnownEnvironmentTools = + | 'Conda' + | 'Pipenv' + | 'Poetry' + | 'VirtualEnv' + | 'Venv' + | 'VirtualEnvWrapper' + | 'Pyenv' + | 'Unknown'; + +/** + * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. + */ +export type EnvironmentType = KnownEnvironmentTypes | string; +/** + * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their + * own separate extensions, in which case they're expected to provide the type themselves. + */ +export type KnownEnvironmentTypes = 'VirtualEnvironment' | 'Conda' | 'Unknown'; + +/** + * Carries bitness for an environment. + */ +export type Bitness = '64-bit' | '32-bit' | 'Unknown'; + +/** + * The possible Python release levels. + */ +export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + readonly level: PythonReleaseLevel; + readonly serial: number; +}; + +export type VersionInfo = { + readonly major: number | undefined; + readonly minor: number | undefined; + readonly micro: number | undefined; + readonly release: PythonVersionRelease | undefined; +}; + +export type ResolvedVersionInfo = { + readonly major: number; + readonly minor: number; + readonly micro: number; + readonly release: PythonVersionRelease; +}; + +/** + * A record containing readonly keys. + */ +export type EnvironmentVariables = { readonly [key: string]: string | undefined }; + +export type EnvironmentVariablesChangeEvent = { + /** + * Workspace folder the environment variables changed for. + */ + readonly resource: WorkspaceFolder | undefined; + /** + * Updated value of environment variables. + */ + readonly env: EnvironmentVariables; +}; diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts index 407cb1dab394..79b267d5b873 100644 --- a/src/client/deprecatedProposedApiTypes.ts +++ b/src/client/deprecatedProposedApiTypes.ts @@ -4,7 +4,7 @@ import { Uri, Event } from 'vscode'; import { PythonEnvKind, EnvPathType } from './pythonEnvironments/base/info'; import { ProgressNotificationEvent, GetRefreshEnvironmentsOptions } from './pythonEnvironments/base/locator'; -import { Resource } from './api/main'; +import { Resource } from './api/types'; export interface EnvironmentDetailsOptions { useCache: boolean; diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts index ee0b28ae2ff4..da6a132b2b44 100644 --- a/src/client/environmentApi.ts +++ b/src/client/environmentApi.ts @@ -30,7 +30,7 @@ import { RefreshOptions, ResolvedEnvironment, Resource, -} from './api/main'; +} from './api/types'; import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi'; type ActiveEnvironmentChangeEvent = { diff --git a/src/client/extension.ts b/src/client/extension.ts index 89649d377c74..a4f5bd5dbfd0 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -41,7 +41,7 @@ import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; import { IStartupDurations } from './types'; import { runAfterActivation } from './common/utils/runAfterActivation'; import { IInterpreterService } from './interpreter/contracts'; -import { PythonExtension } from './api/main'; +import { PythonExtension } from './api/types'; import { WorkspaceService } from './common/application/workspace'; import { disposeAll } from './common/utils/resourceLifecycle'; import { ProposedExtensionAPI } from './proposedApiTypes'; diff --git a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts index e0e79134fc56..0120b2a6e8d7 100644 --- a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts +++ b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License import { Event, Disposable, WorkspaceFolder } from 'vscode'; -import { EnvironmentTools } from '../../api/main'; +import { EnvironmentTools } from '../../api/types'; export type CreateEnvironmentUserActions = 'Back' | 'Cancel'; export type EnvironmentProviderId = string; diff --git a/src/test/api.test.ts b/src/test/api.test.ts index 488ce79073c2..f0813ce16a9b 100644 --- a/src/test/api.test.ts +++ b/src/test/api.test.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { expect } from 'chai'; -import { PythonExtension } from '../client/api/main'; +import { PythonExtension } from '../client/api/types'; import { ProposedExtensionAPI } from '../client/proposedApiTypes'; import { initialize } from './initialize'; diff --git a/src/test/common.ts b/src/test/common.ts index 0168ee47fc37..95345f91e5e0 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -10,7 +10,7 @@ import * as glob from 'glob'; import * as path from 'path'; import { coerce, SemVer } from 'semver'; import { ConfigurationTarget, Event, TextDocument, Uri } from 'vscode'; -import type { PythonExtension } from '../client/api/main'; +import type { PythonExtension } from '../client/api/types'; import { IProcessService } from '../client/common/process/types'; import { IDisposable } from '../client/common/types'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; diff --git a/src/test/environmentApi.unit.test.ts b/src/test/environmentApi.unit.test.ts index 76441247db49..1d8dc3e5c847 100644 --- a/src/test/environmentApi.unit.test.ts +++ b/src/test/environmentApi.unit.test.ts @@ -37,7 +37,7 @@ import { EnvironmentVariablesChangeEvent, EnvironmentsChangeEvent, PythonExtension, -} from '../client/api/main'; +} from '../client/api/types'; suite('Python Environment API', () => { const workspacePath = 'path/to/workspace'; diff --git a/src/test/initialize.ts b/src/test/initialize.ts index 8a7abca0d91c..add1d8624461 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import type { PythonExtension } from '../client/api/main'; +import type { PythonExtension } from '../client/api/types'; import { clearPythonPathInWorkspaceFolder, IExtensionTestApi, diff --git a/tsconfig.json b/tsconfig.json index 89f7a9c808b8..797bf6736f15 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,6 +38,7 @@ "src/smoke", "build", "out", - "tmp" + "tmp", + "pythonExtensionApi" ] } From f4d755634937b43c14c7db2d4d99fe17a2377204 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 14 Jul 2023 16:03:00 -0700 Subject: [PATCH 174/336] Update README for Python extension API to point to examples (#21638) For https://github.com/microsoft/vscode-python/issues/20949 --- build/azure-pipelines/pipeline.yml | 2 ++ pythonExtensionApi/README.md | 27 +++++++++++++++++++++++---- pythonExtensionApi/example/todo.txt | 0 3 files changed, 25 insertions(+), 4 deletions(-) delete mode 100644 pythonExtensionApi/example/todo.txt diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml index 9cab7dedf9b1..85b41c16efc0 100644 --- a/build/azure-pipelines/pipeline.yml +++ b/build/azure-pipelines/pipeline.yml @@ -4,6 +4,8 @@ ############################################################################################### name: $(Date:yyyyMMdd)$(Rev:.r) +trigger: none + pr: none resources: diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md index d7ca4ccfdae4..3f313721d17c 100644 --- a/pythonExtensionApi/README.md +++ b/pythonExtensionApi/README.md @@ -4,8 +4,6 @@ This npm module implements an API facade for the Python extension in VS Code. ## Example -The source code of the example can be found [here](TODO Update example extension link here) - First we need to define a `package.json` for the extension that wants to use the API: ```jsonc @@ -24,8 +22,29 @@ First we need to define a `package.json` for the extension that wants to use the } ``` -TODO insert example here +The actual source code to get the active environment to run some script could look like this: ```typescript -TODO +// Import the API +import { PythonExtension } from '@vscode/python-extension'; + +// Load the Python extension API +const pythonApi: PythonExtension = await PythonExtension.api(); + +// This will return something like /usr/bin/python +const environmentPath = pythonApi.environments.getActiveEnvironmentPath(); + +// `environmentPath.path` carries the value of the setting. Note that this path may point to a folder and not the +// python binary. Depends entirely on how the env was created. +// E.g., `conda create -n myenv python` ensures the env has a python binary +// `conda create -n myenv` does not include a python binary. +// Also, the path specified may not be valid, use the following to get complete details for this environment if +// need be. + +const environment = await pythonApi.environments.resolveEnvironment(environmentPath); +if (environment) { + // run your script here. +} ``` + +Check out [the wiki](https://aka.ms/pythonEnvironmentApi) for many more examples and usage. diff --git a/pythonExtensionApi/example/todo.txt b/pythonExtensionApi/example/todo.txt deleted file mode 100644 index e69de29bb2d1..000000000000 From fc1c391c33fad027d1e51ff60cbf648668af4b1c Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Mon, 17 Jul 2023 22:16:13 +0200 Subject: [PATCH 175/336] Compare global storage data using only `key` (#21636) Closes https://github.com/microsoft/vscode-python/issues/21635 by applying the same fix as done in https://github.com/microsoft/vscode-python/pull/17627. --- src/client/common/persistentState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index 48e885a676a2..76f6d2112fe0 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -173,7 +173,7 @@ export interface IPersistentStorage { */ export function getGlobalStorage(context: IExtensionContext, key: string, defaultValue?: T): IPersistentStorage { const globalKeysStorage = new PersistentState(context.globalState, GLOBAL_PERSISTENT_KEYS, []); - const found = globalKeysStorage.value.find((value) => value.key === key && value.defaultValue === defaultValue); + const found = globalKeysStorage.value.find((value) => value.key === key); if (!found) { const newValue = [{ key, defaultValue }, ...globalKeysStorage.value]; globalKeysStorage.updateValue(newValue).ignoreErrors(); From 2e8dc67455ed779eddab6a609601026f763b6803 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 17 Jul 2023 13:55:15 -0700 Subject: [PATCH 176/336] Add extra logging regarding interpreter discovery (#21639) For https://github.com/microsoft/vscode-python/issues/21310 --- src/client/common/process/rawProcessApis.ts | 4 +++- src/client/common/utils/async.ts | 6 ++++++ .../base/locators/lowLevel/activeStateLocator.ts | 1 + .../base/locators/lowLevel/microsoftStoreLocator.ts | 1 + .../base/locators/lowLevel/posixKnownPathsLocator.ts | 1 + .../base/locators/lowLevel/pyenvLocator.ts | 1 + .../base/locators/lowLevel/windowsKnownPathsLocator.ts | 1 + .../base/locators/lowLevel/windowsRegistryLocator.ts | 1 + 8 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts index 025e5b607229..6f3e40d68736 100644 --- a/src/client/common/process/rawProcessApis.ts +++ b/src/client/common/process/rawProcessApis.ts @@ -100,7 +100,7 @@ export function plainExec( const deferred = createDeferred>(); const disposable: IDisposable = { dispose: () => { - if (!proc.killed && !deferred.completed) { + if (!proc.killed) { proc.kill(); } }, @@ -156,10 +156,12 @@ export function plainExec( deferred.resolve({ stdout, stderr }); } internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); }); proc.once('error', (ex) => { deferred.reject(ex); internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); }); return deferred.promise; diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts index 29bf4a8d6fca..5905399cd4a1 100644 --- a/src/client/common/utils/async.ts +++ b/src/client/common/utils/async.ts @@ -50,11 +50,17 @@ class DeferredImpl implements Deferred { } public resolve(_value: T | PromiseLike) { + if (this.completed) { + return; + } this._resolve.apply(this.scope ? this.scope : this, [_value]); this._resolved = true; } public reject(_reason?: string | Error | Record) { + if (this.completed) { + return; + } this._reject.apply(this.scope ? this.scope : this, [_reason]); this._rejected = true; } diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts index 987abf4f4157..dc507b9c94bd 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts @@ -20,6 +20,7 @@ export class ActiveStateLocator extends LazyResourceBasedLocator { traceVerbose(`Couldn't locate the state binary.`); return; } + traceVerbose(`Searching for active state environments`); const projects = await state.getProjects(); if (projects === undefined) { traceVerbose(`Couldn't fetch State Tool projects.`); diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts index 9b5283f7f967..7adeeae89858 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts @@ -87,6 +87,7 @@ export class MicrosoftStoreLocator extends FSWatchingLocator { protected doIterEnvs(): IPythonEnvsIterator { const iterator = async function* (kind: PythonEnvKind) { + traceVerbose('Searching for windows store envs'); const exes = await getMicrosoftStorePythonExes(); yield* exes.map(async (executablePath: string) => ({ kind, diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts index 97726307c573..2e4e2dc13e61 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts @@ -26,6 +26,7 @@ export class PosixKnownPathsLocator extends Locator { } const iterator = async function* (kind: PythonEnvKind) { + traceVerbose('Searching for interpreters in posix paths locator'); // Filter out pyenv shims. They are not actual python binaries, they are used to launch // the binaries specified in .python-version file in the cwd. We should not be reporting // those binaries as environments. diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts index dc3290c9993c..4fd1891a179c 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts @@ -16,6 +16,7 @@ import { traceError, traceVerbose } from '../../../../logging'; * all the environments (global or virtual) in that directory. */ async function* getPyenvEnvironments(): AsyncIterableIterator { + traceVerbose('Searching for pyenv environments'); const pyenvVersionDir = getPyenvVersionsDir(); const subDirs = getSubDirs(pyenvVersionDir, { resolveSymlinks: true }); diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts index 377b1117b858..337a8fb09a97 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts @@ -93,6 +93,7 @@ function getDirFilesLocator( // rather than in each low-level locator. In the meantime we // take a naive approach. async function* iterEnvs(query: PythonLocatorQuery): IPythonEnvsIterator { + traceVerbose('Searching for windows path interpreters'); yield* await getEnvs(locator.iterEnvs(query)); traceVerbose('Finished searching for windows path interpreters'); } diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts index 954d1bfd2a41..b52e9c35779f 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts @@ -14,6 +14,7 @@ export class WindowsRegistryLocator extends Locator { // eslint-disable-next-line class-methods-use-this public iterEnvs(): IPythonEnvsIterator { const iterator = async function* () { + traceVerbose('Searching for windows registry interpreters'); const interpreters = await getRegistryInterpreters(); for (const interpreter of interpreters) { try { From f7125dadd5422c31b26cb1d50f36fb638a6c34f0 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 17 Jul 2023 16:13:11 -0700 Subject: [PATCH 177/336] Use correct `tsconfig.json` when generating npm package (#21651) For #21631 - Unset `removeComment` as that leads to declarations without docstrings - Set to generate declarations - Use updated typescript which results in cleaner declaration files --- pythonExtensionApi/.eslintrc | 8 ++++++++ pythonExtensionApi/README.md | 2 ++ pythonExtensionApi/package-lock.json | 26 ++++++++++++++++++++++++-- pythonExtensionApi/package.json | 10 +++++++--- pythonExtensionApi/src/main.ts | 5 +++++ pythonExtensionApi/tsconfig.json | 2 +- 6 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 pythonExtensionApi/.eslintrc diff --git a/pythonExtensionApi/.eslintrc b/pythonExtensionApi/.eslintrc new file mode 100644 index 000000000000..cbf1a6350029 --- /dev/null +++ b/pythonExtensionApi/.eslintrc @@ -0,0 +1,8 @@ +{ + "rules": { + "padding-line-between-statements": [ + "error", + { "blankLine": "always", "prev": "export", "next": "*" } + ] + } +} diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md index 3f313721d17c..1587635aae40 100644 --- a/pythonExtensionApi/README.md +++ b/pythonExtensionApi/README.md @@ -28,6 +28,8 @@ The actual source code to get the active environment to run some script could lo // Import the API import { PythonExtension } from '@vscode/python-extension'; +... + // Load the Python extension API const pythonApi: PythonExtension = await PythonExtension.api(); diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index 9c7c4046870e..a8abd6d85bfc 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -1,16 +1,19 @@ { "name": "@vscode/python-extension", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@types/vscode": "^1.78.0" }, + "devDependencies": { + "typescript": "5.0.4" + }, "engines": { "node": ">=16.17.1", "vscode": "^1.78.0" @@ -20,6 +23,19 @@ "version": "1.80.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } } }, "dependencies": { @@ -27,6 +43,12 @@ "version": "1.80.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + }, + "typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true } } } diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index 43b4c19877c3..aaeaaf54a0c4 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Microsoft Corporation" }, @@ -28,13 +28,17 @@ "dependencies": { "@types/vscode": "^1.78.0" }, + "devDependencies": { + "typescript": "5.0.4" + }, "scripts": { "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", "prepack": "npm run all:publish", - "compile": "node ../node_modules/typescript/lib/tsc.js -b ./tsconfig.json", + "compile": "node ./node_modules/typescript/lib/tsc.js -b ./tsconfig.json", "clean": "node ../node_modules/rimraf/bin.js out", "lint": "node ../node_modules/eslint/bin/eslint.js --ext ts src", "all": "npm run clean && npm run compile", - "all:publish": "git clean -xfd . && npm install && npm run compile" + "formatTypings": "node ../node_modules/eslint/bin/eslint.js --fix ./out/main.d.ts", + "all:publish": "git clean -xfd . && npm install && npm run compile && npm run formatTypings" } } diff --git a/pythonExtensionApi/src/main.ts b/pythonExtensionApi/src/main.ts index b9266a732826..b980a06b72f8 100644 --- a/pythonExtensionApi/src/main.ts +++ b/pythonExtensionApi/src/main.ts @@ -316,6 +316,7 @@ export type EnvironmentPath = { * was contributed. */ export type EnvironmentTools = KnownEnvironmentTools | string; + /** * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink * once tools have their own separate extensions. @@ -334,6 +335,7 @@ export type KnownEnvironmentTools = * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. */ export type EnvironmentType = KnownEnvironmentTypes | string; + /** * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their * own separate extensions, in which case they're expected to provide the type themselves. @@ -392,6 +394,9 @@ export const PVSC_EXTENSION_ID = 'ms-python.python'; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace PythonExtension { + /** + * Returns the API exposed by the Python extension in VS Code. + */ export async function api(): Promise { const extension = extensions.getExtension(PVSC_EXTENSION_ID); if (extension === undefined) { diff --git a/pythonExtensionApi/tsconfig.json b/pythonExtensionApi/tsconfig.json index d90209b3a4b6..9ab7617023df 100644 --- a/pythonExtensionApi/tsconfig.json +++ b/pythonExtensionApi/tsconfig.json @@ -25,7 +25,7 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "resolveJsonModule": true, - "removeComments": true + "declaration": true }, "exclude": [ "node_modules", From c1442000d8582e081054e48501b32f4d13c07b01 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 17 Jul 2023 16:53:45 -0700 Subject: [PATCH 178/336] Modify .eslintrc to turn off any errors for declaration files (#21652) For #21631 --- pythonExtensionApi/.eslintrc | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pythonExtensionApi/.eslintrc b/pythonExtensionApi/.eslintrc index cbf1a6350029..8828c49002ed 100644 --- a/pythonExtensionApi/.eslintrc +++ b/pythonExtensionApi/.eslintrc @@ -1,8 +1,11 @@ { - "rules": { - "padding-line-between-statements": [ - "error", - { "blankLine": "always", "prev": "export", "next": "*" } - ] - } + "overrides": [ + { + "files": ["**/main.d.ts"], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "padding-line-between-statements": ["error", { "blankLine": "always", "prev": "export", "next": "*" }] + } + } + ] } From 81ae205e871d4326e7b549ee2667844c50e16c34 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 18 Jul 2023 16:34:58 -0700 Subject: [PATCH 179/336] Bring back feature to Run Python file in dedicated terminal (#21656) Closes https://github.com/microsoft/vscode-python/issues/21282 Closes https://github.com/microsoft/vscode-python/issues/21420 Closes https://github.com/microsoft/vscode-python/issues/21215 Reverts microsoft/vscode-python#21418 --- package.json | 19 +++++++ package.nls.json | 1 + src/client/common/constants.ts | 1 + src/client/common/terminal/factory.ts | 21 +++++-- src/client/common/terminal/types.ts | 2 +- src/client/telemetry/index.ts | 6 ++ .../codeExecution/codeExecutionManager.ts | 56 ++++++++++++------- .../codeExecution/terminalCodeExecution.ts | 40 +++++++------ src/client/terminals/types.ts | 2 +- .../common/terminals/factory.unit.test.ts | 47 +++++++++++++++- .../codeExecutionManager.unit.test.ts | 25 ++++++--- .../terminalCodeExec.unit.test.ts | 29 +++++++++- 12 files changed, 190 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index e38e6921e50a..abadcdb3bb0b 100644 --- a/package.json +++ b/package.json @@ -392,6 +392,12 @@ "icon": "$(play)", "title": "%python.command.python.execInTerminalIcon.title%" }, + { + "category": "Python", + "command": "python.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%" + }, { "category": "Python", "command": "python.debugInTerminal", @@ -1818,6 +1824,13 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "false" }, + { + "category": "Python", + "command": "python.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%", + "when": "false" + }, { "category": "Python", "command": "python.debugInTerminal", @@ -1976,6 +1989,12 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, + { + "command": "python.execInDedicatedTerminal", + "group": "navigation@0", + "title": "%python.command.python.execInDedicatedTerminal.title%", + "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" + }, { "command": "python.debugInTerminal", "group": "navigation@1", diff --git a/package.nls.json b/package.nls.json index 84c920b7c22e..79609f02e83a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -7,6 +7,7 @@ "python.command.python.execInTerminal.title": "Run Python File in Terminal", "python.command.python.debugInTerminal.title": "Debug Python File", "python.command.python.execInTerminalIcon.title": "Run Python File", + "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index b285667aaa6a..bea0ef9e235c 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -44,6 +44,7 @@ export namespace Commands { export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; + export const Exec_In_Separate_Terminal = 'python.execInDedicatedTerminal'; export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index 3855cb6cee3c..39cc88c4b024 100644 --- a/src/client/common/terminal/factory.ts +++ b/src/client/common/terminal/factory.ts @@ -3,6 +3,7 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; +import * as path from 'path'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; @@ -23,13 +24,17 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { ) { this.terminalServices = new Map(); } - public getTerminalService(options: TerminalCreationOptions): ITerminalService { + public getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService { const resource = options?.resource; const title = options?.title; - const terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; + let terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; const interpreter = options?.interpreter; - const id = this.getTerminalId(terminalTitle, resource, interpreter); + const id = this.getTerminalId(terminalTitle, resource, interpreter, options.newTerminalPerFile); if (!this.terminalServices.has(id)) { + if (resource && options.newTerminalPerFile) { + terminalTitle = `${terminalTitle}: ${path.basename(resource.fsPath).replace('.py', '')}`; + } + options.title = terminalTitle; const terminalService = new TerminalService(this.serviceContainer, options); this.terminalServices.set(id, terminalService); } @@ -46,13 +51,19 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { title = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; return new TerminalService(this.serviceContainer, { resource, title }); } - private getTerminalId(title: string, resource?: Uri, interpreter?: PythonEnvironment): string { + private getTerminalId( + title: string, + resource?: Uri, + interpreter?: PythonEnvironment, + newTerminalPerFile?: boolean, + ): string { if (!resource && !interpreter) { return title; } const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource || undefined); - return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}`; + const fileId = resource && newTerminalPerFile ? resource.fsPath : ''; + return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${fileId}`; } } diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index 880bf0dd72fb..303188682378 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -97,7 +97,7 @@ export interface ITerminalServiceFactory { * @returns {ITerminalService} * @memberof ITerminalServiceFactory */ - getTerminalService(options: TerminalCreationOptions): ITerminalService; + getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService; createTerminalService(resource?: Uri, title?: string): ITerminalService; } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 50d7ebb0f9b1..f0b57a4043d9 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -827,6 +827,12 @@ export interface IEventNamePropertyMapping { * @type {('command' | 'icon')} */ trigger?: 'command' | 'icon'; + /** + * Whether user chose to execute this Python file in a separate terminal or not. + * + * @type {boolean} + */ + newTerminalPerFile?: boolean; }; /** * Telemetry Event sent when user executes code against Django Shell. diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index ed671f2846a2..9f1ba6e90d90 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -36,25 +36,31 @@ export class CodeExecutionManager implements ICodeExecutionManager { } public registerCommands() { - [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon].forEach((cmd) => { - this.disposableRegistry.push( - this.commandManager.registerCommand(cmd as any, async (file: Resource) => { - const interpreterService = this.serviceContainer.get(IInterpreterService); - const interpreter = await interpreterService.getActiveInterpreter(file); - if (!interpreter) { - this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); - return; - } - const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; - await this.executeFileInTerminal(file, trigger) - .then(() => { - if (this.shouldTerminalFocusOnStart(file)) - this.commandManager.executeCommand('workbench.action.terminal.focus'); + [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon, Commands.Exec_In_Separate_Terminal].forEach( + (cmd) => { + this.disposableRegistry.push( + this.commandManager.registerCommand(cmd as any, async (file: Resource) => { + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager + .executeCommand(Commands.TriggerEnvironmentSelection, file) + .then(noop, noop); + return; + } + const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; + await this.executeFileInTerminal(file, trigger, { + newTerminalPerFile: cmd === Commands.Exec_In_Separate_Terminal, }) - .catch((ex) => traceError('Failed to execute file in terminal', ex)); - }), - ); - }); + .then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }) + .catch((ex) => traceError('Failed to execute file in terminal', ex)); + }), + ); + }, + ); this.disposableRegistry.push( this.commandManager.registerCommand(Commands.Exec_Selection_In_Terminal as any, async (file: Resource) => { const interpreterService = this.serviceContainer.get(IInterpreterService); @@ -87,8 +93,16 @@ export class CodeExecutionManager implements ICodeExecutionManager { ), ); } - private async executeFileInTerminal(file: Resource, trigger: 'command' | 'icon') { - sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'file', trigger }); + private async executeFileInTerminal( + file: Resource, + trigger: 'command' | 'icon', + options?: { newTerminalPerFile: boolean }, + ): Promise { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { + scope: 'file', + trigger, + newTerminalPerFile: options?.newTerminalPerFile, + }); const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); file = file instanceof Uri ? file : undefined; let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); @@ -110,7 +124,7 @@ export class CodeExecutionManager implements ICodeExecutionManager { } const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); - await executionService.executeFile(fileToExecute); + await executionService.executeFile(fileToExecute, options); } @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index 9261483b45e1..4c329e939599 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -10,7 +10,7 @@ import { IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { IPlatformService } from '../../common/platform/types'; import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types'; -import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; import { IInterpreterService } from '../../interpreter/contracts'; import { buildPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/exec'; import { ICodeExecutionService } from '../../terminals/types'; @@ -19,7 +19,6 @@ import { ICodeExecutionService } from '../../terminals/types'; export class TerminalCodeExecutionProvider implements ICodeExecutionService { private hasRanOutsideCurrentDrive = false; protected terminalTitle!: string; - private _terminalService!: ITerminalService; private replActive?: Promise; constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @@ -30,13 +29,13 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { @inject(IInterpreterService) protected readonly interpreterService: IInterpreterService, ) {} - public async executeFile(file: Uri) { + public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) { await this.setCwdForFileExecution(file); const { command, args } = await this.getExecuteFileArgs(file, [ file.fsPath.fileToCommandArgumentForPythonExt(), ]); - await this.getTerminalService(file).sendCommand(command, args); + await this.getTerminalService(file, options).sendCommand(command, args); } public async execute(code: string, resource?: Uri): Promise { @@ -44,21 +43,27 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { return; } - await this.initializeRepl(); + await this.initializeRepl(resource); await this.getTerminalService(resource).sendText(code); } - public async initializeRepl(resource?: Uri) { + public async initializeRepl(resource: Resource) { + const terminalService = this.getTerminalService(resource); if (this.replActive && (await this.replActive)) { - await this._terminalService.show(); + await terminalService.show(); return; } this.replActive = new Promise(async (resolve) => { const replCommandArgs = await this.getExecutableInfo(resource); - await this.getTerminalService(resource).sendCommand(replCommandArgs.command, replCommandArgs.args); + terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); // Give python repl time to start before we start sending text. setTimeout(() => resolve(true), 1000); }); + this.disposables.push( + terminalService.onDidCloseTerminal(() => { + this.replActive = undefined; + }), + ); await this.replActive; } @@ -76,19 +81,12 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { return this.getExecutableInfo(resource, executeArgs); } - private getTerminalService(resource?: Uri): ITerminalService { - if (!this._terminalService) { - this._terminalService = this.terminalServiceFactory.getTerminalService({ - resource, - title: this.terminalTitle, - }); - this.disposables.push( - this._terminalService.onDidCloseTerminal(() => { - this.replActive = undefined; - }), - ); - } - return this._terminalService; + private getTerminalService(resource: Resource, options?: { newTerminalPerFile: boolean }): ITerminalService { + return this.terminalServiceFactory.getTerminalService({ + resource, + title: this.terminalTitle, + newTerminalPerFile: options?.newTerminalPerFile, + }); } private async setCwdForFileExecution(file: Uri) { const pythonSettings = this.configurationService.getSettings(file); diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index cf31f4ef1dd0..47ac16d9e08b 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -8,7 +8,7 @@ export const ICodeExecutionService = Symbol('ICodeExecutionService'); export interface ICodeExecutionService { execute(code: string, resource?: Uri): Promise; - executeFile(file: Uri): Promise; + executeFile(file: Uri, options?: { newTerminalPerFile: boolean }): Promise; initializeRepl(resource?: Uri): Promise; } diff --git a/src/test/common/terminals/factory.unit.test.ts b/src/test/common/terminals/factory.unit.test.ts index ef6b7d8f5b0f..5ad2da8e793a 100644 --- a/src/test/common/terminals/factory.unit.test.ts +++ b/src/test/common/terminals/factory.unit.test.ts @@ -105,7 +105,7 @@ suite('Terminal Service Factory', () => { expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same'); }); - test('Ensure same terminal is returned when using resources from the same workspace', () => { + test('Ensure same terminal is returned when using different resources from the same workspace', () => { const file1A = Uri.file('1a'); const file2A = Uri.file('2a'); const fileB = Uri.file('b'); @@ -140,4 +140,49 @@ suite('Terminal Service Factory', () => { 'Instances should be different for different workspaces', ); }); + + test('When `newTerminalPerFile` is true, ensure different terminal is returned when using different resources from the same workspace', () => { + const file1A = Uri.file('1a'); + const file2A = Uri.file('2a'); + const fileB = Uri.file('b'); + const workspaceUriA = Uri.file('A'); + const workspaceUriB = Uri.file('B'); + const workspaceFolderA = TypeMoq.Mock.ofType(); + workspaceFolderA.setup((w) => w.uri).returns(() => workspaceUriA); + const workspaceFolderB = TypeMoq.Mock.ofType(); + workspaceFolderB.setup((w) => w.uri).returns(() => workspaceUriB); + + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file1A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file2A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(fileB))) + .returns(() => workspaceFolderB.object); + + const terminalForFile1A = factory.getTerminalService({ + resource: file1A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFile2A = factory.getTerminalService({ + resource: file2A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFileB = factory.getTerminalService({ + resource: fileB, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + + const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; + expect(terminalsAreSameForWorkspaceA).to.equal(false, 'Instances are the same for Workspace A'); + + const terminalsForWorkspaceABAreDifferent = + terminalForFile1A.terminalService === terminalForFileB.terminalService; + expect(terminalsForWorkspaceABAreDifferent).to.equal( + false, + 'Instances should be different for different workspaces', + ); + }); }); diff --git a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 3676834873a0..30f95c94d217 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -77,12 +77,15 @@ suite('Terminal - Code Execution Manager', () => { executionManager.registerCommands(); const sorted = registered.sort(); - expect(sorted).to.deep.equal([ - Commands.Exec_In_Terminal, - Commands.Exec_In_Terminal_Icon, - Commands.Exec_Selection_In_Django_Shell, - Commands.Exec_Selection_In_Terminal, - ]); + expect(sorted).to.deep.equal( + [ + Commands.Exec_In_Separate_Terminal, + Commands.Exec_In_Terminal, + Commands.Exec_In_Terminal_Icon, + Commands.Exec_Selection_In_Django_Shell, + Commands.Exec_Selection_In_Terminal, + ].sort(), + ); }); test('Ensure executeFileInterTerminal will do nothing if no file is avialble', async () => { @@ -135,7 +138,10 @@ suite('Terminal - Code Execution Manager', () => { const fileToExecute = Uri.file('x'); await commandHandler!(fileToExecute); helper.verify(async (h) => h.getFileToExecute(), TypeMoq.Times.never()); - executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); test('Ensure executeFileInterTerminal will use active file', async () => { @@ -164,7 +170,10 @@ suite('Terminal - Code Execution Manager', () => { .returns(() => executionService.object); await commandHandler!(fileToExecute); - executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { diff --git a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts index 1f33b619fad0..9523f35a1040 100644 --- a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts +++ b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts @@ -25,7 +25,7 @@ import { TerminalCodeExecutionProvider } from '../../../client/terminals/codeExe import { ICodeExecutionService } from '../../../client/terminals/types'; import { PYTHON_PATH } from '../../common'; import * as sinon from 'sinon'; -import assert from 'assert'; +import { assert } from 'chai'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { IInterpreterService } from '../../../client/interpreter/contracts'; @@ -390,6 +390,7 @@ suite('Terminal - Code Execution', () => { const env = await createCondaEnv(condaEnv, procService.object, fileSystem.object); if (!env) { assert(false, 'Should not be undefined for conda version 4.9.0'); + return; } const procs = createPythonProcessService(procService.object, env); const condaExecutionService = { @@ -509,6 +510,7 @@ suite('Terminal - Code Execution', () => { const env = await createCondaEnv(condaEnv, procService.object, fileSystem.object); if (!env) { assert(false, 'Should not be undefined for conda version 4.9.0'); + return; } const procs = createPythonProcessService(procService.object, env); const condaExecutionService = { @@ -650,6 +652,31 @@ suite('Terminal - Code Execution', () => { await executor.execute('cmd2'); terminalService.verify(async (t) => t.sendText('cmd2'), TypeMoq.Times.once()); }); + + test('Ensure code is sent to the same terminal for a particular resource', async () => { + const resource = Uri.file('a'); + terminalFactory.reset(); + terminalFactory + .setup((f) => f.getTerminalService(TypeMoq.It.isAny())) + .callback((options: TerminalCreationOptions) => { + assert.deepEqual(options.resource, resource); + }) + .returns(() => terminalService.object); + + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + platform.setup((p) => p.isWindows).returns(() => false); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + + await executor.execute('cmd1', resource); + terminalService.verify(async (t) => t.sendText('cmd1'), TypeMoq.Times.once()); + + await executor.execute('cmd2', resource); + terminalService.verify(async (t) => t.sendText('cmd2'), TypeMoq.Times.once()); + }); }); }); }); From c25667861a963a17b3ba934c995a15944048bfeb Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 18 Jul 2023 17:26:39 -0700 Subject: [PATCH 180/336] Prevent posix paths locator from crashing (#21657) For https://github.com/microsoft/vscode-python/issues/21310 --- .../lowLevel/posixKnownPathsLocator.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts index 2e4e2dc13e61..4cacbd53f5aa 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts @@ -27,23 +27,27 @@ export class PosixKnownPathsLocator extends Locator { const iterator = async function* (kind: PythonEnvKind) { traceVerbose('Searching for interpreters in posix paths locator'); - // Filter out pyenv shims. They are not actual python binaries, they are used to launch - // the binaries specified in .python-version file in the cwd. We should not be reporting - // those binaries as environments. - const knownDirs = (await commonPosixBinPaths()).filter((dirname) => !isPyenvShimDir(dirname)); - let pythonBinaries = await getPythonBinFromPosixPaths(knownDirs); + try { + // Filter out pyenv shims. They are not actual python binaries, they are used to launch + // the binaries specified in .python-version file in the cwd. We should not be reporting + // those binaries as environments. + const knownDirs = (await commonPosixBinPaths()).filter((dirname) => !isPyenvShimDir(dirname)); + let pythonBinaries = await getPythonBinFromPosixPaths(knownDirs); - // Filter out MacOS system installs of Python 2 if necessary. - if (isMacPython2Deprecated) { - pythonBinaries = pythonBinaries.filter((binary) => !isMacDefaultPythonPath(binary)); - } + // Filter out MacOS system installs of Python 2 if necessary. + if (isMacPython2Deprecated) { + pythonBinaries = pythonBinaries.filter((binary) => !isMacDefaultPythonPath(binary)); + } - for (const bin of pythonBinaries) { - try { - yield { executablePath: bin, kind, source: [PythonEnvSource.PathEnvVar] }; - } catch (ex) { - traceError(`Failed to process environment: ${bin}`, ex); + for (const bin of pythonBinaries) { + try { + yield { executablePath: bin, kind, source: [PythonEnvSource.PathEnvVar] }; + } catch (ex) { + traceError(`Failed to process environment: ${bin}`, ex); + } } + } catch (ex) { + traceError('Failed to process posix paths', ex); } traceVerbose('Finished searching for interpreters in posix paths locator'); }; From be334bdb07199c614cbcf22d4a6d2fc84d53e3f8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 19 Jul 2023 14:16:23 -0700 Subject: [PATCH 181/336] Do not resolve symbolic links in posix locator if they exceed the count limit (#21658) Closes https://github.com/microsoft/vscode-python/issues/21310 Fixes interpreter discovery running forever for non-Windows OS --- .../base/locators/lowLevel/posixKnownPathsLocator.ts | 1 + .../pythonEnvironments/common/externalDependencies.ts | 11 ++++++++--- src/client/pythonEnvironments/common/posixUtils.ts | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts index 4cacbd53f5aa..2d7ebc2af111 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts @@ -33,6 +33,7 @@ export class PosixKnownPathsLocator extends Locator { // those binaries as environments. const knownDirs = (await commonPosixBinPaths()).filter((dirname) => !isPyenvShimDir(dirname)); let pythonBinaries = await getPythonBinFromPosixPaths(knownDirs); + traceVerbose(`Found ${pythonBinaries.length} python binaries in posix paths`); // Filter out MacOS system installs of Python 2 if necessary. if (isMacPython2Deprecated) { diff --git a/src/client/pythonEnvironments/common/externalDependencies.ts b/src/client/pythonEnvironments/common/externalDependencies.ts index 54f614ebdd49..ecb6f2212aba 100644 --- a/src/client/pythonEnvironments/common/externalDependencies.ts +++ b/src/client/pythonEnvironments/common/externalDependencies.ts @@ -10,7 +10,7 @@ import { IDisposable, IConfigurationService } from '../../common/types'; import { chain, iterable } from '../../common/utils/async'; import { getOSType, OSType } from '../../common/utils/platform'; import { IServiceContainer } from '../../ioc/types'; -import { traceVerbose } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; let internalServiceContainer: IServiceContainer; export function initializeExternalDependencies(serviceContainer: IServiceContainer): void { @@ -93,16 +93,21 @@ export function arePathsSame(path1: string, path2: string): boolean { return normCasePath(path1) === normCasePath(path2); } -export async function resolveSymbolicLink(absPath: string, stats?: fsapi.Stats): Promise { +export async function resolveSymbolicLink(absPath: string, stats?: fsapi.Stats, count?: number): Promise { stats = stats ?? (await fsapi.lstat(absPath)); if (stats.isSymbolicLink()) { + if (count && count > 5) { + traceError(`Detected a potential symbolic link loop at ${absPath}, terminating resolution.`); + return absPath; + } const link = await fsapi.readlink(absPath); // Result from readlink is not guaranteed to be an absolute path. For eg. on Mac it resolves // /usr/local/bin/python3.9 -> ../../../Library/Frameworks/Python.framework/Versions/3.9/bin/python3.9 // // The resultant path is reported relative to the symlink directory we resolve. Convert that to absolute path. const absLinkPath = path.isAbsolute(link) ? link : path.resolve(path.dirname(absPath), link); - return resolveSymbolicLink(absLinkPath); + count = count ? count + 1 : 1; + return resolveSymbolicLink(absLinkPath, undefined, count); } return absPath; } diff --git a/src/client/pythonEnvironments/common/posixUtils.ts b/src/client/pythonEnvironments/common/posixUtils.ts index cd8f62bf9a08..eb60fc029949 100644 --- a/src/client/pythonEnvironments/common/posixUtils.ts +++ b/src/client/pythonEnvironments/common/posixUtils.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import { uniq } from 'lodash'; import { getSearchPathEntries } from '../../common/utils/exec'; import { resolveSymbolicLink } from './externalDependencies'; -import { traceError, traceInfo } from '../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; /** * Determine if the given filename looks like the simplest Python executable. @@ -123,6 +123,7 @@ export async function getPythonBinFromPosixPaths(searchDirs: string[]): Promise< // Ensure that we have a collection of unique global binaries by // resolving all symlinks to the target binaries. try { + traceVerbose(`Attempting to resolve symbolic link: ${filepath}`); const resolvedBin = await resolveSymbolicLink(filepath); if (binToLinkMap.has(resolvedBin)) { binToLinkMap.get(resolvedBin)?.push(filepath); From 713007f035b77d2724f4a471b97f53db1a9a12e3 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 19 Jul 2023 14:45:18 -0700 Subject: [PATCH 182/336] correct discovery on unittest skip at file level (#21665) given a file called skip_test_file_node.py that has `raise SkipTest(".....")` this should appear in the sidebar with no children. The bug is that currently it shows a "unittest" node that gives "loader" and other incorrect nodes below it. --- .../.data/unittest_skip/unittest_skip_file.py | 10 +++ .../unittest_skip/unittest_skip_function.py | 18 +++++ .../expected_discovery_test_output.py | 68 +++++++++++++++++++ .../tests/unittestadapter/test_discovery.py | 21 +++++- pythonFiles/unittestadapter/utils.py | 8 +++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py create mode 100644 pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py create mode 100644 pythonFiles/tests/unittestadapter/expected_discovery_test_output.py diff --git a/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py b/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py new file mode 100644 index 000000000000..927a56bc920b --- /dev/null +++ b/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from unittest import SkipTest + +raise SkipTest("This is unittest.SkipTest calling") + + +def test_example(): + assert 1 == 1 diff --git a/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py b/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py new file mode 100644 index 000000000000..59e66e9a1d40 --- /dev/null +++ b/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +def add(x, y): + return x + y + + +class SimpleTest(unittest.TestCase): + @unittest.skip("demonstrating skipping") + def testadd1(self): + self.assertEquals(add(4, 5), 9) + + +if __name__ == "__main__": + unittest.main() diff --git a/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py b/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py new file mode 100644 index 000000000000..3043ec158a2e --- /dev/null +++ b/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +from unittestadapter.utils import TestNodeTypeEnum +from .helpers import TEST_DATA_PATH + +skip_unittest_folder_discovery_output = { + "path": os.fspath(TEST_DATA_PATH / "unittest_skip"), + "name": "unittest_skip", + "type_": TestNodeTypeEnum.folder, + "children": [ + { + "path": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py" + ), + "name": "unittest_skip_file.py", + "type_": TestNodeTypeEnum.file, + "children": [], + "id_": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py" + ), + }, + { + "path": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ), + "name": "unittest_skip_function.py", + "type_": TestNodeTypeEnum.file, + "children": [ + { + "path": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ), + "name": "SimpleTest", + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "testadd1", + "path": os.fspath( + TEST_DATA_PATH + / "unittest_skip" + / "unittest_skip_function.py" + ), + "lineno": "13", + "type_": TestNodeTypeEnum.test, + "id_": os.fspath( + TEST_DATA_PATH + / "unittest_skip" + / "unittest_skip_function.py" + ) + + "\\SimpleTest\\testadd1", + "runID": "unittest_skip_function.SimpleTest.testadd1", + } + ], + "id_": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ) + + "\\SimpleTest", + } + ], + "id_": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip"), +} diff --git a/pythonFiles/tests/unittestadapter/test_discovery.py b/pythonFiles/tests/unittestadapter/test_discovery.py index 28dc51f55dcd..c4778aa85852 100644 --- a/pythonFiles/tests/unittestadapter/test_discovery.py +++ b/pythonFiles/tests/unittestadapter/test_discovery.py @@ -12,7 +12,7 @@ parse_discovery_cli_args, ) from unittestadapter.utils import TestNodeTypeEnum, parse_unittest_args - +from . import expected_discovery_test_output from .helpers import TEST_DATA_PATH, is_same_tree @@ -214,3 +214,22 @@ def test_error_discovery() -> None: assert actual["status"] == "error" assert is_same_tree(expected, actual.get("tests")) assert len(actual.get("error", [])) == 1 + + +def test_unit_skip() -> None: + """The discover_tests function should return a dictionary with a "success" status, a uuid, no errors, and test tree. + if unittest discovery was performed and found a test in one file marked as skipped and another file marked as skipped. + """ + start_dir = os.fsdecode(TEST_DATA_PATH / "unittest_skip") + pattern = "unittest_*" + + uuid = "some-uuid" + actual = discover_tests(start_dir, pattern, None, uuid) + + assert actual["status"] == "success" + assert "tests" in actual + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.skip_unittest_folder_discovery_output, + ) + assert "error" not in actual diff --git a/pythonFiles/unittestadapter/utils.py b/pythonFiles/unittestadapter/utils.py index a461baf7d870..78000e2a945f 100644 --- a/pythonFiles/unittestadapter/utils.py +++ b/pythonFiles/unittestadapter/utils.py @@ -159,6 +159,14 @@ def build_test_tree( test_id = test_case.id() if test_id.startswith("unittest.loader._FailedTest"): error.append(str(test_case._exception)) # type: ignore + elif test_id.startswith("unittest.loader.ModuleSkipped"): + components = test_id.split(".") + class_name = f"{components[-1]}.py" + # Find/build class node. + file_path = os.fsdecode(os.path.join(directory_path, class_name)) + current_node = get_child_node( + class_name, file_path, TestNodeTypeEnum.file, root + ) else: # Get the static test path components: filename, class name and function name. components = test_id.split(".") From 9bcb82d65d19e51e6d057937a037b23b8feab7f5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 24 Jul 2023 07:54:51 -0700 Subject: [PATCH 183/336] Ensure `Run Python in dedicated terminal` uses `python.executeInFirDir` setting (#21681) --- .../base/locators/lowLevel/windowsKnownPathsLocator.ts | 6 ++++-- .../terminals/codeExecution/terminalCodeExecution.ts | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts index 337a8fb09a97..5bfc62d99d48 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts @@ -94,8 +94,10 @@ function getDirFilesLocator( // take a naive approach. async function* iterEnvs(query: PythonLocatorQuery): IPythonEnvsIterator { traceVerbose('Searching for windows path interpreters'); - yield* await getEnvs(locator.iterEnvs(query)); - traceVerbose('Finished searching for windows path interpreters'); + yield* await getEnvs(locator.iterEnvs(query)).then((res) => { + traceVerbose('Finished searching for windows path interpreters'); + return res; + }); } return { providerId: locator.providerId, diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index 4c329e939599..50270c3586c4 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -30,7 +30,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { ) {} public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) { - await this.setCwdForFileExecution(file); + await this.setCwdForFileExecution(file, options); const { command, args } = await this.getExecuteFileArgs(file, [ file.fsPath.fileToCommandArgumentForPythonExt(), ]); @@ -88,7 +88,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { newTerminalPerFile: options?.newTerminalPerFile, }); } - private async setCwdForFileExecution(file: Uri) { + private async setCwdForFileExecution(file: Uri, options?: { newTerminalPerFile: boolean }) { const pythonSettings = this.configurationService.getSettings(file); if (!pythonSettings.terminal.executeInFileDir) { return; @@ -106,7 +106,9 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { await this.getTerminalService(file).sendText(`${fileDrive}:`); } } - await this.getTerminalService(file).sendText(`cd ${fileDirPath.fileToCommandArgumentForPythonExt()}`); + await this.getTerminalService(file, options).sendText( + `cd ${fileDirPath.fileToCommandArgumentForPythonExt()}`, + ); } } } From 73a0e9ddd853202614c48ae605b5f8f65e28bc43 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 24 Jul 2023 09:54:01 -0700 Subject: [PATCH 184/336] handle skip unittest at file without error (#21678) fixes https://github.com/microsoft/vscode-python/issues/21653 --- .../.data/unittest_skiptest_file_level.py | 13 +++++++++++++ .../pytestadapter/expected_discovery_test_output.py | 10 ++++++++++ pythonFiles/tests/pytestadapter/test_discovery.py | 4 ++++ pythonFiles/vscode_pytest/__init__.py | 2 ++ 4 files changed, 29 insertions(+) create mode 100644 pythonFiles/tests/pytestadapter/.data/unittest_skiptest_file_level.py diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_skiptest_file_level.py b/pythonFiles/tests/pytestadapter/.data/unittest_skiptest_file_level.py new file mode 100644 index 000000000000..362c74cbb76f --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/unittest_skiptest_file_level.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +from unittest import SkipTest + +# Due to the skip at the file level, no tests will be discovered. +raise SkipTest("Skip all tests in this file, they should not be recognized by pytest.") + + +class SimpleTest(unittest.TestCase): + def testadd1(self): + assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index fb8234350fb4..91c1453dfc77 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -100,6 +100,16 @@ "id_": TEST_DATA_PATH_STR, } +# This is the expected output for the unittest_skip_file_level test. +# └── unittest_skiptest_file_level.py +unittest_skip_file_level_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [], + "id_": TEST_DATA_PATH_STR, +} + # This is the expected output for the unittest_folder tests # └── unittest_folder # ├── test_add.py diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 02ea1ddcd871..5288c7ad769e 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -87,6 +87,10 @@ def test_parameterized_error_collect(): @pytest.mark.parametrize( "file, expected_const", [ + ( + "unittest_skiptest_file_level.py", + expected_discovery_test_output.unittest_skip_file_level_expected_output, + ), ( "param_same_name", expected_discovery_test_output.param_same_name_expected_output, diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 072c5ef5d3ad..1ac287a8410a 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -73,6 +73,8 @@ def pytest_exception_interact(node, call, report): # if discovery, then add the error to error logs. if type(report) == pytest.CollectReport: if call.excinfo and call.excinfo.typename != "AssertionError": + if report.outcome == "skipped" and "SkipTest" in str(call): + return ERRORS.append( call.excinfo.exconly() + "\n Check Python Test Logs for more details." ) From 6af959d34870a43e27b2bc3059239206b8851016 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 24 Jul 2023 15:56:06 -0700 Subject: [PATCH 185/336] Dev Container Using MCR (#21675) Dev container rewrite using MCR. Pyenv for installing and managing python versions. Fish also installed as optional (able to view as shell option in codespaces). Also fixing conda error. Takes care of: #21591 rewrite from: #21435 to adhere to company policy. --- .devcontainer/Dockerfile | 32 +++++++---------- .devcontainer/devcontainer.json | 3 +- scripts/onCreateCommand.sh | 36 +++++++++++++++++++ scripts/postCreateCommand.sh | 15 -------- src/client/common/interpreterPathService.ts | 2 +- .../interpreterPathService.unit.test.ts | 9 +++-- .../common/commonUtils.functional.test.ts | 4 --- 7 files changed, 58 insertions(+), 43 deletions(-) create mode 100644 scripts/onCreateCommand.sh delete mode 100644 scripts/postCreateCommand.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f5f49445b399..5fbf068de65f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,26 +1,18 @@ -# This image will serve as a starting point for devcontainer.json. -# Get latest image of Fedora as the base image. -FROM docker.io/library/fedora:latest +FROM mcr.microsoft.com/devcontainers/typescript-node:16-bookworm -# Install supported python versions and nodejs. -RUN dnf -y --nodocs install /usr/bin/{python3.7,python3.8,python3.9,python3.10,python3.11,git,conda,clang} && \ - dnf clean all +RUN apt-get install -y wget bzip2 -ENV NVM_VERSION=0.39.3 -ENV NODE_VERSION=16.17.1 -ENV NPM_VERSION=8.19.3 - -# Installation instructions from https://github.com/nvm-sh/nvm . -RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v$NVM_VERSION/install.sh | bash -RUN export NVM_DIR="$HOME/.nvm" && \ - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ - [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" && \ - nvm install $NODE_VERSION && \ - npm install -g npm@$NPM_VERSION - -# For clean open source builds. -ENV DISABLE_TRANSLATIONS=true +# Run in silent mode and save downloaded script as anaconda.sh. +# Run with /bin/bash and run in silent mode to /opt/conda. +# Also get rid of installation script after finishing. +RUN wget --quiet https://repo.anaconda.com/archive/Anaconda3-2023.07-1-Linux-x86_64.sh -O ~/anaconda.sh && \ + /bin/bash ~/anaconda.sh -b -p /opt/conda && \ + rm ~/anaconda.sh +ENV PATH="/opt/conda/bin:$PATH" +# Sudo apt update needs to run in order for installation of fish to work . +RUN sudo apt update && \ + sudo apt install fish -y diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6435ba5bbda8..fe15f35764e6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,7 +21,8 @@ }, // Commands to execute on container creation,start. "postCreateCommand": "bash scripts/postCreateCommand.sh", - // Environment variable placed inside containerEnv following: https://containers.dev/implementors/json_reference/#general-properties + "onCreateCommand": "bash scripts/onCreateCommand.sh", + "containerEnv": { "CI_PYTHON_PATH": "/workspaces/vscode-python/.venv/bin/python" } diff --git a/scripts/onCreateCommand.sh b/scripts/onCreateCommand.sh new file mode 100644 index 000000000000..e93c74f610b9 --- /dev/null +++ b/scripts/onCreateCommand.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Install pyenv and Python versions here to avoid using shim. +curl https://pyenv.run | bash +echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc +echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc +# echo 'eval "$(pyenv init -)"' >> ~/.bashrc + +export PYENV_ROOT="$HOME/.pyenv" +command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" +# eval "$(pyenv init -)" Comment this out and DO NOT use shim. +source ~/.bashrc + +# Install Python via pyenv . +pyenv install 3.7:latest 3.8:latest 3.9:latest 3.10:latest 3.11:latest + +# Set default Python version to 3.7 . +pyenv global 3.7.17 + +npm ci + +# Create Virutal environment. +pyenv exec python3.7 -m venv .venv + +# Activate Virtual environment. +source /workspaces/vscode-python/.venv/bin/activate + +# Install required Python libraries. +npx gulp installPythonLibs + +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/test-requirements.txt +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/smoke-test-requirements.txt +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/functional-test-requirements.txt + +# Below will crash codespace +# npm run compile diff --git a/scripts/postCreateCommand.sh b/scripts/postCreateCommand.sh deleted file mode 100644 index 85462caf7fad..000000000000 --- a/scripts/postCreateCommand.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -npm ci -# Create Virutal environment. -python3.7 -m venv /workspaces/vscode-python/.venv - -# Activate Virtual environment. -source /workspaces/vscode-python/.venv/bin/activate - -# Install required Python libraries. -npx gulp installPythonLibs - -# Install testing requirement using python in .venv . -/workspaces/vscode-python/.venv/bin/python -m pip install -r build/test-requirements.txt -/workspaces/vscode-python/.venv/bin/python -m pip install -r build/smoke-test-requirements.txt -/workspaces/vscode-python/.venv/bin/python -m pip install -r build/functional-test-requirements.txt diff --git a/src/client/common/interpreterPathService.ts b/src/client/common/interpreterPathService.ts index 9eea1548977c..8af142962565 100644 --- a/src/client/common/interpreterPathService.ts +++ b/src/client/common/interpreterPathService.ts @@ -30,7 +30,7 @@ export const isRemoteGlobalSettingCopiedKey = 'isRemoteGlobalSettingCopiedKey'; export const defaultInterpreterPathSetting: keyof IPythonSettings = 'defaultInterpreterPath'; const CI_PYTHON_PATH = getCIPythonPath(); -function getCIPythonPath(): string { +export function getCIPythonPath(): string { if (process.env.CI_PYTHON_PATH && fs.existsSync(process.env.CI_PYTHON_PATH)) { return process.env.CI_PYTHON_PATH; } diff --git a/src/test/common/interpreterPathService.unit.test.ts b/src/test/common/interpreterPathService.unit.test.ts index 6ba63d9d663d..58a34b3cbcde 100644 --- a/src/test/common/interpreterPathService.unit.test.ts +++ b/src/test/common/interpreterPathService.unit.test.ts @@ -15,7 +15,11 @@ import { WorkspaceConfiguration, } from 'vscode'; import { IApplicationEnvironment, IWorkspaceService } from '../../client/common/application/types'; -import { defaultInterpreterPathSetting, InterpreterPathService } from '../../client/common/interpreterPathService'; +import { + defaultInterpreterPathSetting, + getCIPythonPath, + InterpreterPathService, +} from '../../client/common/interpreterPathService'; import { FileSystemPaths } from '../../client/common/platform/fs-paths'; import { InterpreterConfigurationScope, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; import { createDeferred, sleep } from '../../client/common/utils/async'; @@ -447,7 +451,8 @@ suite('Interpreter Path Service', async () => { workspaceValue: undefined, }); const settingValue = interpreterPathService.get(resource); - expect(settingValue).to.equal('python'); + + expect(settingValue).to.equal(getCIPythonPath()); }); test('If defaultInterpreterPathSetting is changed, an event is fired', async () => { diff --git a/src/test/pythonEnvironments/common/commonUtils.functional.test.ts b/src/test/pythonEnvironments/common/commonUtils.functional.test.ts index e0c1f755e2c8..647a17a40a90 100644 --- a/src/test/pythonEnvironments/common/commonUtils.functional.test.ts +++ b/src/test/pythonEnvironments/common/commonUtils.functional.test.ts @@ -86,7 +86,6 @@ suite('pyenvs common utils - finding Python executables', () => { python3 -> sub2/sub2.2/python3 python3.7 -> sub2/sub2.1/sub2.1.1/python - python2.7 -> does-not-exist `); } }); @@ -106,7 +105,6 @@ suite('pyenvs common utils - finding Python executables', () => { // These will match. 'python', 'python2', - 'python2.7', 'python3', 'python3.7', 'python3.8', @@ -137,7 +135,6 @@ suite('pyenvs common utils - finding Python executables', () => { // These will match. 'python', 'python2', - 'python2.7', 'python3', 'python3.7', 'python3.8', @@ -167,7 +164,6 @@ suite('pyenvs common utils - finding Python executables', () => { // These will match. 'python', 'python2', - 'python2.7', 'python3', 'python3.7', 'python3.8', From a42cb33cea73a6e276f2bc2ad635f62fc2c1e6a2 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 24 Jul 2023 15:59:12 -0700 Subject: [PATCH 186/336] Add new telemetry property to GPDR (#21683) This property was added for tracking diagnostics we emit in Pylance. --- src/client/telemetry/pylance.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 905cacc5fbf2..c59d279b98c2 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -50,7 +50,8 @@ "numfilesinprogram" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "resolverid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "rssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + "rssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "diagnosticsseen" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ /* __GDPR__ From f536b744a757bd8fa8f0668c58b7906b518d98b4 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 24 Jul 2023 18:05:39 -0700 Subject: [PATCH 187/336] Edit issue-labels.yml, triage-info-needed.yml (#21685) Add Anthony to issue-labels.yml and triage-info-needed.yml --- .github/workflows/issue-labels.yml | 2 +- .github/workflows/triage-info-needed.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index ec2c5eb002fd..e942a4c965ec 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -7,7 +7,7 @@ on: env: # To update the list of labels, see `getLabels.js`. REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' - TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd"]' + TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd","anthonykim1"]' permissions: issues: write diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index 1c384d824da5..c717d7ec94b3 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -5,7 +5,7 @@ on: types: [created] env: - TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd", "brettcannon"]' + TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd", "brettcannon","anthonykim1"]' jobs: add_label: From 8b9bca1fd5a684d454e0d97583642cb46553241e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 25 Jul 2023 12:21:08 -0700 Subject: [PATCH 188/336] Do not show "Select at workspace level" option if only one workspace folder is opened (#21689) Closes https://github.com/microsoft/vscode-python/issues/21220 --- .../configuration/interpreterSelector/commands/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/base.ts b/src/client/interpreter/configuration/interpreterSelector/commands/base.ts index 6ed2dee36c89..6307e286dbfe 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/base.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/base.ts @@ -53,7 +53,7 @@ export abstract class BaseInterpreterSelectorCommand implements IExtensionSingle }, ]; } - if (!this.workspaceService.workspaceFile && workspaceFolders.length === 1) { + if (workspaceFolders.length === 1) { return [ { folderUri: workspaceFolders[0].uri, From d6730049ca5bf08aa9183266cdbbb4ce0d638ac2 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 25 Jul 2023 13:57:06 -0700 Subject: [PATCH 189/336] Convert JS-style typings to native TS in `@vscode/python-extension` (#21692) Closes https://github.com/microsoft/vscode-python/issues/21690 --- pythonExtensionApi/src/main.ts | 14 +++++--------- src/client/api/types.ts | 34 ++++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/pythonExtensionApi/src/main.ts b/pythonExtensionApi/src/main.ts index b980a06b72f8..cf1461f04d81 100644 --- a/pythonExtensionApi/src/main.ts +++ b/pythonExtensionApi/src/main.ts @@ -10,7 +10,6 @@ import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem, extensio export interface PythonExtension { /** * Promise indicating whether all parts of the extension have completed loading or not. - * @type {Promise} */ ready: Promise; jupyter: { @@ -21,10 +20,9 @@ export interface PythonExtension { * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. * Users can append another array of strings of what they want to execute along with relevant arguments to Python. * E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` - * @param {string} host - * @param {number} port - * @param {boolean} [waitUntilDebuggerAttaches=true] - * @returns {Promise} + * @param host + * @param port + * @param waitUntilDebuggerAttaches Defaults to `true`. */ getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; @@ -38,8 +36,8 @@ export interface PythonExtension { datascience: { /** * Launches Data Viewer component. - * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param {string} title Data Viewer title + * @param dataProvider Instance that will be used by the Data Viewer component to fetch data. + * @param title Data Viewer title */ showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; /** @@ -316,7 +314,6 @@ export type EnvironmentPath = { * was contributed. */ export type EnvironmentTools = KnownEnvironmentTools | string; - /** * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink * once tools have their own separate extensions. @@ -335,7 +332,6 @@ export type KnownEnvironmentTools = * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. */ export type EnvironmentType = KnownEnvironmentTypes | string; - /** * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their * own separate extensions, in which case they're expected to provide the type themselves. diff --git a/src/client/api/types.ts b/src/client/api/types.ts index 4e13ec4853ec..cf1461f04d81 100644 --- a/src/client/api/types.ts +++ b/src/client/api/types.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem } from 'vscode'; +import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem, extensions } from 'vscode'; /* * Do not introduce any breaking changes to this API. @@ -10,7 +10,6 @@ import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem } from 'v export interface PythonExtension { /** * Promise indicating whether all parts of the extension have completed loading or not. - * @type {Promise} */ ready: Promise; jupyter: { @@ -21,10 +20,9 @@ export interface PythonExtension { * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. * Users can append another array of strings of what they want to execute along with relevant arguments to Python. * E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` - * @param {string} host - * @param {number} port - * @param {boolean} [waitUntilDebuggerAttaches=true] - * @returns {Promise} + * @param host + * @param port + * @param waitUntilDebuggerAttaches Defaults to `true`. */ getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; @@ -38,8 +36,8 @@ export interface PythonExtension { datascience: { /** * Launches Data Viewer component. - * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param {string} title Data Viewer title + * @param dataProvider Instance that will be used by the Data Viewer component to fetch data. + * @param title Data Viewer title */ showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; /** @@ -387,3 +385,23 @@ export type EnvironmentVariablesChangeEvent = { */ readonly env: EnvironmentVariables; }; + +export const PVSC_EXTENSION_ID = 'ms-python.python'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PythonExtension { + /** + * Returns the API exposed by the Python extension in VS Code. + */ + export async function api(): Promise { + const extension = extensions.getExtension(PVSC_EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: PythonExtension = extension.exports; + return pythonApi; + } +} From 83107cc253508a329de2bc2f084bb6f4294a4289 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 27 Jul 2023 13:48:23 -0700 Subject: [PATCH 190/336] Move "vscode" out of required dependencies for npm package (#21701) Closes https://github.com/microsoft/vscode-python/issues/21684 --- pythonExtensionApi/package-lock.json | 17 ++++++++++------- pythonExtensionApi/package.json | 7 ++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index a8abd6d85bfc..a03c33ab5c0d 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -1,28 +1,30 @@ { "name": "@vscode/python-extension", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", - "dependencies": { - "@types/vscode": "^1.78.0" - }, "devDependencies": { + "@types/vscode": "^1.78.0", "typescript": "5.0.4" }, "engines": { "node": ">=16.17.1", "vscode": "^1.78.0" + }, + "peerDependencies": { + "@types/vscode": "^1.78.0" } }, "node_modules/@types/vscode": { "version": "1.80.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", - "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", + "dev": true }, "node_modules/typescript": { "version": "5.0.4", @@ -42,7 +44,8 @@ "@types/vscode": { "version": "1.80.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", - "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", + "dev": true }, "typescript": { "version": "5.0.4", diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index aaeaaf54a0c4..b19a64dd7d66 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Microsoft Corporation" }, @@ -25,11 +25,12 @@ "bugs": { "url": "https://github.com/Microsoft/vscode-python/issues" }, - "dependencies": { + "peerDependencies": { "@types/vscode": "^1.78.0" }, "devDependencies": { - "typescript": "5.0.4" + "typescript": "5.0.4", + "@types/vscode": "^1.78.0" }, "scripts": { "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", From 06d62aa04557d51576633014bcca6b5d7cae50ca Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 27 Jul 2023 21:11:28 -0700 Subject: [PATCH 191/336] Update homepage for Python API package (#21703) For #21631 --- pythonExtensionApi/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index b19a64dd7d66..dd0e921f8abe 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -17,7 +17,7 @@ "vscode": "^1.78.0" }, "license": "MIT", - "homepage": "https://github.com/Microsoft/vscode-python", + "homepage": "https://github.com/microsoft/vscode-python/tree/main/pythonExtensionApi", "repository": { "type": "git", "url": "https://github.com/Microsoft/vscode-python" From efcc3d7382c4a7f6ce930372dcd05a7c5358dc83 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 28 Jul 2023 09:52:29 -0700 Subject: [PATCH 192/336] Make test_ids relative to workspace path not root dir (#21682) makes sure all testIds that are returned to the extension are relative to the workspace (which will be the invocation directory) instead of to the root. This will stop testIds for not being recognized when using a config file or another parameter that changes the root directory during pytest. fixes https://github.com/microsoft/vscode-python/issues/21640 and https://github.com/microsoft/vscode-python/issues/21637 --- .../pytestadapter/.data/root/tests/pytest.ini | 0 .../pytestadapter/.data/root/tests/test_a.py | 6 + .../pytestadapter/.data/root/tests/test_b.py | 6 + .../expected_discovery_test_output.py | 516 +++++++++++++----- .../expected_execution_test_output.py | 362 +++++++++--- pythonFiles/tests/pytestadapter/helpers.py | 16 +- .../tests/pytestadapter/test_discovery.py | 52 +- .../tests/pytestadapter/test_execution.py | 46 +- pythonFiles/vscode_pytest/__init__.py | 58 +- 9 files changed, 812 insertions(+), 250 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini b/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py new file mode 100644 index 000000000000..3ec3dd9626cb --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_a_function(): # test_marker--test_a_function + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py new file mode 100644 index 000000000000..0d3148641f85 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_b_function(): # test_marker--test_b_function + assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 91c1453dfc77..2b2c07ab8ea7 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -1,6 +1,7 @@ import os -from .helpers import TEST_DATA_PATH, find_test_line_number + +from .helpers import TEST_DATA_PATH, find_test_line_number, get_absolute_test_id # This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. @@ -18,7 +19,7 @@ # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function -simple_test_file_path = os.fspath(TEST_DATA_PATH / "simple_pytest.py") +simple_test_file_path = TEST_DATA_PATH / "simple_pytest.py" simple_discovery_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -26,20 +27,24 @@ "children": [ { "name": "simple_pytest.py", - "path": simple_test_file_path, + "path": os.fspath(simple_test_file_path), "type_": "file", - "id_": simple_test_file_path, + "id_": os.fspath(simple_test_file_path), "children": [ { "name": "test_function", - "path": simple_test_file_path, + "path": os.fspath(simple_test_file_path), "lineno": find_test_line_number( "test_function", simple_test_file_path, ), "type_": "test", - "id_": "simple_pytest.py::test_function", - "runID": "simple_pytest.py::test_function", + "id_": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), + "runID": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), } ], } @@ -52,7 +57,7 @@ # ├── TestExample # │ └── test_true_unittest # └── test_true_pytest -unit_pytest_same_file_path = os.fspath(TEST_DATA_PATH / "unittest_pytest_same_file.py") +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" unit_pytest_same_file_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -60,39 +65,51 @@ "children": [ { "name": "unittest_pytest_same_file.py", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "type_": "file", - "id_": unit_pytest_same_file_path, + "id_": os.fspath(unit_pytest_same_file_path), "children": [ { "name": "TestExample", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "type_": "class", "children": [ { "name": "test_true_unittest", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "lineno": find_test_line_number( "test_true_unittest", - unit_pytest_same_file_path, + os.fspath(unit_pytest_same_file_path), ), "type_": "test", - "id_": "unittest_pytest_same_file.py::TestExample::test_true_unittest", - "runID": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), } ], "id_": "unittest_pytest_same_file.py::TestExample", }, { "name": "test_true_pytest", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "lineno": find_test_line_number( "test_true_pytest", unit_pytest_same_file_path, ), "type_": "test", - "id_": "unittest_pytest_same_file.py::test_true_pytest", - "runID": "unittest_pytest_same_file.py::test_true_pytest", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), }, ], } @@ -124,9 +141,9 @@ # └── test_subtract_positive_numbers # │ └── TestDuplicateFunction # │ └── test_dup_s -unittest_folder_path = os.fspath(TEST_DATA_PATH / "unittest_folder") -test_add_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_add.py") -test_subtract_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_subtract.py") +unittest_folder_path = TEST_DATA_PATH / "unittest_folder" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" unittest_folder_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -134,61 +151,79 @@ "children": [ { "name": "unittest_folder", - "path": unittest_folder_path, + "path": os.fspath(unittest_folder_path), "type_": "folder", - "id_": unittest_folder_path, + "id_": os.fspath(unittest_folder_path), "children": [ { "name": "test_add.py", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "file", - "id_": test_add_path, + "id_": os.fspath(test_add_path), "children": [ { "name": "TestAddFunction", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "class", "children": [ { "name": "test_add_negative_numbers", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_add_negative_numbers", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", - "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), }, { "name": "test_add_positive_numbers", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_add_positive_numbers", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), }, ], "id_": "unittest_folder/test_add.py::TestAddFunction", }, { "name": "TestDuplicateFunction", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "class", "children": [ { "name": "test_dup_a", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_dup_a", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", - "runID": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), }, ], "id_": "unittest_folder/test_add.py::TestDuplicateFunction", @@ -197,55 +232,73 @@ }, { "name": "test_subtract.py", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "file", - "id_": test_subtract_path, + "id_": os.fspath(test_subtract_path), "children": [ { "name": "TestSubtractFunction", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "class", "children": [ { "name": "test_subtract_negative_numbers", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_subtract_negative_numbers", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", - "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), }, { "name": "test_subtract_positive_numbers", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_subtract_positive_numbers", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", - "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), }, ], "id_": "unittest_folder/test_subtract.py::TestSubtractFunction", }, { "name": "TestDuplicateFunction", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "class", "children": [ { "name": "test_dup_s", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_dup_s", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", - "runID": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), }, ], "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction", @@ -268,20 +321,23 @@ # └── test_bottom_folder.py # └── test_bottom_function_t # └── test_bottom_function_f -dual_level_nested_folder_path = os.fspath(TEST_DATA_PATH / "dual_level_nested_folder") -test_top_folder_path = os.fspath( +dual_level_nested_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" +test_top_folder_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" ) -test_nested_folder_one_path = os.fspath( + +test_nested_folder_one_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" ) -test_bottom_folder_path = os.fspath( + +test_bottom_folder_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" ) + dual_level_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -289,73 +345,97 @@ "children": [ { "name": "dual_level_nested_folder", - "path": dual_level_nested_folder_path, + "path": os.fspath(dual_level_nested_folder_path), "type_": "folder", - "id_": dual_level_nested_folder_path, + "id_": os.fspath(dual_level_nested_folder_path), "children": [ { "name": "test_top_folder.py", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "type_": "file", - "id_": test_top_folder_path, + "id_": os.fspath(test_top_folder_path), "children": [ { "name": "test_top_function_t", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "lineno": find_test_line_number( "test_top_function_t", test_top_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), }, { "name": "test_top_function_f", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "lineno": find_test_line_number( "test_top_function_f", test_top_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), }, ], }, { "name": "nested_folder_one", - "path": test_nested_folder_one_path, + "path": os.fspath(test_nested_folder_one_path), "type_": "folder", - "id_": test_nested_folder_one_path, + "id_": os.fspath(test_nested_folder_one_path), "children": [ { "name": "test_bottom_folder.py", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "type_": "file", - "id_": test_bottom_folder_path, + "id_": os.fspath(test_bottom_folder_path), "children": [ { "name": "test_bottom_function_t", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "lineno": find_test_line_number( "test_bottom_function_t", test_bottom_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), }, { "name": "test_bottom_function_f", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "lineno": find_test_line_number( "test_bottom_function_f", test_bottom_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), }, ], } @@ -374,12 +454,10 @@ # └── test_nest.py # └── test_function -folder_a_path = os.fspath(TEST_DATA_PATH / "folder_a") -folder_b_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b") -folder_a_nested_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a") -test_nest_path = os.fspath( - TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" -) +folder_a_path = TEST_DATA_PATH / "folder_a" +folder_b_path = TEST_DATA_PATH / "folder_a" / "folder_b" +folder_a_nested_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" +test_nest_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" double_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -387,38 +465,44 @@ "children": [ { "name": "folder_a", - "path": folder_a_path, + "path": os.fspath(folder_a_path), "type_": "folder", - "id_": folder_a_path, + "id_": os.fspath(folder_a_path), "children": [ { "name": "folder_b", - "path": folder_b_path, + "path": os.fspath(folder_b_path), "type_": "folder", - "id_": folder_b_path, + "id_": os.fspath(folder_b_path), "children": [ { "name": "folder_a", - "path": folder_a_nested_path, + "path": os.fspath(folder_a_nested_path), "type_": "folder", - "id_": folder_a_nested_path, + "id_": os.fspath(folder_a_nested_path), "children": [ { "name": "test_nest.py", - "path": test_nest_path, + "path": os.fspath(test_nest_path), "type_": "file", - "id_": test_nest_path, + "id_": os.fspath(test_nest_path), "children": [ { "name": "test_function", - "path": test_nest_path, + "path": os.fspath(test_nest_path), "lineno": find_test_line_number( "test_function", test_nest_path, ), "type_": "test", - "id_": "folder_a/folder_b/folder_a/test_nest.py::test_function", - "runID": "folder_a/folder_b/folder_a/test_nest.py::test_function", + "id_": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), + "runID": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), } ], } @@ -438,7 +522,7 @@ # └── [3+5-8] # └── [2+4-6] # └── [6+9-16] -parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") +parameterize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" parametrize_tests_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -446,77 +530,107 @@ "children": [ { "name": "parametrize_tests.py", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "file", - "id_": parameterize_tests_path, + "id_": os.fspath(parameterize_tests_path), "children": [ { "name": "test_adding", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "function", "id_": "parametrize_tests.py::test_adding", "children": [ { "name": "[3+5-8]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[3+5-8]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[3+5-8]", - "runID": "parametrize_tests.py::test_adding[3+5-8]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", + parameterize_tests_path, + ), }, { "name": "[2+4-6]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[2+4-6]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[2+4-6]", - "runID": "parametrize_tests.py::test_adding[2+4-6]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", + parameterize_tests_path, + ), }, { "name": "[6+9-16]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[6+9-16]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[6+9-16]", - "runID": "parametrize_tests.py::test_adding[6+9-16]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", + parameterize_tests_path, + ), }, ], }, { "name": "test_under_ten", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "function", "children": [ { "name": "[1]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_under_ten[1]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_under_ten[1]", - "runID": "parametrize_tests.py::test_under_ten[1]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[1]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[1]", + parameterize_tests_path, + ), }, { "name": "[2]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_under_ten[2]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_under_ten[2]", - "runID": "parametrize_tests.py::test_under_ten[2]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[2]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[2]", + parameterize_tests_path, + ), }, ], "id_": "parametrize_tests.py::test_under_ten", @@ -529,7 +643,7 @@ # This is the expected output for the text_docstring.txt tests. # └── text_docstring.txt -text_docstring_path = os.fspath(TEST_DATA_PATH / "text_docstring.txt") +text_docstring_path = TEST_DATA_PATH / "text_docstring.txt" doctest_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -537,20 +651,24 @@ "children": [ { "name": "text_docstring.txt", - "path": text_docstring_path, + "path": os.fspath(text_docstring_path), "type_": "file", - "id_": text_docstring_path, + "id_": os.fspath(text_docstring_path), "children": [ { "name": "text_docstring.txt", - "path": text_docstring_path, + "path": os.fspath(text_docstring_path), "lineno": find_test_line_number( "text_docstring.txt", - text_docstring_path, + os.fspath(text_docstring_path), ), "type_": "test", - "id_": "text_docstring.txt::text_docstring.txt", - "runID": "text_docstring.txt::text_docstring.txt", + "id_": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), + "runID": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), } ], } @@ -570,8 +688,8 @@ # └── [1] # └── [2] # └── [3] -param1_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param1.py") -param2_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param2.py") +param1_path = TEST_DATA_PATH / "param_same_name" / "test_param1.py" +param2_path = TEST_DATA_PATH / "param_same_name" / "test_param2.py" param_same_name_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -585,38 +703,56 @@ "children": [ { "name": "test_param1.py", - "path": param1_path, + "path": os.fspath(param1_path), "type_": "file", - "id_": param1_path, + "id_": os.fspath(param1_path), "children": [ { "name": "test_odd_even", - "path": param1_path, + "path": os.fspath(param1_path), "type_": "function", "children": [ { "name": "[a]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[a]", - "runID": "param_same_name/test_param1.py::test_odd_even[a]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), }, { "name": "[b]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[b]", - "runID": "param_same_name/test_param1.py::test_odd_even[b]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), }, { "name": "[c]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[c]", - "runID": "param_same_name/test_param1.py::test_odd_even[c]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), }, ], "id_": "param_same_name/test_param1.py::test_odd_even", @@ -625,38 +761,56 @@ }, { "name": "test_param2.py", - "path": param2_path, + "path": os.fspath(param2_path), "type_": "file", - "id_": param2_path, + "id_": os.fspath(param2_path), "children": [ { "name": "test_odd_even", - "path": param2_path, + "path": os.fspath(param2_path), "type_": "function", "children": [ { "name": "[1]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[1]", - "runID": "param_same_name/test_param2.py::test_odd_even[1]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), }, { "name": "[2]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[2]", - "runID": "param_same_name/test_param2.py::test_odd_even[2]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), }, { "name": "[3]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[3]", - "runID": "param_same_name/test_param2.py::test_odd_even[3]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), }, ], "id_": "param_same_name/test_param2.py::test_odd_even", @@ -668,3 +822,67 @@ ], "id_": TEST_DATA_PATH_STR, } + +tests_path = TEST_DATA_PATH / "root" / "tests" +tests_a_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +tests_b_path = TEST_DATA_PATH / "root" / "tests" / "test_b.py" +# This is the expected output for the root folder tests. +# └── tests +# └── test_a.py +# └── test_a_function +# └── test_b.py +# └── test_b_function +root_with_config_expected_output = { + "name": "tests", + "path": os.fspath(tests_path), + "type_": "folder", + "children": [ + { + "name": "test_a.py", + "path": os.fspath(tests_a_path), + "type_": "file", + "id_": os.fspath(tests_a_path), + "children": [ + { + "name": "test_a_function", + "path": os.fspath(os.path.join(tests_path, "test_a.py")), + "lineno": find_test_line_number( + "test_a_function", + os.path.join(tests_path, "test_a.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_a.py::test_a_function", tests_a_path + ), + "runID": get_absolute_test_id( + "tests/test_a.py::test_a_function", tests_a_path + ), + } + ], + }, + { + "name": "test_b.py", + "path": os.fspath(tests_b_path), + "type_": "file", + "id_": os.fspath(tests_b_path), + "children": [ + { + "name": "test_b_function", + "path": os.fspath(os.path.join(tests_path, "test_b.py")), + "lineno": find_test_line_number( + "test_b_function", + os.path.join(tests_path, "test_b.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_b.py::test_b_function", tests_b_path + ), + "runID": get_absolute_test_id( + "tests/test_b.py::test_b_function", tests_b_path + ), + } + ], + }, + ], + "id_": os.fspath(tests_path), +} diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index fe1d40a55b43..0a7e737dfc0e 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -6,6 +6,7 @@ SUCCESS = "success" FAILURE = "failure" TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" +from .helpers import TEST_DATA_PATH, get_absolute_test_id # This is the expected output for the unittest_folder execute tests # └── unittest_folder @@ -17,30 +18,52 @@ # └── TestSubtractFunction # ├── test_subtract_negative_numbers: failure # └── test_subtract_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" uf_execution_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers": { - "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ), "outcome": FAILURE, "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers": { - "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -55,16 +78,26 @@ # │ └── TestAddFunction # │ ├── test_add_negative_numbers: success # │ └── test_add_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + uf_single_file_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -72,19 +105,24 @@ }, } + # This is the expected output for the unittest_folder execute only signle method # └── unittest_folder # ├── test_add.py # │ └── TestAddFunction # │ └── test_add_positive_numbers: success uf_single_method_execution_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, - } + }, } # This is the expected output for the unittest_folder tests run where two tests @@ -96,18 +134,28 @@ # └── test_subtract.py # └── TestSubtractFunction # └── test_subtract_positive_numbers: success +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + uf_non_adjacent_tests_execution_expected_output = { - TEST_SUBTRACT_FUNCTION - + "test_subtract_positive_numbers": { - "test": TEST_SUBTRACT_FUNCTION + "test_subtract_positive_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", test_subtract_path + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - TEST_ADD_FUNCTION - + "test_add_positive_numbers": { - "test": TEST_ADD_FUNCTION + "test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -115,12 +163,15 @@ }, } + # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function: success +simple_pytest_path = TEST_DATA_PATH / "unittest_folder" / "simple_pytest.py" + simple_execution_pytest_expected_output = { - "simple_pytest.py::test_function": { - "test": "simple_pytest.py::test_function", + get_absolute_test_id("test_function", simple_pytest_path): { + "test": get_absolute_test_id("test_function", simple_pytest_path), "outcome": "success", "message": None, "traceback": None, @@ -128,21 +179,34 @@ } } + # This is the expected output for the unittest_pytest_same_file.py file. # ├── unittest_pytest_same_file.py # ├── TestExample # │ └── test_true_unittest: success # └── test_true_pytest: success +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" unit_pytest_same_file_execution_expected_output = { - "unittest_pytest_same_file.py::TestExample::test_true_unittest": { - "test": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_pytest_same_file.py::test_true_pytest": { - "test": "unittest_pytest_same_file.py::test_true_pytest", + get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", unit_pytest_same_file_path + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), "outcome": "success", "message": None, "traceback": None, @@ -154,9 +218,15 @@ # └── error_raise_exception.py # ├── TestSomething # │ └── test_a: failure +error_raised_exception_path = TEST_DATA_PATH / "error_raise_exception.py" error_raised_exception_execution_expected_output = { - "error_raise_exception.py::TestSomething::test_a": { - "test": "error_raise_exception.py::TestSomething::test_a", + get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", error_raised_exception_path + ): { + "test": get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", + error_raised_exception_path, + ), "outcome": "error", "message": "ERROR MESSAGE", "traceback": "TRACEBACK", @@ -172,44 +242,60 @@ # ├── TestClass # │ └── test_class_function_a: skipped # │ └── test_class_function_b: skipped + +skip_tests_path = TEST_DATA_PATH / "skip_tests.py" skip_tests_execution_expected_output = { - "skip_tests.py::test_something": { - "test": "skip_tests.py::test_something", + get_absolute_test_id("skip_tests.py::test_something", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_something", skip_tests_path), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_another_thing": { - "test": "skip_tests.py::test_another_thing", + get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_another_thing", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_decorator_thing": { - "test": "skip_tests.py::test_decorator_thing", + get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_decorator_thing", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_decorator_thing_2": { - "test": "skip_tests.py::test_decorator_thing_2", + get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_decorator_thing_2", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::TestClass::test_class_function_a": { - "test": "skip_tests.py::TestClass::test_class_function_a", + get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_a", skip_tests_path + ): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_a", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::TestClass::test_class_function_b": { - "test": "skip_tests.py::TestClass::test_class_function_b", + get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_b", skip_tests_path + ): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_b", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, @@ -227,30 +313,59 @@ # └── test_bottom_folder.py # └── test_bottom_function_t: success # └── test_bottom_function_f: failure +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH + / "dual_level_nested_folder" + / "nested_folder_one" + / "test_bottom_folder.py" +) dual_level_nested_folder_execution_expected_output = { - "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -264,38 +379,59 @@ # └── folder_a # └── test_nest.py # └── test_function: success + +nested_folder_path = ( + TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +) double_nested_folder_expected_execution_output = { - "folder_a/folder_b/folder_a/test_nest.py::test_function": { - "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", + get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ): { + "test": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, } } - # This is the expected output for the nested_folder tests. # └── parametrize_tests.py # └── test_adding[3+5-8]: success # └── test_adding[2+4-6]: success # └── test_adding[6+9-16]: failure +parametrize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" + parametrize_tests_expected_execution_output = { - "parametrize_tests.py::test_adding[3+5-8]": { - "test": "parametrize_tests.py::test_adding[3+5-8]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "parametrize_tests.py::test_adding[2+4-6]": { - "test": "parametrize_tests.py::test_adding[2+4-6]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "parametrize_tests.py::test_adding[6+9-16]": { - "test": "parametrize_tests.py::test_adding[6+9-16]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -307,8 +443,12 @@ # └── parametrize_tests.py # └── test_adding[3+5-8]: success single_parametrize_tests_expected_execution_output = { - "parametrize_tests.py::test_adding[3+5-8]": { - "test": "parametrize_tests.py::test_adding[3+5-8]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, @@ -319,9 +459,12 @@ # This is the expected output for the single parameterized tests. # └── text_docstring.txt # └── text_docstring: success +doc_test_path = TEST_DATA_PATH / "text_docstring.txt" doctest_pytest_expected_execution_output = { - "text_docstring.txt::text_docstring.txt": { - "test": "text_docstring.txt::text_docstring.txt", + get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path): { + "test": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", doc_test_path + ), "outcome": "success", "message": None, "traceback": None, @@ -330,68 +473,127 @@ } # Will run all tests in the cwd that fit the test file naming pattern. +folder_a_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH + / "dual_level_nested_folder" + / "nested_folder_one" + / "test_bottom_folder.py" +) +unittest_folder_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +unittest_folder_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" + no_test_ids_pytest_execution_expected_output = { - "folder_a/folder_b/folder_a/test_nest.py::test_function": { - "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", + get_absolute_test_id("test_function", folder_a_path): { + "test": get_absolute_test_id("test_function", folder_a_path), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id( + "test_top_function_t", dual_level_nested_folder_top_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id( + "test_top_function_f", dual_level_nested_folder_top_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + get_absolute_test_id( + "test_bottom_function_t", dual_level_nested_folder_bottom_path + ): { + "test": get_absolute_test_id( + "test_bottom_function_t", dual_level_nested_folder_bottom_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + get_absolute_test_id( + "test_bottom_function_f", dual_level_nested_folder_bottom_path + ): { + "test": get_absolute_test_id( + "test_bottom_function_f", dual_level_nested_folder_bottom_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers": { - "test": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + get_absolute_test_id( + "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path + ): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers": { - "test": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + get_absolute_test_id( + "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path + ): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers": { - "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers": { - "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, } + +# This is the expected output for the root folder with the config file referenced. +# └── test_a.py +# └── test_a_function: success +test_add_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +config_file_pytest_expected_execution_output = { + get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path): { + "test": get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index c3e01d52170a..28feb6282b92 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -16,6 +16,13 @@ from typing_extensions import TypedDict +def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: + split_id = test_id.split("::")[1:] + absolute_test_id = "::".join([str(testPath), *split_id]) + print("absolute path", absolute_test_id) + return absolute_test_id + + def create_server( host: str = "127.0.0.1", port: int = 0, @@ -104,6 +111,13 @@ def process_rpc_json(data: str) -> List[Dict[str, Any]]: def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: + """Run the pytest discovery and return the JSON data from the server.""" + return runner_with_cwd(args, TEST_DATA_PATH) + + +def runner_with_cwd( + args: List[str], path: pathlib.Path +) -> Optional[List[Dict[str, Any]]]: """Run the pytest discovery and return the JSON data from the server.""" process_args: List[str] = [ sys.executable, @@ -134,7 +148,7 @@ def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: t2 = threading.Thread( target=_run_test_code, - args=(process_args, env, TEST_DATA_PATH, completed), + args=(process_args, env, path, completed), ) t2.start() diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 5288c7ad769e..8d785be27c8b 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -7,7 +7,7 @@ import pytest from . import expected_discovery_test_output -from .helpers import TEST_DATA_PATH, runner +from .helpers import TEST_DATA_PATH, runner, runner_with_cwd def test_import_error(tmp_path): @@ -153,3 +153,53 @@ def test_pytest_collect(file, expected_const): assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) assert actual["tests"] == expected_const + + +def test_pytest_root_dir(): + """ + Test to test pytest discovery with the command line arg --rootdir specified to be a subfolder + of the workspace root. Discovery should succeed and testids should be relative to workspace root. + """ + rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" + actual = runner_with_cwd( + [ + "--collect-only", + rd, + ], + TEST_DATA_PATH / "root", + ) + if actual: + actual = actual[0] + assert actual + assert all(item in actual for item in ("status", "cwd", "tests")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert ( + actual["tests"] + == expected_discovery_test_output.root_with_config_expected_output + ) + + +def test_pytest_config_file(): + """ + Test to test pytest discovery with the command line arg -c with a specified config file which + changes the workspace root. Discovery should succeed and testids should be relative to workspace root. + """ + actual = runner_with_cwd( + [ + "--collect-only", + "-c", + "tests/pytest.ini", + ], + TEST_DATA_PATH / "root", + ) + if actual: + actual = actual[0] + assert actual + assert all(item in actual for item in ("status", "cwd", "tests")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert ( + actual["tests"] + == expected_discovery_test_output.root_with_config_expected_output + ) diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index ffc84955bf54..2be4886c24c1 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -1,12 +1,56 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json import os import shutil import pytest from tests.pytestadapter import expected_execution_test_output -from .helpers import TEST_DATA_PATH, runner +from .helpers import TEST_DATA_PATH, runner, runner_with_cwd + + +def test_config_file(): + """Test pytest execution when a config file is specified.""" + args = [ + "-c", + "tests/pytest.ini", + str(TEST_DATA_PATH / "root" / "tests" / "test_a.py::test_a_function"), + ] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = ( + expected_execution_test_output.config_file_pytest_expected_execution_output + ) + assert actual + assert len(actual) == len(expected_const) + actual_result_dict = dict() + for a in actual: + assert all(item in a for item in ("status", "cwd", "result")) + assert a["status"] == "success" + assert a["cwd"] == os.fspath(new_cwd) + actual_result_dict.update(a["result"]) + assert actual_result_dict == expected_const + + +def test_rootdir_specified(): + """Test pytest execution when a --rootdir is specified.""" + rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" + args = [rd, "tests/test_a.py::test_a_function"] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = ( + expected_execution_test_output.config_file_pytest_expected_execution_output + ) + assert actual + assert len(actual) == len(expected_const) + actual_result_dict = dict() + for a in actual: + assert all(item in a for item in ("status", "cwd", "result")) + assert a["status"] == "success" + assert a["cwd"] == os.fspath(new_cwd) + actual_result_dict.update(a["result"]) + assert actual_result_dict == expected_const def test_syntax_error_execution(tmp_path): diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 1ac287a8410a..49d429662e3a 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -69,8 +69,7 @@ def pytest_exception_interact(node, call, report): """ # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. - # See if it is during discovery or execution. - # if discovery, then add the error to error logs. + # If it is during discovery, then add the error to error logs. if type(report) == pytest.CollectReport: if call.excinfo and call.excinfo.typename != "AssertionError": if report.outcome == "skipped" and "SkipTest" in str(call): @@ -83,11 +82,11 @@ def pytest_exception_interact(node, call, report): report.longreprtext + "\n Check Python Test Logs for more details." ) else: - # if execution, send this data that the given node failed. + # If during execution, send this data that the given node failed. report_value = "error" if call.excinfo.typename == "AssertionError": report_value = "failure" - node_id = str(node.nodeid) + node_id = get_absolute_test_id(node.nodeid, get_node_path(node)) if node_id not in collected_tests_so_far: collected_tests_so_far.append(node_id) item_result = create_test_outcome( @@ -106,6 +105,22 @@ def pytest_exception_interact(node, call, report): ) +def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: + """A function that returns the absolute test id. This is necessary because testIds are relative to the rootdir. + This does not work for our case since testIds when referenced during run time are relative to the instantiation + location. Absolute paths for testIds are necessary for the test tree ensures configurations that change the rootdir + of pytest are handled correctly. + + Keyword arguments: + test_id -- the pytest id of the test which is relative to the rootdir. + testPath -- the path to the file the test is located in, as a pathlib.Path object. + """ + split_id = test_id.split("::")[1:] + absolute_test_id = "::".join([str(testPath), *split_id]) + print("absolute path", absolute_test_id) + return absolute_test_id + + def pytest_keyboard_interrupt(excinfo): """A pytest hook that is called when a keyboard interrupt is raised. @@ -130,7 +145,7 @@ class TestOutcome(Dict): def create_test_outcome( - test: str, + testid: str, outcome: str, message: Union[str, None], traceback: Union[str, None], @@ -138,7 +153,7 @@ def create_test_outcome( ) -> TestOutcome: """A function that creates a TestOutcome object.""" return TestOutcome( - test=test, + test=testid, outcome=outcome, message=message, traceback=traceback, # TODO: traceback @@ -154,6 +169,7 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): IS_DISCOVERY = False +map_id_to_path = dict() def pytest_load_initial_conftests(early_config, parser, args): @@ -184,17 +200,21 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - node_id = str(report.nodeid) - if node_id not in collected_tests_so_far: - collected_tests_so_far.append(node_id) + node_path = map_id_to_path[report.nodeid] + if not node_path: + node_path = cwd + # Calculate the absolute test id and use this as the ID moving forward. + absolute_node_id = get_absolute_test_id(report.nodeid, node_path) + if absolute_node_id not in collected_tests_so_far: + collected_tests_so_far.append(absolute_node_id) item_result = create_test_outcome( - node_id, + absolute_node_id, report_value, message, traceback, ) collected_test = testRunResultDict() - collected_test[node_id] = item_result + collected_test[absolute_node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -211,21 +231,22 @@ def pytest_report_teststatus(report, config): def pytest_runtest_protocol(item, nextitem): + map_id_to_path[item.nodeid] = get_node_path(item) skipped = check_skipped_wrapper(item) if skipped: - node_id = str(item.nodeid) + absolute_node_id = get_absolute_test_id(item.nodeid, get_node_path(item)) report_value = "skipped" cwd = pathlib.Path.cwd() - if node_id not in collected_tests_so_far: - collected_tests_so_far.append(node_id) + if absolute_node_id not in collected_tests_so_far: + collected_tests_so_far.append(absolute_node_id) item_result = create_test_outcome( - node_id, + absolute_node_id, report_value, None, None, ) collected_test = testRunResultDict() - collected_test[node_id] = item_result + collected_test[absolute_node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -471,13 +492,14 @@ def create_test_node( test_case_loc: str = ( str(test_case.location[1] + 1) if (test_case.location[1] is not None) else "" ) + absolute_test_id = get_absolute_test_id(test_case.nodeid, get_node_path(test_case)) return { "name": test_case.name, "path": get_node_path(test_case), "lineno": test_case_loc, "type_": "test", - "id_": test_case.nodeid, - "runID": test_case.nodeid, + "id_": absolute_test_id, + "runID": absolute_test_id, } From 11a9f1d2a9a81097d16ce5f71385faa730bb7236 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 31 Jul 2023 23:34:41 +1000 Subject: [PATCH 193/336] Remove unwanted Jupyter API (#21702) Fixes https://github.com/microsoft/vscode-jupyter/issues/13986 --- pythonExtensionApi/src/main.ts | 60 +----------------------- src/client/api.ts | 12 ----- src/client/api/types.ts | 14 ------ src/client/jupyter/jupyterIntegration.ts | 30 ------------ 4 files changed, 1 insertion(+), 115 deletions(-) diff --git a/pythonExtensionApi/src/main.ts b/pythonExtensionApi/src/main.ts index cf1461f04d81..4de554bf5a24 100644 --- a/pythonExtensionApi/src/main.ts +++ b/pythonExtensionApi/src/main.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem, extensions } from 'vscode'; +import { CancellationToken, Event, Uri, WorkspaceFolder, extensions } from 'vscode'; /* * Do not introduce any breaking changes to this API. @@ -12,9 +12,6 @@ export interface PythonExtension { * Promise indicating whether all parts of the extension have completed loading or not. */ ready: Promise; - jupyter: { - registerHooks(): void; - }; debug: { /** * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. @@ -33,20 +30,6 @@ export interface PythonExtension { getDebuggerPackagePath(): Promise; }; - datascience: { - /** - * Launches Data Viewer component. - * @param dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param title Data Viewer title - */ - showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; - /** - * Registers a remote server provider component that's used to pick remote jupyter server URIs - * @param serverProvider object called back when picking jupyter server URI - */ - registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; - }; - /** * These APIs provide a way for extensions to work with by python environments available in the user's machine * as found by the Python extension. See @@ -123,47 +106,6 @@ export interface PythonExtension { }; } -interface IJupyterServerUri { - baseUrl: string; - token: string; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationHeader: any; // JSON object for authorization header. - expiration?: Date; // Date/time when header expires and should be refreshed. - displayName: string; -} - -type JupyterServerUriHandle = string; - -export interface IJupyterUriProvider { - readonly id: string; // Should be a unique string (like a guid) - getQuickPickEntryItems(): QuickPickItem[]; - handleQuickPick(item: QuickPickItem, backEnabled: boolean): Promise; - getServerUri(handle: JupyterServerUriHandle): Promise; -} - -interface IDataFrameInfo { - columns?: { key: string; type: ColumnType }[]; - indexColumn?: string; - rowCount?: number; -} - -export interface IDataViewerDataProvider { - dispose(): void; - getDataFrameInfo(): Promise; - getAllRows(): Promise; - getRows(start: number, end: number): Promise; -} - -enum ColumnType { - String = 'string', - Number = 'number', - Bool = 'bool', -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type IRowsResponse = any[]; - export type RefreshOptions = { /** * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so diff --git a/src/client/api.ts b/src/client/api.ts index 32ce68f6a28b..2371a4d88de1 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -4,7 +4,6 @@ 'use strict'; -import { noop } from 'lodash'; import { Uri, Event } from 'vscode'; import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; import { LanguageClient } from 'vscode-languageclient/node'; @@ -17,7 +16,6 @@ import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extens import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer, IServiceManager } from './ioc/types'; import { JupyterExtensionIntegration } from './jupyter/jupyterIntegration'; -import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types'; import { traceError } from './logging'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { buildEnvironmentApi } from './environmentApi'; @@ -111,16 +109,6 @@ export function buildApi( return { execCommand: pythonPath === '' ? undefined : [pythonPath] }; }, }, - // These are for backwards compatibility. Other extensions are using these APIs and we don't want - // to force them to move to the jupyter extension ... yet. - datascience: { - registerRemoteServerProvider: jupyterIntegration - ? jupyterIntegration.registerRemoteServerProvider.bind(jupyterIntegration) - : ((noop as unknown) as (serverProvider: IJupyterUriProvider) => void), - showDataViewer: jupyterIntegration - ? jupyterIntegration.showDataViewer.bind(jupyterIntegration) - : ((noop as unknown) as (dataProvider: IDataViewerDataProvider, title: string) => Promise), - }, pylance: { createClient: (...args: any[]): BaseLanguageClient => { // Make sure we share output channel so that we can share one with diff --git a/src/client/api/types.ts b/src/client/api/types.ts index cf1461f04d81..63954a16d868 100644 --- a/src/client/api/types.ts +++ b/src/client/api/types.ts @@ -33,20 +33,6 @@ export interface PythonExtension { getDebuggerPackagePath(): Promise; }; - datascience: { - /** - * Launches Data Viewer component. - * @param dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param title Data Viewer title - */ - showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; - /** - * Registers a remote server provider component that's used to pick remote jupyter server URIs - * @param serverProvider object called back when picking jupyter server URI - */ - registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; - }; - /** * These APIs provide a way for extensions to work with by python environments available in the user's machine * as found by the Python extension. See diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index a0fa0fedb63f..7f660d26e5e4 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -33,7 +33,6 @@ import { PythonEnvironmentsChangedEvent, } from '../interpreter/contracts'; import { PythonEnvironment } from '../pythonEnvironments/info'; -import { IDataViewerDataProvider, IJupyterUriProvider } from './types'; import { PylanceApi } from '../activation/node/pylanceApi'; import { ExtensionContextKey } from '../common/application/contextKeys'; /** @@ -168,17 +167,6 @@ type JupyterExtensionApi = { * @param interpreterService */ registerPythonApi(interpreterService: PythonApiForJupyterExtension): void; - /** - * Launches Data Viewer component. - * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param {string} title Data Viewer title - */ - showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; - /** - * Registers a remote server provider component that's used to pick remote jupyter server URIs - * @param serverProvider object called back when picking jupyter server URI - */ - registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; }; @injectable() @@ -286,24 +274,6 @@ export class JupyterExtensionIntegration { } } - public registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void { - this.getExtensionApi() - .then((e) => { - if (e) { - e.registerRemoteServerProvider(serverProvider); - } - }) - .ignoreErrors(); - } - - public async showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise { - const api = await this.getExtensionApi(); - if (api) { - return api.showDataViewer(dataProvider, title); - } - return undefined; - } - private async getExtensionApi(): Promise { if (!this.pylanceExtension) { const pylanceExtension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); From ceecdb0c81b46e2e53e2b2dfe1002c7c3ad5343f Mon Sep 17 00:00:00 2001 From: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> Date: Mon, 31 Jul 2023 12:36:31 -0500 Subject: [PATCH 194/336] Removing Jupyter Notebooks mentions from package.json (#21708) --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index abadcdb3bb0b..ad196c164a5f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "python", "displayName": "Python", - "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", + "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", "version": "2023.13.0-dev", "featureFlags": { "usingNewInterpreterStorage": true @@ -62,8 +62,7 @@ "Formatters", "Other", "Data Science", - "Machine Learning", - "Notebooks" + "Machine Learning" ], "activationEvents": [ "onDebugInitialConfigurations", From d9e368f04733802d766280b90e67aa2076768c12 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 31 Jul 2023 11:51:06 -0700 Subject: [PATCH 195/336] add area-repl to issue label (#21718) added area-repl as one of the issue label. --- .github/workflows/issue-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index e942a4c965ec..d54015d94e46 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -6,7 +6,7 @@ on: env: # To update the list of labels, see `getLabels.js`. - REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' + REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-repl","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd","anthonykim1"]' permissions: From 237f82b696725011beaa6a46bc75b6307efefc6f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 31 Jul 2023 15:29:08 -0700 Subject: [PATCH 196/336] Fix UUID and disposing to resolve race condition (#21667) fixes https://github.com/microsoft/vscode-python/issues/21599 and https://github.com/microsoft/vscode-python/issues/21507 --- .../testing/testController/common/server.ts | 27 +- .../testing/testController/common/types.ts | 7 +- .../pytest/pytestDiscoveryAdapter.ts | 27 +- .../pytest/pytestExecutionAdapter.ts | 45 ++-- .../unittest/testDiscoveryAdapter.ts | 11 +- .../unittest/testExecutionAdapter.ts | 21 +- src/test/mocks/mockChildProcess.ts | 238 +++++++++++++++++ .../pytestDiscoveryAdapter.unit.test.ts | 76 ++++-- .../pytestExecutionAdapter.unit.test.ts | 118 +++++++-- .../testController/server.unit.test.ts | 248 +++++++++++++----- .../workspaceTestAdapter.unit.test.ts | 3 +- 11 files changed, 664 insertions(+), 157 deletions(-) create mode 100644 src/test/mocks/mockChildProcess.ts diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index f854371ffc35..564bd82f2ef6 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -3,10 +3,11 @@ import * as net from 'net'; import * as crypto from 'crypto'; -import { Disposable, Event, EventEmitter } from 'vscode'; +import { Disposable, Event, EventEmitter, TestRun } from 'vscode'; import * as path from 'path'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -15,6 +16,7 @@ import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; +import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); @@ -140,7 +142,12 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDataReceived.event; } - async sendCommand(options: TestCommandOptions, runTestIdPort?: string, callback?: () => void): Promise { + async sendCommand( + options: TestCommandOptions, + runTestIdPort?: string, + runInstance?: TestRun, + callback?: () => void, + ): Promise { const { uuid } = options; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; @@ -154,7 +161,7 @@ export class PythonTestServer implements ITestServer, Disposable { }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; - const isRun = !options.testIds; + const isRun = runTestIdPort !== undefined; // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, @@ -195,7 +202,19 @@ export class PythonTestServer implements ITestServer, Disposable { // This means it is running discovery traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } - await execService.exec(args, spawnOptions); + const deferred = createDeferred>(); + + const result = execService.execObservable(args, spawnOptions); + + runInstance?.token.onCancellationRequested(() => { + result?.proc?.kill(); + }); + result?.proc?.on('close', () => { + traceLog('Exec server closed.', uuid); + deferred.resolve({ stdout: '', stderr: '' }); + callback?.(); + }); + await deferred.promise; } } catch (ex) { this.uuids = this.uuids.filter((u) => u !== uuid); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index d4e54951bfd7..16c0bd0e3cee 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -174,7 +174,12 @@ export interface ITestServer { readonly onDataReceived: Event; readonly onRunDataReceived: Event; readonly onDiscoveryDataReceived: Event; - sendCommand(options: TestCommandOptions, runTestIdsPort?: string, callback?: () => void): Promise; + sendCommand( + options: TestCommandOptions, + runTestIdsPort?: string, + runInstance?: TestRun, + callback?: () => void, + ): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index b83224d4161b..810fae0fa11c 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -4,13 +4,14 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceError, traceVerbose } from '../../../logging'; +import { traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, @@ -48,7 +49,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); @@ -78,17 +79,15 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. - execService - ?.exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) - .then(() => { - this.testServer.deleteUUID(uuid); - return deferred.resolve(); - }) - .catch((err) => { - traceError(`Error while trying to run pytest discovery, \n${err}\r\n\r\n`); - this.testServer.deleteUUID(uuid); - return deferred.reject(err); - }); - return deferred.promise; + const deferredExec = createDeferred>(); + const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + const result = execService?.execObservable(execArgs, spawnOptions); + + result?.proc?.on('close', () => { + deferredExec.resolve({ stdout: '', stderr: '' }); + this.testServer.deleteUUID(uuid); + deferred.resolve(); + }); + await deferredExec.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index a75a6089627c..b05fa21fc046 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -15,6 +15,7 @@ import { } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -22,13 +23,7 @@ import { removePositionalFoldersAndFiles } from './arguments'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; -import { startTestIdServer } from '../common/utils'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; -/** - * Wrapper Class for pytest test execution.. - */ +import * as utils from '../common/utils'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( @@ -48,18 +43,20 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); traceVerbose(uri, testIds, debugBool); - const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - try { - await this.runTestsNew(uri, testIds, uuid, debugBool, executionFactory, debugLauncher); - } finally { - this.testServer.deleteUUID(uuid); - disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) - } + const dispose = function (testServer: ITestServer) { + testServer.deleteUUID(uuid); + disposedDataReceived.dispose(); + }; + runInstance?.token.onCancellationRequested(() => { + dispose(this.testServer); + }); + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, executionFactory, debugLauncher); + // placeholder until after the rewrite is adopted // TODO: remove after adoption. const executionPayload: ExecutionTestPayload = { @@ -74,6 +71,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, + runInstance?: TestRun, debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, @@ -124,7 +122,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); - const pytestRunTestIdsPort = await startTestIdServer(testIds); + const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); @@ -143,6 +141,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { deferred.resolve(); + this.testServer.deleteUUID(uuid); }); } else { // combine path to run script with run args @@ -150,7 +149,19 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const runArgs = [scriptPath, ...testArgs]; traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); - await execService?.exec(runArgs, spawnOptions); + const deferredExec = createDeferred>(); + const result = execService?.execObservable(runArgs, spawnOptions); + + runInstance?.token.onCancellationRequested(() => { + result?.proc?.kill(); + }); + + result?.proc?.on('close', () => { + deferredExec.resolve({ stdout: '', stderr: '' }); + this.testServer.deleteUUID(uuid); + deferred.resolve(); + }); + await deferredExec.promise; } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 6deca55117ea..b49ac3dabd0e 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -46,12 +46,11 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); }); - try { - await this.callSendCommand(options); - } finally { + + await this.callSendCommand(options, () => { this.testServer.deleteUUID(uuid); disposable.dispose(); - } + }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { @@ -61,8 +60,8 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - private async callSendCommand(options: TestCommandOptions): Promise { - await this.testServer.sendCommand(options); + private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { + await this.testServer.sendCommand(options, undefined, undefined, callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 4cab941c2608..4cd392f93a43 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -37,18 +37,19 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - try { - await this.runTestsNew(uri, testIds, uuid, debugBool); - } finally { + const dispose = function () { + disposedDataReceived.dispose(); + }; + runInstance?.token.onCancellationRequested(() => { this.testServer.deleteUUID(uuid); - disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) - } + dispose(); + }); + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, dispose); const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } @@ -57,7 +58,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, + runInstance?: TestRun, debugBool?: boolean, + dispose?: () => void, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -80,8 +83,10 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), () => { + await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, () => { + this.testServer.deleteUUID(uuid); deferred.resolve(); + dispose?.(); }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts new file mode 100644 index 000000000000..c038c0f845ab --- /dev/null +++ b/src/test/mocks/mockChildProcess.ts @@ -0,0 +1,238 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Serializable, SendHandle, MessageOptions } from 'child_process'; +import { Writable, Readable, Pipe } from 'stream'; +import { EventEmitter } from 'node:events'; + +export class MockChildProcess extends EventEmitter { + constructor(spawnfile: string, spawnargs: string[]) { + super(); + this.spawnfile = spawnfile; + this.spawnargs = spawnargs; + this.stdin = new Writable(); + this.stdout = new Readable(); + this.stderr = null; + this.channel = null; + this.stdio = [this.stdin, this.stdout, this.stdout, this.stderr, null]; + this.killed = false; + this.connected = false; + this.exitCode = null; + this.signalCode = null; + this.eventMap = new Map(); + } + + stdin: Writable | null; + + stdout: Readable | null; + + stderr: Readable | null; + + eventMap: Map; + + readonly channel?: Pipe | null | undefined; + + readonly stdio: [ + Writable | null, + // stdin + Readable | null, + // stdout + Readable | null, + // stderr + Readable | Writable | null | undefined, + // extra + Readable | Writable | null | undefined, // extra + ]; + + readonly killed: boolean; + + readonly pid?: number | undefined; + + readonly connected: boolean; + + readonly exitCode: number | null; + + readonly signalCode: NodeJS.Signals | null; + + readonly spawnargs: string[]; + + readonly spawnfile: string; + + signal?: NodeJS.Signals | number; + + send(message: Serializable, callback?: (error: Error | null) => void): boolean; + + send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean; + + send( + message: Serializable, + sendHandle?: SendHandle, + options?: MessageOptions, + callback?: (error: Error | null) => void, + ): boolean; + + send( + message: Serializable, + _sendHandleOrCallback?: SendHandle | ((error: Error | null) => void), + _optionsOrCallback?: MessageOptions | ((error: Error | null) => void), + _callback?: (error: Error | null) => void, + ): boolean { + // Implementation of the send method + // For example, you might want to emit a 'message' event + this.stdout?.push(message.toString()); + return true; + } + + // eslint-disable-next-line class-methods-use-this + disconnect(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + unref(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + ref(): void { + /* noop */ + } + + addListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'disconnect', listener: () => void): this; + + addListener(event: 'error', listener: (err: Error) => void): this; + + addListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + addListener(event: 'spawn', listener: () => void): this; + + addListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + emit(event: 'close', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'disconnect'): boolean; + + emit(event: 'error', err: Error): boolean; + + emit(event: 'exit', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'message', message: Serializable, sendHandle: SendHandle): boolean; + + emit(event: 'spawn', listener: () => void): boolean; + + emit(event: string | symbol, ...args: unknown[]): boolean { + if (this.eventMap.has(event.toString())) { + this.eventMap.get(event.toString()).forEach((listener: (arg0: unknown) => void) => { + const argsArray = Array.isArray(args) ? args : [args]; + listener(argsArray); + }); + } + return true; + } + + on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'disconnect', listener: () => void): this; + + on(event: 'error', listener: (err: Error) => void): this; + + on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + on(event: 'spawn', listener: () => void): this; + + on(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'disconnect', listener: () => void): this; + + once(event: 'error', listener: (err: Error) => void): this; + + once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + once(event: 'spawn', listener: () => void): this; + + once(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'disconnect', listener: () => void): this; + + prependListener(event: 'error', listener: (err: Error) => void): this; + + prependListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependListener(event: 'spawn', listener: () => void): this; + + prependListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependOnceListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'disconnect', listener: () => void): this; + + prependOnceListener(event: 'error', listener: (err: Error) => void): this; + + prependOnceListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependOnceListener(event: 'spawn', listener: () => void): this; + + prependOnceListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + trigger(event: string): Array { + if (this.eventMap.has(event)) { + return this.eventMap.get(event); + } + return []; + } + + kill(_signal?: NodeJS.Signals | number): boolean { + this.stdout?.destroy(); + return true; + } +} diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 18212b2d1032..8ba7dd9a6f00 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; import { ITestServer } from '../../../../client/testing/testController/common/types'; @@ -12,9 +13,11 @@ import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions, + Output, } from '../../../../client/common/process/types'; -import { createDeferred, Deferred } from '../../../../client/common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { Deferred, createDeferred } from '../../../../client/common/utils/async'; suite('pytest test discovery adapter', () => { let testServer: typeMoq.IMock; @@ -29,6 +32,7 @@ suite('pytest test discovery adapter', () => { let expectedPath: string; let uri: Uri; let expectedExtraVariables: Record; + let mockProc: MockChildProcess; setup(() => { const mockExtensionRootDir = typeMoq.Mock.ofType(); @@ -66,32 +70,46 @@ suite('pytest test discovery adapter', () => { }), } as unknown) as IConfigurationService; - // set up exec factory - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => Promise.resolve(execService.object)); - - // set up exec service + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); - deferred = createDeferred(); + const output = new Observable>(() => { + /* no op */ + }); execService - .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve({ stdout: '{}' }); - }); + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); outputChannel = typeMoq.Mock.ofType(); }); test('Discovery should call exec with correct basic args', async () => { + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - await adapter.discoverTests(uri, execFactory.object); - const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); + // verification + const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); @@ -108,16 +126,34 @@ suite('pytest test discovery adapter', () => { const expectedPathNew = path.join('other', 'path'); const configServiceNew: IConfigurationService = ({ getSettings: () => ({ - testing: { pytestArgs: ['.', 'abc', 'xyz'], cwd: expectedPathNew }, + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPathNew, + }, }), } as unknown) as IConfigurationService; + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configServiceNew, outputChannel.object); - await adapter.discoverTests(uri, execFactory.object); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); + + // verification const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.', 'abc', 'xyz']; execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 44116fd753b0..43b763f56e6c 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -6,11 +6,13 @@ import { TestRun, Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as sinon from 'sinon'; import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { ITestServer } from '../../../../client/testing/testController/common/types'; import { IPythonExecutionFactory, IPythonExecutionService, + Output, SpawnOptions, } from '../../../../client/common/process/types'; import { createDeferred, Deferred } from '../../../../client/common/utils/async'; @@ -18,6 +20,7 @@ import { PytestTestExecutionAdapter } from '../../../../client/testing/testContr import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; import * as util from '../../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; suite('pytest test execution adapter', () => { let testServer: typeMoq.IMock; @@ -29,8 +32,8 @@ suite('pytest test execution adapter', () => { let debugLauncher: typeMoq.IMock; (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; - let startTestIdServerStub: sinon.SinonStub; - + let mockProc: MockChildProcess; + let utilsStub: sinon.SinonStub; setup(() => { testServer = typeMoq.Mock.ofType(); testServer.setup((t) => t.getPort()).returns(() => 12345); @@ -47,8 +50,24 @@ suite('pytest test execution adapter', () => { }), isTestExecution: () => false, } as unknown) as IConfigurationService; - execFactory = typeMoq.Mock.ofType(); + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execFactory = typeMoq.Mock.ofType(); + utilsStub = sinon.stub(util, 'startTestIdServer'); debugLauncher = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) @@ -66,7 +85,6 @@ suite('pytest test execution adapter', () => { deferred.resolve(); return Promise.resolve(); }); - startTestIdServerStub = sinon.stub(util, 'startTestIdServer').returns(Promise.resolve(54321)); execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); @@ -77,10 +95,25 @@ suite('pytest test execution adapter', () => { sinon.restore(); }); test('startTestIdServer called with correct testIds', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -88,19 +121,38 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - const testIds = ['test1id', 'test2id']; - await adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); + adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); - sinon.assert.calledWithExactly(startTestIdServerStub, testIds); + // add in await and trigger + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); + + // assert + sinon.assert.calledWithExactly(utilsStub, testIds); }); test('pytest execution called with correct args', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -108,9 +160,12 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -123,7 +178,7 @@ suite('pytest test execution adapter', () => { // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -139,6 +194,21 @@ suite('pytest test execution adapter', () => { ); }); test('pytest execution respects settings.testing.cwd when present', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const newCwd = path.join('new', 'path'); configService = ({ getSettings: () => ({ @@ -149,7 +219,7 @@ suite('pytest test execution adapter', () => { const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -157,9 +227,12 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -172,7 +245,7 @@ suite('pytest test execution adapter', () => { execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -188,10 +261,17 @@ suite('pytest test execution adapter', () => { ); }); test('Debug launched correctly for pytest', async () => { + const deferred3 = createDeferred(); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -199,9 +279,9 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); await adapter.runTests(uri, [], true, testRun.object, execFactory.object, debugLauncher.object); + await deferred3.promise; debugLauncher.verify( (x) => x.launchDebugger( diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 1131c26c6444..53c2b72e40f7 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -1,15 +1,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as assert from 'assert'; import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; import { OutputChannel, Uri } from 'vscode'; -import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions } from '../../../client/common/process/types'; +import { Observable } from 'rxjs'; +import * as typeMoq from 'typemoq'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; suite('Python Test Server', () => { const fakeUuid = 'fake-uuid'; @@ -18,10 +27,12 @@ suite('Python Test Server', () => { let stubExecutionService: IPythonExecutionService; let server: PythonTestServer; let sandbox: sinon.SinonSandbox; - let execArgs: string[]; - let spawnOptions: SpawnOptions; let v4Stub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock; + let deferred: Deferred; + let execFactory = typeMoq.Mock.ofType(); setup(() => { sandbox = sinon.createSandbox(); @@ -29,27 +40,42 @@ suite('Python Test Server', () => { v4Stub.returns(fakeUuid); stubExecutionService = ({ - exec: (args: string[], spawnOptionsProvided: SpawnOptions) => { - execArgs = args; - spawnOptions = spawnOptionsProvided; - return Promise.resolve({ stdout: '', stderr: '' }); - }, + execObservable: () => Promise.resolve({ stdout: '', stderr: '' }), } as unknown) as IPythonExecutionService; stubExecutionFactory = ({ createActivatedEnvironment: () => Promise.resolve(stubExecutionService), } as unknown) as IPythonExecutionFactory; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + execService = typeMoq.Mock.ofType(); + const output = new Observable>(() => { + /* no op */ + }); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); }); teardown(() => { sandbox.restore(); - execArgs = []; server.dispose(); }); test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, + command: { + script: 'myscript', + args: ['-foo', 'foo'], + }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, @@ -59,17 +85,31 @@ suite('Python Test Server', () => { outputChannel: undefined, token: undefined, throwOnStdErr: true, - extraVariables: { PYTHONPATH: '/foo/bar', RUN_TEST_IDS_PORT: '56789' }, + extraVariables: { + PYTHONPATH: '/foo/bar', + RUN_TEST_IDS_PORT: '56789', + }, } as SpawnOptions; + const deferred2 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - await server.sendCommand(options, '56789'); - const port = server.getPort(); + server.sendCommand(options, '56789'); + // add in await and trigger + await deferred2.promise; + mockProc.trigger('close'); - assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); - assert.deepStrictEqual(spawnOptions, expectedSpawnOptions); + const port = server.getPort(); + const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']; + execService.verify((x) => x.execObservable(expectedArgs, expectedSpawnOptions), typeMoq.Times.once()); }); test('sendCommand should write to an output channel if it is provided as an option', async () => { @@ -80,17 +120,31 @@ suite('Python Test Server', () => { }, } as OutputChannel; const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, + command: { + script: 'myscript', + args: ['-foo', 'foo'], + }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, outChannel, }; + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - await server.sendCommand(options); + server.sendCommand(options); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); const port = server.getPort(); const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); @@ -99,13 +153,12 @@ suite('Python Test Server', () => { }); test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { - let eventData: { status: string; errors: string[] }; + let eventData: { status: string; errors: string[] } | undefined; stubExecutionService = ({ - exec: () => { + execObservable: () => { throw new Error('Failed to execute'); }, } as unknown) as IPythonExecutionService; - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -122,30 +175,43 @@ suite('Python Test Server', () => { await server.sendCommand(options); - assert.deepStrictEqual(eventData!.status, 'error'); - assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); + assert.notEqual(eventData, undefined); + assert.deepStrictEqual(eventData?.status, 'error'); + assert.deepStrictEqual(eventData?.errors, ['Failed to execute']); }); test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + + deferred = createDeferred(); + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -161,16 +227,17 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); + // add in await and trigger await deferred.promise; + mockProc.trigger('close'); + assert.deepStrictEqual(eventData, ''); }); test('If the server doesnt recognize the UUID it should ignore it', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -178,14 +245,28 @@ suite('Python Test Server', () => { uuid: fakeUuid, }; - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -201,7 +282,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -212,23 +293,34 @@ suite('Python Test Server', () => { test('Error if payload does not have a content length header', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -244,7 +336,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -267,7 +359,6 @@ Request-uuid: UUID_HERE // Your test logic here let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, @@ -275,15 +366,28 @@ Request-uuid: UUID_HERE cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); payload = payload.replace('UUID_HERE', uuid); @@ -301,7 +405,7 @@ Request-uuid: UUID_HERE console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, expectedResult); }); @@ -310,8 +414,29 @@ Request-uuid: UUID_HERE test('Calls run resolver if the result header is in the payload', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { + client.connect(server.getPort()); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -319,14 +444,6 @@ Request-uuid: UUID_HERE uuid: fakeUuid, }; - stubExecutionService = ({ - exec: async () => { - client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); - }, - } as unknown) as IPythonExecutionService; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); server.onRunDataReceived(({ data }) => { @@ -349,9 +466,8 @@ Request-uuid: ${uuid} console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; - console.log('event data', eventData); const expectedResult = '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; assert.deepStrictEqual(eventData, expectedResult); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index 5a2e48130746..41cd1bbd7ef2 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -164,8 +164,7 @@ suite('Workspace test adapter', () => { const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); const testProvider = 'unittest'; - const abc = await workspaceTestAdapter.discoverTests(testController); - console.log(abc); + await workspaceTestAdapter.discoverTests(testController); sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); From 3e7118fe205d3489fd7a396c3d1897416c8380d5 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 31 Jul 2023 18:56:11 -0700 Subject: [PATCH 197/336] Update packages for Jedi and core python (#21710) --- .../jedilsp_requirements/requirements.txt | 134 +++++++++--------- requirements.txt | 10 +- 2 files changed, 75 insertions(+), 69 deletions(-) diff --git a/pythonFiles/jedilsp_requirements/requirements.txt b/pythonFiles/jedilsp_requirements/requirements.txt index 062b037e0783..90d10b467640 100644 --- a/pythonFiles/jedilsp_requirements/requirements.txt +++ b/pythonFiles/jedilsp_requirements/requirements.txt @@ -4,28 +4,31 @@ # # pip-compile --generate-hashes 'pythonFiles\jedilsp_requirements\requirements.in' # -attrs==22.2.0 \ - --hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \ - --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99 +attrs==23.1.0 \ + --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ + --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 # via # cattrs # lsprotocol -cattrs==22.2.0 \ - --hash=sha256:bc12b1f0d000b9f9bee83335887d532a1d3e99a833d1bf0882151c97d3e68c21 \ - --hash=sha256:f0eed5642399423cf656e7b66ce92cdc5b963ecafd041d1b24d136fdde7acf6d +cattrs==23.1.2 \ + --hash=sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4 \ + --hash=sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657 # via lsprotocol -docstring-to-markdown==0.11 \ - --hash=sha256:01900aee1bc7fde5aacaf319e517a5e1d4f0bf04e401373c08d28fcf79bfb73b \ - --hash=sha256:5b1da2c89d9d0d09b955dec0ee111284ceadd302a938a03ed93f66e09134f9b5 +docstring-to-markdown==0.12 \ + --hash=sha256:40004224b412bd6f64c0f3b85bb357a41341afd66c4b4896709efa56827fb2bb \ + --hash=sha256:7df6311a887dccf9e770f51242ec002b19f0591994c4783be49d24cdc1df3737 # via jedi-language-server -exceptiongroup==1.1.0 \ - --hash=sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e \ - --hash=sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23 +exceptiongroup==1.1.2 \ + --hash=sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5 \ + --hash=sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f # via cattrs importlib-metadata==3.10.1 \ --hash=sha256:2ec0faae539743ae6aaa84b49a169670a465f7f5d64e6add98388cc29fd1f2f6 \ --hash=sha256:c9356b657de65c53744046fa8f7358afe0714a1af7d570c00c3835c2d724a7c1 - # via jedi-language-server + # via + # attrs + # jedi-language-server + # typeguard jedi==0.18.2 \ --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \ --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612 @@ -34,9 +37,9 @@ jedi-language-server==0.40.0 \ --hash=sha256:53e590400b5cd2f6e363e77a4d824b1883798994b731cb0b4370d103748d30e2 \ --hash=sha256:bacbae2930b6a8a0f1f284c211672fceec94b4808b0415d1c3352fa4b1ac5ad6 # via -r pythonFiles\jedilsp_requirements\requirements.in -lsprotocol==2022.0.0a10 \ - --hash=sha256:2cd78770b7a4ec979f3ee3761265effd50ea0f5e858ce21bf2fba972e1783c50 \ - --hash=sha256:ef516aec43c2b3c8debc06e84558ea9a64c36d635422d1614fd7fd2a45b1d291 +lsprotocol==2023.0.0a2 \ + --hash=sha256:80aae7e39171b49025876a524937c10be2eb986f4be700ca22ee7d186b8488aa \ + --hash=sha256:c4f2f77712b50d065b17f9b50d2b88c480dc2ce4bbaa56eea8269dbf54bc9701 # via # jedi-language-server # pygls @@ -44,62 +47,63 @@ parso==0.8.3 \ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 # via jedi -pydantic==1.10.4 \ - --hash=sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72 \ - --hash=sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423 \ - --hash=sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f \ - --hash=sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c \ - --hash=sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06 \ - --hash=sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53 \ - --hash=sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774 \ - --hash=sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6 \ - --hash=sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c \ - --hash=sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f \ - --hash=sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6 \ - --hash=sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3 \ - --hash=sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817 \ - --hash=sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903 \ - --hash=sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a \ - --hash=sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e \ - --hash=sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d \ - --hash=sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85 \ - --hash=sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00 \ - --hash=sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28 \ - --hash=sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3 \ - --hash=sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024 \ - --hash=sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4 \ - --hash=sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e \ - --hash=sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d \ - --hash=sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa \ - --hash=sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854 \ - --hash=sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15 \ - --hash=sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648 \ - --hash=sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8 \ - --hash=sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c \ - --hash=sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857 \ - --hash=sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f \ - --hash=sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416 \ - --hash=sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978 \ - --hash=sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d +pydantic==1.10.12 \ + --hash=sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303 \ + --hash=sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe \ + --hash=sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47 \ + --hash=sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494 \ + --hash=sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33 \ + --hash=sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86 \ + --hash=sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d \ + --hash=sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c \ + --hash=sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a \ + --hash=sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565 \ + --hash=sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb \ + --hash=sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62 \ + --hash=sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62 \ + --hash=sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0 \ + --hash=sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523 \ + --hash=sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d \ + --hash=sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405 \ + --hash=sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f \ + --hash=sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b \ + --hash=sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718 \ + --hash=sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed \ + --hash=sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb \ + --hash=sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5 \ + --hash=sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc \ + --hash=sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942 \ + --hash=sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe \ + --hash=sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246 \ + --hash=sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350 \ + --hash=sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303 \ + --hash=sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09 \ + --hash=sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33 \ + --hash=sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8 \ + --hash=sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a \ + --hash=sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1 \ + --hash=sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6 \ + --hash=sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d # via jedi-language-server -pygls==1.0.0 \ - --hash=sha256:3414594ac29ff3ab990f004c675d1077e4e2659eae5cc3ae67cc6fa4d861e342 \ - --hash=sha256:c2a1c22e30028f7ca9d3f0a04da8eef29f0f1701bdbd97d8614d8e1e6711f336 +pygls==1.0.2 \ + --hash=sha256:6d278d29fa6559b0f7a448263c85cb64ec6e9369548b02f1a7944060848b21f9 \ + --hash=sha256:888ed63d1f650b4fc64d603d73d37545386ec533c0caac921aed80f80ea946a4 # via # -r pythonFiles\jedilsp_requirements\requirements.in # jedi-language-server -typeguard==2.13.3 \ - --hash=sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4 \ - --hash=sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1 +typeguard==3.0.2 \ + --hash=sha256:bbe993854385284ab42fd5bd3bee6f6556577ce8b50696d6cb956d704f286c8e \ + --hash=sha256:fee5297fdb28f8e9efcb8142b5ee219e02375509cd77ea9d270b5af826358d5a # via pygls -typing-extensions==4.4.0 \ - --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ - --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e +typing-extensions==4.7.1 \ + --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ + --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 # via # cattrs # importlib-metadata # pydantic -zipp==3.12.0 \ - --hash=sha256:73efd63936398aac78fd92b6f4865190119d6c91b531532e798977ea8dd402eb \ - --hash=sha256:9eb0a4c5feab9b08871db0d672745b53450d7f26992fd1e4653aa43345e97b86 + # typeguard +zipp==3.15.0 \ + --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ + --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 # via importlib-metadata diff --git a/requirements.txt b/requirements.txt index 2747490fdba8..f2af0ca4204b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,9 @@ # # pip-compile --generate-hashes requirements.in # -importlib-metadata==6.6.0 \ - --hash=sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed \ - --hash=sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705 +importlib-metadata==6.7.0 \ + --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ + --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 # via -r requirements.in microvenv==2023.2.0 \ --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ @@ -23,7 +23,9 @@ tomli==2.0.1 \ typing-extensions==4.7.1 \ --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 - # via -r requirements.in + # via + # -r requirements.in + # importlib-metadata zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 From 4ab510d1ae6e079b5d0a2509d7cd21822d93e17e Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 1 Aug 2023 13:59:38 -0700 Subject: [PATCH 198/336] Update version for release candidate (#21727) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3678852d1f65..ae13db90c8df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.13.0-dev", + "version": "2023.14.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.13.0-dev", + "version": "2023.14.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index ad196c164a5f..96e252302abb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.13.0-dev", + "version": "2023.14.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From a6a8cb183913e20249a2bb8165c9a5b0d91de40e Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 1 Aug 2023 14:23:43 -0700 Subject: [PATCH 199/336] Update main to next pre-release (#21728) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae13db90c8df..01f03cb31661 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.14.0-rc", + "version": "2023.15.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.14.0-rc", + "version": "2023.15.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 96e252302abb..b6a4b65b42a9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.14.0-rc", + "version": "2023.15.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 8f3d60bf378a2c50609d5512c930e4174bf61728 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 1 Aug 2023 14:32:44 -0700 Subject: [PATCH 200/336] unittest discovery errors not displaying in test explorer (#21726) saw an issue where if discovery failed there was no notice in the test explorer for unittest. It was due to a different value for the new blank value for the payload tests. fixes https://github.com/microsoft/vscode-python/issues/21725 and https://github.com/microsoft/vscode-python/issues/21688 --- src/client/testing/testController/common/resultResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 8baf4d0d7ae7..6e875473c836 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -68,7 +68,7 @@ export class PythonResultResolver implements ITestResultResolver { // remove error node only if no errors exist. this.testController.items.delete(`DiscoveryError:${workspacePath}`); } - if (rawTestData.tests) { + if (rawTestData.tests || rawTestData.tests === null) { // if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not. // parse and insert test data. From 358635da33750e05af3edb1497f601fd9a71a27b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 2 Aug 2023 20:27:00 +1000 Subject: [PATCH 201/336] Remove old and unused API for Jupyter Ext (#21731) We have not used any of this API for a while now, hence its safe to remove these. Will be removing more soon. --- src/client/common/installer/condaInstaller.ts | 13 +- .../common/installer/moduleInstaller.ts | 12 -- src/client/common/installer/productNames.ts | 6 - src/client/common/installer/productService.ts | 6 - src/client/common/types.ts | 6 - src/client/jupyter/jupyterIntegration.ts | 144 +---------------- ...eractiveWindowMiddlewareAddon.unit.test.ts | 12 +- .../installer/productInstaller.unit.test.ts | 149 ------------------ 8 files changed, 6 insertions(+), 342 deletions(-) diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts index a20b35e0f110..fbb3dcf183ef 100644 --- a/src/client/common/installer/condaInstaller.ts +++ b/src/client/common/installer/condaInstaller.ts @@ -88,18 +88,7 @@ export class CondaInstaller extends ModuleInstaller { // Found that using conda-forge is best at packages like tensorboard & ipykernel which seem to get updated first on conda-forge // https://github.com/microsoft/vscode-jupyter/issues/7787 & https://github.com/microsoft/vscode-python/issues/17628 // Do this just for the datascience packages. - if ( - [ - Product.tensorboard, - Product.ipykernel, - Product.pandas, - Product.nbconvert, - Product.jupyter, - Product.notebook, - ] - .map(translateProductToModule) - .includes(moduleName) - ) { + if ([Product.tensorboard].map(translateProductToModule).includes(moduleName)) { args.push('-c', 'conda-forge'); } if (info && info.name) { diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 62160b7e25c9..f70dd937aba9 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -264,18 +264,6 @@ export function translateProductToModule(product: Product): string { return 'unittest'; case Product.bandit: return 'bandit'; - case Product.jupyter: - return 'jupyter'; - case Product.notebook: - return 'notebook'; - case Product.pandas: - return 'pandas'; - case Product.ipykernel: - return 'ipykernel'; - case Product.nbconvert: - return 'nbconvert'; - case Product.kernelspec: - return 'kernelspec'; case Product.tensorboard: return 'tensorboard'; case Product.torchProfilerInstallName: diff --git a/src/client/common/installer/productNames.ts b/src/client/common/installer/productNames.ts index 6474e8a2a514..378fd5a38dba 100644 --- a/src/client/common/installer/productNames.ts +++ b/src/client/common/installer/productNames.ts @@ -19,11 +19,5 @@ ProductNames.set(Product.yapf, 'yapf'); ProductNames.set(Product.tensorboard, 'tensorboard'); ProductNames.set(Product.torchProfilerInstallName, 'torch-tb-profiler'); ProductNames.set(Product.torchProfilerImportName, 'torch_tb_profiler'); -ProductNames.set(Product.jupyter, 'jupyter'); -ProductNames.set(Product.notebook, 'notebook'); -ProductNames.set(Product.ipykernel, 'ipykernel'); -ProductNames.set(Product.nbconvert, 'nbconvert'); -ProductNames.set(Product.kernelspec, 'kernelspec'); -ProductNames.set(Product.pandas, 'pandas'); ProductNames.set(Product.pip, 'pip'); ProductNames.set(Product.ensurepip, 'ensurepip'); diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts index 5de130e84d06..26a01e37c3ba 100644 --- a/src/client/common/installer/productService.ts +++ b/src/client/common/installer/productService.ts @@ -25,12 +25,6 @@ export class ProductService implements IProductService { this.ProductTypes.set(Product.autopep8, ProductType.Formatter); this.ProductTypes.set(Product.black, ProductType.Formatter); this.ProductTypes.set(Product.yapf, ProductType.Formatter); - this.ProductTypes.set(Product.jupyter, ProductType.DataScience); - this.ProductTypes.set(Product.notebook, ProductType.DataScience); - this.ProductTypes.set(Product.ipykernel, ProductType.DataScience); - this.ProductTypes.set(Product.nbconvert, ProductType.DataScience); - this.ProductTypes.set(Product.kernelspec, ProductType.DataScience); - this.ProductTypes.set(Product.pandas, ProductType.DataScience); this.ProductTypes.set(Product.tensorboard, ProductType.DataScience); this.ProductTypes.set(Product.torchProfilerInstallName, ProductType.DataScience); this.ProductTypes.set(Product.torchProfilerImportName, ProductType.DataScience); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 3359854f89b7..b48a2daadaa6 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -109,12 +109,6 @@ export enum Product { isort = 15, black = 16, bandit = 17, - jupyter = 18, - ipykernel = 19, - notebook = 20, - kernelspec = 21, - nbconvert = 22, - pandas = 23, tensorboard = 24, torchProfilerInstallName = 25, torchProfilerImportName = 26, diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index 7f660d26e5e4..dbfd1bdf5681 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -6,90 +6,20 @@ import { inject, injectable, named } from 'inversify'; import { dirname } from 'path'; -import { CancellationToken, Event, Extension, Memento, Uri } from 'vscode'; +import { Extension, Memento, Uri } from 'vscode'; import type { SemVer } from 'semver'; import { IContextKeyManager, IWorkspaceService } from '../common/application/types'; import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; -import { InterpreterUri, ModuleInstallFlags } from '../common/installer/types'; -import { - GLOBAL_MEMENTO, - IExtensions, - IInstaller, - IMemento, - InstallerResponse, - Product, - ProductInstallStatus, - Resource, -} from '../common/types'; +import { GLOBAL_MEMENTO, IExtensions, IMemento, Resource } from '../common/types'; import { getDebugpyPackagePath } from '../debugger/extension/adapter/remoteLaunchers'; import { IEnvironmentActivationService } from '../interpreter/activation/types'; import { IInterpreterQuickPickItem, IInterpreterSelector } from '../interpreter/configuration/types'; -import { - IComponentAdapter, - ICondaService, - IInterpreterDisplay, - IInterpreterService, - IInterpreterStatusbarVisibilityFilter, - PythonEnvironmentsChangedEvent, -} from '../interpreter/contracts'; +import { ICondaService, IInterpreterDisplay, IInterpreterStatusbarVisibilityFilter } from '../interpreter/contracts'; import { PythonEnvironment } from '../pythonEnvironments/info'; import { PylanceApi } from '../activation/node/pylanceApi'; import { ExtensionContextKey } from '../common/application/contextKeys'; -/** - * This allows Python extension to update Product enum without breaking Jupyter. - * I.e. we have a strict contract, else using numbers (in enums) is bound to break across products. - */ -enum JupyterProductToInstall { - jupyter = 'jupyter', - ipykernel = 'ipykernel', - notebook = 'notebook', - kernelspec = 'kernelspec', - nbconvert = 'nbconvert', - pandas = 'pandas', - pip = 'pip', -} - -const ProductMapping: { [key in JupyterProductToInstall]: Product } = { - [JupyterProductToInstall.ipykernel]: Product.ipykernel, - [JupyterProductToInstall.jupyter]: Product.jupyter, - [JupyterProductToInstall.kernelspec]: Product.kernelspec, - [JupyterProductToInstall.nbconvert]: Product.nbconvert, - [JupyterProductToInstall.notebook]: Product.notebook, - [JupyterProductToInstall.pandas]: Product.pandas, - [JupyterProductToInstall.pip]: Product.pip, -}; type PythonApiForJupyterExtension = { - /** - * IInterpreterService - */ - onDidChangeInterpreter: Event; - /** - * IInterpreterService - */ - readonly refreshPromise: Promise | undefined; - /** - * IInterpreterService - */ - readonly onDidChangeInterpreters: Event; - /** - * Equivalent to getInterpreters() in IInterpreterService - */ - getKnownInterpreters(resource?: Uri): PythonEnvironment[]; - /** - * @deprecated Use `getKnownInterpreters`, `onDidChangeInterpreters`, and `refreshPromise` instead. - * Equivalent to getAllInterpreters() in IInterpreterService - */ - getInterpreters(resource?: Uri): Promise; - /** - * IInterpreterService - */ - getActiveInterpreter(resource?: Uri): Promise; - /** - * IInterpreterService - */ - getInterpreterDetails(pythonPath: string, resource?: Uri): Promise; - /** * IEnvironmentActivationService */ @@ -98,31 +28,11 @@ type PythonApiForJupyterExtension = { interpreter?: PythonEnvironment, allowExceptions?: boolean, ): Promise; - isMicrosoftStoreInterpreter(pythonPath: string): Promise; - suggestionToQuickPickItem(suggestion: PythonEnvironment, workspaceUri?: Uri | undefined): IInterpreterQuickPickItem; getKnownSuggestions(resource: Resource): IInterpreterQuickPickItem[]; /** * @deprecated Use `getKnownSuggestions` and `suggestionToQuickPickItem` instead. */ getSuggestions(resource: Resource): Promise; - /** - * IInstaller - */ - install( - product: JupyterProductToInstall, - resource?: InterpreterUri, - cancel?: CancellationToken, - reInstallAndUpdate?: boolean, - installPipIfRequired?: boolean, - ): Promise; - /** - * IInstaller - */ - isProductVersionCompatible( - product: Product, - semVerRequirement: string, - resource?: InterpreterUri, - ): Promise; /** * Returns path to where `debugpy` is. In python extension this is `/pythonFiles/lib/python`. */ @@ -140,10 +50,6 @@ type PythonApiForJupyterExtension = { * Returns the conda executable. */ getCondaFile(): Promise; - getEnvironmentActivationShellCommands( - resource: Resource, - interpreter?: PythonEnvironment, - ): Promise; /** * Call to provide a function that the Python extension can call to request the Python @@ -181,13 +87,10 @@ export class JupyterExtensionIntegration { constructor( @inject(IExtensions) private readonly extensions: IExtensions, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, - @inject(IInstaller) private readonly installer: IInstaller, @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, @inject(IInterpreterDisplay) private interpreterDisplay: IInterpreterDisplay, - @inject(IComponentAdapter) private pyenvs: IComponentAdapter, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(ICondaService) private readonly condaService: ICondaService, @inject(IContextKeyManager) private readonly contextManager: IContextKeyManager, @@ -201,54 +104,15 @@ export class JupyterExtensionIntegration { } // Forward python parts jupyterExtensionApi.registerPythonApi({ - onDidChangeInterpreter: this.interpreterService.onDidChangeInterpreter, - getActiveInterpreter: async (resource?: Uri) => this.interpreterService.getActiveInterpreter(resource), - getInterpreterDetails: async (pythonPath: string) => - this.interpreterService.getInterpreterDetails(pythonPath), - refreshPromise: this.interpreterService.refreshPromise, - onDidChangeInterpreters: this.interpreterService.onDidChangeInterpreters, - getKnownInterpreters: (resource: Uri | undefined) => this.pyenvs.getInterpreters(resource), - getInterpreters: async (resource: Uri | undefined) => this.interpreterService.getAllInterpreters(resource), getActivatedEnvironmentVariables: async ( resource: Resource, interpreter?: PythonEnvironment, allowExceptions?: boolean, ) => this.envActivation.getActivatedEnvironmentVariables(resource, interpreter, allowExceptions), - isMicrosoftStoreInterpreter: async (pythonPath: string): Promise => - this.pyenvs.isMicrosoftStoreInterpreter(pythonPath), getSuggestions: async (resource: Resource): Promise => this.interpreterSelector.getAllSuggestions(resource), getKnownSuggestions: (resource: Resource): IInterpreterQuickPickItem[] => this.interpreterSelector.getSuggestions(resource), - suggestionToQuickPickItem: ( - suggestion: PythonEnvironment, - workspaceUri?: Uri | undefined, - ): IInterpreterQuickPickItem => - this.interpreterSelector.suggestionToQuickPickItem(suggestion, workspaceUri), - install: async ( - product: JupyterProductToInstall, - resource?: InterpreterUri, - cancel?: CancellationToken, - reInstallAndUpdate?: boolean, - installPipIfRequired?: boolean, - ): Promise => { - let flags = - reInstallAndUpdate === true - ? ModuleInstallFlags.updateDependencies | ModuleInstallFlags.reInstall - : undefined; - if (installPipIfRequired === true) { - flags = flags - ? flags | ModuleInstallFlags.installPipIfRequired - : ModuleInstallFlags.installPipIfRequired; - } - return this.installer.install(ProductMapping[product], resource, cancel, flags); - }, - isProductVersionCompatible: async ( - product: Product, - semVerRequirement: string, - resource?: InterpreterUri, - ): Promise => - this.installer.isProductVersionCompatible(product, semVerRequirement, resource), getDebuggerPath: async () => dirname(getDebugpyPackagePath()), getInterpreterPathSelectedForJupyterServer: () => this.globalState.get('INTERPRETER_PATH_SELECTED_FOR_JUPYTER_SERVER'), @@ -257,8 +121,6 @@ export class JupyterExtensionIntegration { ), getCondaFile: () => this.condaService.getCondaFile(), getCondaVersion: () => this.condaService.getCondaVersion(), - getEnvironmentActivationShellCommands: (resource: Resource, interpreter?: PythonEnvironment) => - this.envActivation.getEnvironmentActivationShellCommands(resource, interpreter), registerJupyterPythonPathFunction: (func: (uri: Uri) => Promise) => this.registerJupyterPythonPathFunction(func), registerGetNotebookUriForTextDocumentUriFunction: (func: (textDocumentUri: Uri) => Uri | undefined) => diff --git a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts b/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts index 256e57a5d724..32d9198ef7ba 100644 --- a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts +++ b/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts @@ -10,13 +10,8 @@ import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; import { LanguageClient } from 'vscode-languageclient/node'; import { LspInteractiveWindowMiddlewareAddon } from '../../../client/activation/node/lspInteractiveWindowMiddlewareAddon'; import { JupyterExtensionIntegration } from '../../../client/jupyter/jupyterIntegration'; -import { IExtensions, IInstaller } from '../../../client/common/types'; -import { - IComponentAdapter, - ICondaService, - IInterpreterDisplay, - IInterpreterService, -} from '../../../client/interpreter/contracts'; +import { IExtensions } from '../../../client/common/types'; +import { ICondaService, IInterpreterDisplay } from '../../../client/interpreter/contracts'; import { IInterpreterSelector } from '../../../client/interpreter/configuration/types'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IContextKeyManager, IWorkspaceService } from '../../../client/common/application/types'; @@ -32,13 +27,10 @@ suite('Pylance Language Server - Interactive Window LSP Notebooks', () => { languageClient = instance(languageClientMock); jupyterApi = new JupyterExtensionIntegration( mock(), - mock(), mock(), - mock(), mock(), new MockMemento(), mock(), - mock(), mock(), mock(), mock(), diff --git a/src/test/common/installer/productInstaller.unit.test.ts b/src/test/common/installer/productInstaller.unit.test.ts index ed1be158c0aa..66e0cc005870 100644 --- a/src/test/common/installer/productInstaller.unit.test.ts +++ b/src/test/common/installer/productInstaller.unit.test.ts @@ -57,155 +57,6 @@ suite('DataScienceInstaller install', async () => { // noop }); - test('Requires interpreter Uri', async () => { - let threwUp = false; - try { - await dataScienceInstaller.install(Product.ipykernel); - } catch (ex) { - threwUp = true; - } - expect(threwUp).to.equal(true, 'Should raise exception'); - }); - - test('Will ignore with no installer modules', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.VirtualEnv, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([])); - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Ignore, 'Should be InstallerResponse.Ignore'); - }); - - test('Will invoke conda for conda environments', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.Conda, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Conda); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - - test('Will invoke pip by default', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.VirtualEnv, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Pip); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - - test('Will invoke poetry', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.Poetry, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Poetry); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - - test('Will invoke pipenv', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.Pipenv, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Pipenv); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - test('Will invoke pip for pytorch with conda environment', async () => { // See https://github.com/microsoft/vscode-jupyter/issues/5034 const testEnvironment: PythonEnvironment = { From ef16727d0b26238b8606bab3e821720f159bcdda Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:36:26 -0700 Subject: [PATCH 202/336] Clean up smoke test requirement (#21729) Cleaning up smoke test dependency: See if all Github action test pass with removing the smoke test requirement file content. Checked one by one, and came to see removing all doesn't seem to have impact on the outcome of running smoke test.(Seems to have no difference in smoke test result outcome when ran with "Run and Debug" in VS Code with smoke-test option selected). Also got rid of below, after checking smoke test correctly passing after removal of smoke-test-requirement.txt content: ![Screenshot 2023-08-01 at 2 57 45 PM](https://github.com/microsoft/vscode-python/assets/62267334/45d404de-74dd-45a5-885b-71a25ef16ad7) Resolve: #21496 --- .github/actions/smoke-tests/action.yml | 6 ------ build/smoke-test-requirements.txt | 6 ------ scripts/onCreateCommand.sh | 1 - 3 files changed, 13 deletions(-) delete mode 100644 build/smoke-test-requirements.txt diff --git a/.github/actions/smoke-tests/action.yml b/.github/actions/smoke-tests/action.yml index 9ad6e87cdd26..b2d002050433 100644 --- a/.github/actions/smoke-tests/action.yml +++ b/.github/actions/smoke-tests/action.yml @@ -26,7 +26,6 @@ runs: cache-dependency-path: | build/test-requirements.txt requirements.txt - build/smoke-test-requirements.txt - name: Install dependencies (npm ci) run: npm ci --prefer-offline @@ -43,11 +42,6 @@ runs: python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --implementation py --no-deps --upgrade --pre debugpy shell: bash - - name: pip install smoke test requirements - run: | - python -m pip install --upgrade -r build/smoke-test-requirements.txt - shell: bash - # Bits from the VSIX are reused by smokeTest.ts to speed things up. - name: Download VSIX uses: actions/download-artifact@v2 diff --git a/build/smoke-test-requirements.txt b/build/smoke-test-requirements.txt deleted file mode 100644 index 7d5ac3da00d9..000000000000 --- a/build/smoke-test-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -# List of requirements for smoke tests (they will attempt to run a kernel) -jupyter -numpy -matplotlib -pandas -livelossplot \ No newline at end of file diff --git a/scripts/onCreateCommand.sh b/scripts/onCreateCommand.sh index e93c74f610b9..a90a5366417d 100644 --- a/scripts/onCreateCommand.sh +++ b/scripts/onCreateCommand.sh @@ -29,7 +29,6 @@ source /workspaces/vscode-python/.venv/bin/activate npx gulp installPythonLibs /workspaces/vscode-python/.venv/bin/python -m pip install -r build/test-requirements.txt -/workspaces/vscode-python/.venv/bin/python -m pip install -r build/smoke-test-requirements.txt /workspaces/vscode-python/.venv/bin/python -m pip install -r build/functional-test-requirements.txt # Below will crash codespace From 84bbff9c7b6f0a2835829bba414ca263bd66de20 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 2 Aug 2023 15:08:15 -0700 Subject: [PATCH 203/336] add cwd for debugging (#21668) fixes https://github.com/microsoft/vscode-python/issues/21648#issuecomment-1638551934 --- src/client/testing/common/debugLauncher.ts | 16 ++++++- src/client/testing/common/helpers.ts | 37 ++++++++++++++ .../testing/common/debugLauncher.unit.test.ts | 8 ++++ src/test/testing/common/helpers.unit.test.ts | 48 +++++++++++++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 src/client/testing/common/helpers.ts create mode 100644 src/test/testing/common/helpers.unit.test.ts diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index dcf23c0478db..63e2a4543beb 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -17,6 +17,7 @@ import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis import { showErrorMessage } from '../../common/vscodeApis/windowApis'; import { createDeferred } from '../../common/utils/async'; import { pythonTestAdapterRewriteEnabled } from '../testController/common/utils'; +import { addPathToPythonpath } from './helpers'; @injectable() export class DebugLauncher implements ITestDebugLauncher { @@ -223,8 +224,19 @@ export class DebugLauncher implements ITestDebugLauncher { `Missing value for debug setup, both port and uuid need to be defined. port: "${options.pytestPort}" uuid: "${options.pytestUUID}"`, ); } - const pluginPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); - launchArgs.env.PYTHONPATH = pluginPath; + } + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + // check if PYTHONPATH is already set in the environment variables + if (launchArgs.env) { + const additionalPythonPath = [pluginPath]; + if (launchArgs.cwd) { + additionalPythonPath.push(launchArgs.cwd); + } else if (options.cwd) { + additionalPythonPath.push(options.cwd); + } + // add the plugin path or cwd to PYTHONPATH if it is not already there using the following function + // this function will handle if PYTHONPATH is undefined + addPathToPythonpath(additionalPythonPath, launchArgs.env.PYTHONPATH); } // Clear out purpose so we can detect if the configuration was used to diff --git a/src/client/testing/common/helpers.ts b/src/client/testing/common/helpers.ts new file mode 100644 index 000000000000..021849277b33 --- /dev/null +++ b/src/client/testing/common/helpers.ts @@ -0,0 +1,37 @@ +import * as path from 'path'; + +/** + * This function normalizes the provided paths and the existing paths in PYTHONPATH, + * adds the provided paths to PYTHONPATH if they're not already present, + * and then returns the updated PYTHONPATH. + * + * @param newPaths - An array of paths to be added to PYTHONPATH + * @param launchPythonPath - The initial PYTHONPATH + * @returns The updated PYTHONPATH + */ +export function addPathToPythonpath(newPaths: string[], launchPythonPath: string | undefined): string { + // Split PYTHONPATH into array of paths if it exists + let paths: string[]; + if (!launchPythonPath) { + paths = []; + } else { + paths = launchPythonPath.split(path.delimiter); + } + + // Normalize each path in the existing PYTHONPATH + paths = paths.map((p) => path.normalize(p)); + + // Normalize each new path and add it to PYTHONPATH if it's not already present + newPaths.forEach((newPath) => { + const normalizedNewPath: string = path.normalize(newPath); + + if (!paths.includes(normalizedNewPath)) { + paths.push(normalizedNewPath); + } + }); + + // Join the paths with ':' to create the updated PYTHONPATH + const updatedPythonPath: string = paths.join(path.delimiter); + + return updatedPythonPath; +} diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index 4712c9b6136a..bbb65f0b2e2a 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -215,6 +215,9 @@ suite('Unit Tests - Debug Launcher', () => { if (!expected.cwd) { expected.cwd = workspaceFolders[0].uri.fsPath; } + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + const pythonPath = `${pluginPath}${path.delimiter}${expected.cwd}`; + expected.env.PYTHONPATH = pythonPath; // added by LaunchConfigurationResolver: if (!expected.python) { @@ -342,6 +345,10 @@ suite('Unit Tests - Debug Launcher', () => { }; const expected = getDefaultDebugConfig(); expected.cwd = 'path/to/settings/cwd'; + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + const pythonPath = `${pluginPath}${path.delimiter}${expected.cwd}`; + expected.env.PYTHONPATH = pythonPath; + setupSuccess(options, 'unittest', expected); await debugLauncher.launchDebugger(options); @@ -366,6 +373,7 @@ suite('Unit Tests - Debug Launcher', () => { console: 'integratedTerminal', cwd: 'some/dir', env: { + PYTHONPATH: 'one/two/three', SPAM: 'EGGS', }, envFile: 'some/dir/.env', diff --git a/src/test/testing/common/helpers.unit.test.ts b/src/test/testing/common/helpers.unit.test.ts new file mode 100644 index 000000000000..441b257d4d0e --- /dev/null +++ b/src/test/testing/common/helpers.unit.test.ts @@ -0,0 +1,48 @@ +import * as path from 'path'; +import * as assert from 'assert'; +import { addPathToPythonpath } from '../../../client/testing/common/helpers'; + +suite('Unit Tests - Test Helpers', () => { + const newPaths = [path.join('path', 'to', 'new')]; + test('addPathToPythonpath handles undefined path', async () => { + const launchPythonPath = undefined; + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + assert.equal(actualPath, path.join('path', 'to', 'new')); + }); + test('addPathToPythonpath adds path if it does not exist in the python path', async () => { + const launchPythonPath = path.join('random', 'existing', 'pythonpath'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = + path.join('random', 'existing', 'pythonpath') + path.delimiter + path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath does not add to python path if the given python path already contains the path', async () => { + const launchPythonPath = path.join('path', 'to', 'new'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath correctly normalizes both existing and new paths', async () => { + const newerPaths = [path.join('path', 'to', '/', 'new')]; + const launchPythonPath = path.join('path', 'to', '..', 'old'); + const actualPath = addPathToPythonpath(newerPaths, launchPythonPath); + const expectedPath = path.join('path', 'old') + path.delimiter + path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath splits pythonpath then rejoins it', async () => { + const launchPythonPath = + path.join('path', 'to', 'new') + + path.delimiter + + path.join('path', 'to', 'old') + + path.delimiter + + path.join('path', 'to', 'random'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = + path.join('path', 'to', 'new') + + path.delimiter + + path.join('path', 'to', 'old') + + path.delimiter + + path.join('path', 'to', 'random'); + assert.equal(actualPath, expectedPath); + }); +}); From ca4dfd4254ea161f3f409bd95c2af79703e6da36 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 2 Aug 2023 15:14:47 -0700 Subject: [PATCH 204/336] update tests only on save with more files excluded (#21741) fixes https://github.com/microsoft/vscode-python/issues/21014 and https://github.com/microsoft/vscode-python/issues/21061 --- .../testing/testController/controller.ts | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index eff333a4cdd9..1550323ff8f8 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -15,6 +15,7 @@ import { CancellationTokenSource, Uri, EventEmitter, + TextDocument, } from 'vscode'; import { IExtensionSingleActivationService } from '../../activation/types'; import { ICommandManager, IWorkspaceService } from '../../common/application/types'; @@ -48,6 +49,7 @@ import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { ITestDebugLauncher } from '../common/types'; import { IServiceContainer } from '../../ioc/types'; import { PythonResultResolver } from './common/resultResolver'; +import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; @@ -209,7 +211,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc if (settings.testing.autoTestDiscoverOnSaveEnabled) { traceVerbose(`Testing: Setting up watcher for ${workspace.uri.fsPath}`); this.watchForSettingsChanges(workspace); - this.watchForTestContentChanges(workspace); + this.watchForTestContentChangeOnSave(); } }); } @@ -493,12 +495,23 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.disposables.push(watcher); this.disposables.push( - watcher.onDidChange((uri) => { - traceVerbose(`Testing: Trigger refresh after change in ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - this.refreshData.trigger(uri, false); + onDidSaveTextDocument(async (doc: TextDocument) => { + const file = doc.fileName; + // refresh on any settings file save + if ( + file.includes('settings.json') || + file.includes('pytest.ini') || + file.includes('setup.cfg') || + file.includes('pyproject.toml') + ) { + traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(doc.uri, false); + } }), ); + /* Keep both watchers for create and delete since config files can change test behavior without content + due to their impact on pythonPath. */ this.disposables.push( watcher.onDidCreate((uri) => { traceVerbose(`Testing: Trigger refresh after creating ${uri.fsPath}`); @@ -515,31 +528,14 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); } - private watchForTestContentChanges(workspace: WorkspaceFolder): void { - const pattern = new RelativePattern(workspace, '**/*.py'); - const watcher = this.workspaceService.createFileSystemWatcher(pattern); - this.disposables.push(watcher); - - this.disposables.push( - watcher.onDidChange((uri) => { - traceVerbose(`Testing: Trigger refresh after change in ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - // We want to invalidate tests for code change - this.refreshData.trigger(uri, true); - }), - ); - this.disposables.push( - watcher.onDidCreate((uri) => { - traceVerbose(`Testing: Trigger refresh after creating ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - this.refreshData.trigger(uri, false); - }), - ); + private watchForTestContentChangeOnSave(): void { this.disposables.push( - watcher.onDidDelete((uri) => { - traceVerbose(`Testing: Trigger refresh after deleting in ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - this.refreshData.trigger(uri, false); + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.fileName.endsWith('.py')) { + traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(doc.uri, false); + } }), ); } From dff25d4362389014a560f3dd6aaf61a3d814d2a1 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 2 Aug 2023 17:04:10 -0700 Subject: [PATCH 205/336] revert absolute test-ids (#21742) seeing a substantial error where test discovery is broken. Reverting this commit seems to be the temporary fix until I can diagnose the real problem. commit it is reverting: https://github.com/microsoft/vscode-python/pull/21682 --- .../pytestadapter/.data/root/tests/pytest.ini | 0 .../pytestadapter/.data/root/tests/test_a.py | 6 - .../pytestadapter/.data/root/tests/test_b.py | 6 - .../expected_discovery_test_output.py | 516 +++++------------- .../expected_execution_test_output.py | 362 +++--------- pythonFiles/tests/pytestadapter/helpers.py | 16 +- .../tests/pytestadapter/test_discovery.py | 52 +- .../tests/pytestadapter/test_execution.py | 46 +- pythonFiles/vscode_pytest/__init__.py | 58 +- 9 files changed, 250 insertions(+), 812 deletions(-) delete mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini delete mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py delete mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini b/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py deleted file mode 100644 index 3ec3dd9626cb..000000000000 --- a/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def test_a_function(): # test_marker--test_a_function - assert True diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py deleted file mode 100644 index 0d3148641f85..000000000000 --- a/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def test_b_function(): # test_marker--test_b_function - assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 2b2c07ab8ea7..91c1453dfc77 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -1,7 +1,6 @@ import os - -from .helpers import TEST_DATA_PATH, find_test_line_number, get_absolute_test_id +from .helpers import TEST_DATA_PATH, find_test_line_number # This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. @@ -19,7 +18,7 @@ # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function -simple_test_file_path = TEST_DATA_PATH / "simple_pytest.py" +simple_test_file_path = os.fspath(TEST_DATA_PATH / "simple_pytest.py") simple_discovery_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -27,24 +26,20 @@ "children": [ { "name": "simple_pytest.py", - "path": os.fspath(simple_test_file_path), + "path": simple_test_file_path, "type_": "file", - "id_": os.fspath(simple_test_file_path), + "id_": simple_test_file_path, "children": [ { "name": "test_function", - "path": os.fspath(simple_test_file_path), + "path": simple_test_file_path, "lineno": find_test_line_number( "test_function", simple_test_file_path, ), "type_": "test", - "id_": get_absolute_test_id( - "simple_pytest.py::test_function", simple_test_file_path - ), - "runID": get_absolute_test_id( - "simple_pytest.py::test_function", simple_test_file_path - ), + "id_": "simple_pytest.py::test_function", + "runID": "simple_pytest.py::test_function", } ], } @@ -57,7 +52,7 @@ # ├── TestExample # │ └── test_true_unittest # └── test_true_pytest -unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" +unit_pytest_same_file_path = os.fspath(TEST_DATA_PATH / "unittest_pytest_same_file.py") unit_pytest_same_file_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -65,51 +60,39 @@ "children": [ { "name": "unittest_pytest_same_file.py", - "path": os.fspath(unit_pytest_same_file_path), + "path": unit_pytest_same_file_path, "type_": "file", - "id_": os.fspath(unit_pytest_same_file_path), + "id_": unit_pytest_same_file_path, "children": [ { "name": "TestExample", - "path": os.fspath(unit_pytest_same_file_path), + "path": unit_pytest_same_file_path, "type_": "class", "children": [ { "name": "test_true_unittest", - "path": os.fspath(unit_pytest_same_file_path), + "path": unit_pytest_same_file_path, "lineno": find_test_line_number( "test_true_unittest", - os.fspath(unit_pytest_same_file_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_pytest_same_file.py::TestExample::test_true_unittest", - unit_pytest_same_file_path, - ), - "runID": get_absolute_test_id( - "unittest_pytest_same_file.py::TestExample::test_true_unittest", unit_pytest_same_file_path, ), + "type_": "test", + "id_": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "runID": "unittest_pytest_same_file.py::TestExample::test_true_unittest", } ], "id_": "unittest_pytest_same_file.py::TestExample", }, { "name": "test_true_pytest", - "path": os.fspath(unit_pytest_same_file_path), + "path": unit_pytest_same_file_path, "lineno": find_test_line_number( "test_true_pytest", unit_pytest_same_file_path, ), "type_": "test", - "id_": get_absolute_test_id( - "unittest_pytest_same_file.py::test_true_pytest", - unit_pytest_same_file_path, - ), - "runID": get_absolute_test_id( - "unittest_pytest_same_file.py::test_true_pytest", - unit_pytest_same_file_path, - ), + "id_": "unittest_pytest_same_file.py::test_true_pytest", + "runID": "unittest_pytest_same_file.py::test_true_pytest", }, ], } @@ -141,9 +124,9 @@ # └── test_subtract_positive_numbers # │ └── TestDuplicateFunction # │ └── test_dup_s -unittest_folder_path = TEST_DATA_PATH / "unittest_folder" -test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" -test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +unittest_folder_path = os.fspath(TEST_DATA_PATH / "unittest_folder") +test_add_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_add.py") +test_subtract_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_subtract.py") unittest_folder_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -151,79 +134,61 @@ "children": [ { "name": "unittest_folder", - "path": os.fspath(unittest_folder_path), + "path": unittest_folder_path, "type_": "folder", - "id_": os.fspath(unittest_folder_path), + "id_": unittest_folder_path, "children": [ { "name": "test_add.py", - "path": os.fspath(test_add_path), + "path": test_add_path, "type_": "file", - "id_": os.fspath(test_add_path), + "id_": test_add_path, "children": [ { "name": "TestAddFunction", - "path": os.fspath(test_add_path), + "path": test_add_path, "type_": "class", "children": [ { "name": "test_add_negative_numbers", - "path": os.fspath(test_add_path), + "path": test_add_path, "lineno": find_test_line_number( "test_add_negative_numbers", - os.fspath(test_add_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", - test_add_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", test_add_path, ), + "type_": "test", + "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", }, { "name": "test_add_positive_numbers", - "path": os.fspath(test_add_path), + "path": test_add_path, "lineno": find_test_line_number( "test_add_positive_numbers", - os.fspath(test_add_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - test_add_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", test_add_path, ), + "type_": "test", + "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", }, ], "id_": "unittest_folder/test_add.py::TestAddFunction", }, { "name": "TestDuplicateFunction", - "path": os.fspath(test_add_path), + "path": test_add_path, "type_": "class", "children": [ { "name": "test_dup_a", - "path": os.fspath(test_add_path), + "path": test_add_path, "lineno": find_test_line_number( "test_dup_a", - os.fspath(test_add_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", - test_add_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", test_add_path, ), + "type_": "test", + "id_": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + "runID": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", }, ], "id_": "unittest_folder/test_add.py::TestDuplicateFunction", @@ -232,73 +197,55 @@ }, { "name": "test_subtract.py", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "type_": "file", - "id_": os.fspath(test_subtract_path), + "id_": test_subtract_path, "children": [ { "name": "TestSubtractFunction", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "type_": "class", "children": [ { "name": "test_subtract_negative_numbers", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "lineno": find_test_line_number( "test_subtract_negative_numbers", - os.fspath(test_subtract_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", - test_subtract_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", test_subtract_path, ), + "type_": "test", + "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", }, { "name": "test_subtract_positive_numbers", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "lineno": find_test_line_number( "test_subtract_positive_numbers", - os.fspath(test_subtract_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", - test_subtract_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", test_subtract_path, ), + "type_": "test", + "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", }, ], "id_": "unittest_folder/test_subtract.py::TestSubtractFunction", }, { "name": "TestDuplicateFunction", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "type_": "class", "children": [ { "name": "test_dup_s", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "lineno": find_test_line_number( "test_dup_s", - os.fspath(test_subtract_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", - test_subtract_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", test_subtract_path, ), + "type_": "test", + "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + "runID": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", }, ], "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction", @@ -321,23 +268,20 @@ # └── test_bottom_folder.py # └── test_bottom_function_t # └── test_bottom_function_f -dual_level_nested_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" -test_top_folder_path = ( +dual_level_nested_folder_path = os.fspath(TEST_DATA_PATH / "dual_level_nested_folder") +test_top_folder_path = os.fspath( TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" ) - -test_nested_folder_one_path = ( +test_nested_folder_one_path = os.fspath( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" ) - -test_bottom_folder_path = ( +test_bottom_folder_path = os.fspath( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" ) - dual_level_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -345,97 +289,73 @@ "children": [ { "name": "dual_level_nested_folder", - "path": os.fspath(dual_level_nested_folder_path), + "path": dual_level_nested_folder_path, "type_": "folder", - "id_": os.fspath(dual_level_nested_folder_path), + "id_": dual_level_nested_folder_path, "children": [ { "name": "test_top_folder.py", - "path": os.fspath(test_top_folder_path), + "path": test_top_folder_path, "type_": "file", - "id_": os.fspath(test_top_folder_path), + "id_": test_top_folder_path, "children": [ { "name": "test_top_function_t", - "path": os.fspath(test_top_folder_path), + "path": test_top_folder_path, "lineno": find_test_line_number( "test_top_function_t", test_top_folder_path, ), "type_": "test", - "id_": get_absolute_test_id( - "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - test_top_folder_path, - ), - "runID": get_absolute_test_id( - "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - test_top_folder_path, - ), + "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", }, { "name": "test_top_function_f", - "path": os.fspath(test_top_folder_path), + "path": test_top_folder_path, "lineno": find_test_line_number( "test_top_function_f", test_top_folder_path, ), "type_": "test", - "id_": get_absolute_test_id( - "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - test_top_folder_path, - ), - "runID": get_absolute_test_id( - "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - test_top_folder_path, - ), + "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", }, ], }, { "name": "nested_folder_one", - "path": os.fspath(test_nested_folder_one_path), + "path": test_nested_folder_one_path, "type_": "folder", - "id_": os.fspath(test_nested_folder_one_path), + "id_": test_nested_folder_one_path, "children": [ { "name": "test_bottom_folder.py", - "path": os.fspath(test_bottom_folder_path), + "path": test_bottom_folder_path, "type_": "file", - "id_": os.fspath(test_bottom_folder_path), + "id_": test_bottom_folder_path, "children": [ { "name": "test_bottom_function_t", - "path": os.fspath(test_bottom_folder_path), + "path": test_bottom_folder_path, "lineno": find_test_line_number( "test_bottom_function_t", test_bottom_folder_path, ), "type_": "test", - "id_": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - test_bottom_folder_path, - ), - "runID": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - test_bottom_folder_path, - ), + "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", }, { "name": "test_bottom_function_f", - "path": os.fspath(test_bottom_folder_path), + "path": test_bottom_folder_path, "lineno": find_test_line_number( "test_bottom_function_f", test_bottom_folder_path, ), "type_": "test", - "id_": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - test_bottom_folder_path, - ), - "runID": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - test_bottom_folder_path, - ), + "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", }, ], } @@ -454,10 +374,12 @@ # └── test_nest.py # └── test_function -folder_a_path = TEST_DATA_PATH / "folder_a" -folder_b_path = TEST_DATA_PATH / "folder_a" / "folder_b" -folder_a_nested_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" -test_nest_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +folder_a_path = os.fspath(TEST_DATA_PATH / "folder_a") +folder_b_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b") +folder_a_nested_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a") +test_nest_path = os.fspath( + TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +) double_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -465,44 +387,38 @@ "children": [ { "name": "folder_a", - "path": os.fspath(folder_a_path), + "path": folder_a_path, "type_": "folder", - "id_": os.fspath(folder_a_path), + "id_": folder_a_path, "children": [ { "name": "folder_b", - "path": os.fspath(folder_b_path), + "path": folder_b_path, "type_": "folder", - "id_": os.fspath(folder_b_path), + "id_": folder_b_path, "children": [ { "name": "folder_a", - "path": os.fspath(folder_a_nested_path), + "path": folder_a_nested_path, "type_": "folder", - "id_": os.fspath(folder_a_nested_path), + "id_": folder_a_nested_path, "children": [ { "name": "test_nest.py", - "path": os.fspath(test_nest_path), + "path": test_nest_path, "type_": "file", - "id_": os.fspath(test_nest_path), + "id_": test_nest_path, "children": [ { "name": "test_function", - "path": os.fspath(test_nest_path), + "path": test_nest_path, "lineno": find_test_line_number( "test_function", test_nest_path, ), "type_": "test", - "id_": get_absolute_test_id( - "folder_a/folder_b/folder_a/test_nest.py::test_function", - test_nest_path, - ), - "runID": get_absolute_test_id( - "folder_a/folder_b/folder_a/test_nest.py::test_function", - test_nest_path, - ), + "id_": "folder_a/folder_b/folder_a/test_nest.py::test_function", + "runID": "folder_a/folder_b/folder_a/test_nest.py::test_function", } ], } @@ -522,7 +438,7 @@ # └── [3+5-8] # └── [2+4-6] # └── [6+9-16] -parameterize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" +parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") parametrize_tests_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -530,107 +446,77 @@ "children": [ { "name": "parametrize_tests.py", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "type_": "file", - "id_": os.fspath(parameterize_tests_path), + "id_": parameterize_tests_path, "children": [ { "name": "test_adding", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "type_": "function", "id_": "parametrize_tests.py::test_adding", "children": [ { "name": "[3+5-8]", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "lineno": find_test_line_number( "test_adding[3+5-8]", parameterize_tests_path, ), "type_": "test", - "id_": get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", - parameterize_tests_path, - ), - "runID": get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", - parameterize_tests_path, - ), + "id_": "parametrize_tests.py::test_adding[3+5-8]", + "runID": "parametrize_tests.py::test_adding[3+5-8]", }, { "name": "[2+4-6]", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "lineno": find_test_line_number( "test_adding[2+4-6]", parameterize_tests_path, ), "type_": "test", - "id_": get_absolute_test_id( - "parametrize_tests.py::test_adding[2+4-6]", - parameterize_tests_path, - ), - "runID": get_absolute_test_id( - "parametrize_tests.py::test_adding[2+4-6]", - parameterize_tests_path, - ), + "id_": "parametrize_tests.py::test_adding[2+4-6]", + "runID": "parametrize_tests.py::test_adding[2+4-6]", }, { "name": "[6+9-16]", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "lineno": find_test_line_number( "test_adding[6+9-16]", parameterize_tests_path, ), "type_": "test", - "id_": get_absolute_test_id( - "parametrize_tests.py::test_adding[6+9-16]", - parameterize_tests_path, - ), - "runID": get_absolute_test_id( - "parametrize_tests.py::test_adding[6+9-16]", - parameterize_tests_path, - ), + "id_": "parametrize_tests.py::test_adding[6+9-16]", + "runID": "parametrize_tests.py::test_adding[6+9-16]", }, ], }, { "name": "test_under_ten", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "type_": "function", "children": [ { "name": "[1]", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "lineno": find_test_line_number( "test_under_ten[1]", parameterize_tests_path, ), "type_": "test", - "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", - parameterize_tests_path, - ), - "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", - parameterize_tests_path, - ), + "id_": "parametrize_tests.py::test_under_ten[1]", + "runID": "parametrize_tests.py::test_under_ten[1]", }, { "name": "[2]", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "lineno": find_test_line_number( "test_under_ten[2]", parameterize_tests_path, ), "type_": "test", - "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", - parameterize_tests_path, - ), - "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", - parameterize_tests_path, - ), + "id_": "parametrize_tests.py::test_under_ten[2]", + "runID": "parametrize_tests.py::test_under_ten[2]", }, ], "id_": "parametrize_tests.py::test_under_ten", @@ -643,7 +529,7 @@ # This is the expected output for the text_docstring.txt tests. # └── text_docstring.txt -text_docstring_path = TEST_DATA_PATH / "text_docstring.txt" +text_docstring_path = os.fspath(TEST_DATA_PATH / "text_docstring.txt") doctest_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -651,24 +537,20 @@ "children": [ { "name": "text_docstring.txt", - "path": os.fspath(text_docstring_path), + "path": text_docstring_path, "type_": "file", - "id_": os.fspath(text_docstring_path), + "id_": text_docstring_path, "children": [ { "name": "text_docstring.txt", - "path": os.fspath(text_docstring_path), + "path": text_docstring_path, "lineno": find_test_line_number( "text_docstring.txt", - os.fspath(text_docstring_path), + text_docstring_path, ), "type_": "test", - "id_": get_absolute_test_id( - "text_docstring.txt::text_docstring.txt", text_docstring_path - ), - "runID": get_absolute_test_id( - "text_docstring.txt::text_docstring.txt", text_docstring_path - ), + "id_": "text_docstring.txt::text_docstring.txt", + "runID": "text_docstring.txt::text_docstring.txt", } ], } @@ -688,8 +570,8 @@ # └── [1] # └── [2] # └── [3] -param1_path = TEST_DATA_PATH / "param_same_name" / "test_param1.py" -param2_path = TEST_DATA_PATH / "param_same_name" / "test_param2.py" +param1_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param1.py") +param2_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param2.py") param_same_name_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -703,56 +585,38 @@ "children": [ { "name": "test_param1.py", - "path": os.fspath(param1_path), + "path": param1_path, "type_": "file", - "id_": os.fspath(param1_path), + "id_": param1_path, "children": [ { "name": "test_odd_even", - "path": os.fspath(param1_path), + "path": param1_path, "type_": "function", "children": [ { "name": "[a]", - "path": os.fspath(param1_path), + "path": param1_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[a]", - param1_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[a]", - param1_path, - ), + "id_": "param_same_name/test_param1.py::test_odd_even[a]", + "runID": "param_same_name/test_param1.py::test_odd_even[a]", }, { "name": "[b]", - "path": os.fspath(param1_path), + "path": param1_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[b]", - param1_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[b]", - param1_path, - ), + "id_": "param_same_name/test_param1.py::test_odd_even[b]", + "runID": "param_same_name/test_param1.py::test_odd_even[b]", }, { "name": "[c]", - "path": os.fspath(param1_path), + "path": param1_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[c]", - param1_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[c]", - param1_path, - ), + "id_": "param_same_name/test_param1.py::test_odd_even[c]", + "runID": "param_same_name/test_param1.py::test_odd_even[c]", }, ], "id_": "param_same_name/test_param1.py::test_odd_even", @@ -761,56 +625,38 @@ }, { "name": "test_param2.py", - "path": os.fspath(param2_path), + "path": param2_path, "type_": "file", - "id_": os.fspath(param2_path), + "id_": param2_path, "children": [ { "name": "test_odd_even", - "path": os.fspath(param2_path), + "path": param2_path, "type_": "function", "children": [ { "name": "[1]", - "path": os.fspath(param2_path), + "path": param2_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[1]", - param2_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[1]", - param2_path, - ), + "id_": "param_same_name/test_param2.py::test_odd_even[1]", + "runID": "param_same_name/test_param2.py::test_odd_even[1]", }, { "name": "[2]", - "path": os.fspath(param2_path), + "path": param2_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[2]", - param2_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[2]", - param2_path, - ), + "id_": "param_same_name/test_param2.py::test_odd_even[2]", + "runID": "param_same_name/test_param2.py::test_odd_even[2]", }, { "name": "[3]", - "path": os.fspath(param2_path), + "path": param2_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[3]", - param2_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[3]", - param2_path, - ), + "id_": "param_same_name/test_param2.py::test_odd_even[3]", + "runID": "param_same_name/test_param2.py::test_odd_even[3]", }, ], "id_": "param_same_name/test_param2.py::test_odd_even", @@ -822,67 +668,3 @@ ], "id_": TEST_DATA_PATH_STR, } - -tests_path = TEST_DATA_PATH / "root" / "tests" -tests_a_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" -tests_b_path = TEST_DATA_PATH / "root" / "tests" / "test_b.py" -# This is the expected output for the root folder tests. -# └── tests -# └── test_a.py -# └── test_a_function -# └── test_b.py -# └── test_b_function -root_with_config_expected_output = { - "name": "tests", - "path": os.fspath(tests_path), - "type_": "folder", - "children": [ - { - "name": "test_a.py", - "path": os.fspath(tests_a_path), - "type_": "file", - "id_": os.fspath(tests_a_path), - "children": [ - { - "name": "test_a_function", - "path": os.fspath(os.path.join(tests_path, "test_a.py")), - "lineno": find_test_line_number( - "test_a_function", - os.path.join(tests_path, "test_a.py"), - ), - "type_": "test", - "id_": get_absolute_test_id( - "tests/test_a.py::test_a_function", tests_a_path - ), - "runID": get_absolute_test_id( - "tests/test_a.py::test_a_function", tests_a_path - ), - } - ], - }, - { - "name": "test_b.py", - "path": os.fspath(tests_b_path), - "type_": "file", - "id_": os.fspath(tests_b_path), - "children": [ - { - "name": "test_b_function", - "path": os.fspath(os.path.join(tests_path, "test_b.py")), - "lineno": find_test_line_number( - "test_b_function", - os.path.join(tests_path, "test_b.py"), - ), - "type_": "test", - "id_": get_absolute_test_id( - "tests/test_b.py::test_b_function", tests_b_path - ), - "runID": get_absolute_test_id( - "tests/test_b.py::test_b_function", tests_b_path - ), - } - ], - }, - ], - "id_": os.fspath(tests_path), -} diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index 0a7e737dfc0e..fe1d40a55b43 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -6,7 +6,6 @@ SUCCESS = "success" FAILURE = "failure" TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" -from .helpers import TEST_DATA_PATH, get_absolute_test_id # This is the expected output for the unittest_folder execute tests # └── unittest_folder @@ -18,52 +17,30 @@ # └── TestSubtractFunction # ├── test_subtract_negative_numbers: failure # └── test_subtract_positive_numbers: success -test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" -test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" uf_execution_expected_output = { - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path - ), + f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ), + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", - test_subtract_path, - ): { - "test": get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", - test_subtract_path, - ), + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers": { + "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", "outcome": FAILURE, "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", - test_subtract_path, - ): { - "test": get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", - test_subtract_path, - ), + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers": { + "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, @@ -78,26 +55,16 @@ # │ └── TestAddFunction # │ ├── test_add_negative_numbers: success # │ └── test_add_positive_numbers: success -test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" - uf_single_file_expected_output = { - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path - ), + f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ), + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, @@ -105,24 +72,19 @@ }, } - # This is the expected output for the unittest_folder execute only signle method # └── unittest_folder # ├── test_add.py # │ └── TestAddFunction # │ └── test_add_positive_numbers: success uf_single_method_execution_expected_output = { - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ), + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, - }, + } } # This is the expected output for the unittest_folder tests run where two tests @@ -134,28 +96,18 @@ # └── test_subtract.py # └── TestSubtractFunction # └── test_subtract_positive_numbers: success -test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" -test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" - uf_non_adjacent_tests_execution_expected_output = { - get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", test_subtract_path - ): { - "test": get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", - test_subtract_path, - ), + TEST_SUBTRACT_FUNCTION + + "test_subtract_positive_numbers": { + "test": TEST_SUBTRACT_FUNCTION + "test_subtract_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ), + TEST_ADD_FUNCTION + + "test_add_positive_numbers": { + "test": TEST_ADD_FUNCTION + "test_add_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, @@ -163,15 +115,12 @@ }, } - # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function: success -simple_pytest_path = TEST_DATA_PATH / "unittest_folder" / "simple_pytest.py" - simple_execution_pytest_expected_output = { - get_absolute_test_id("test_function", simple_pytest_path): { - "test": get_absolute_test_id("test_function", simple_pytest_path), + "simple_pytest.py::test_function": { + "test": "simple_pytest.py::test_function", "outcome": "success", "message": None, "traceback": None, @@ -179,34 +128,21 @@ } } - # This is the expected output for the unittest_pytest_same_file.py file. # ├── unittest_pytest_same_file.py # ├── TestExample # │ └── test_true_unittest: success # └── test_true_pytest: success -unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" unit_pytest_same_file_execution_expected_output = { - get_absolute_test_id( - "unittest_pytest_same_file.py::TestExample::test_true_unittest", - unit_pytest_same_file_path, - ): { - "test": get_absolute_test_id( - "unittest_pytest_same_file.py::TestExample::test_true_unittest", - unit_pytest_same_file_path, - ), + "unittest_pytest_same_file.py::TestExample::test_true_unittest": { + "test": "unittest_pytest_same_file.py::TestExample::test_true_unittest", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "unittest_pytest_same_file.py::test_true_pytest", unit_pytest_same_file_path - ): { - "test": get_absolute_test_id( - "unittest_pytest_same_file.py::test_true_pytest", - unit_pytest_same_file_path, - ), + "unittest_pytest_same_file.py::test_true_pytest": { + "test": "unittest_pytest_same_file.py::test_true_pytest", "outcome": "success", "message": None, "traceback": None, @@ -218,15 +154,9 @@ # └── error_raise_exception.py # ├── TestSomething # │ └── test_a: failure -error_raised_exception_path = TEST_DATA_PATH / "error_raise_exception.py" error_raised_exception_execution_expected_output = { - get_absolute_test_id( - "error_raise_exception.py::TestSomething::test_a", error_raised_exception_path - ): { - "test": get_absolute_test_id( - "error_raise_exception.py::TestSomething::test_a", - error_raised_exception_path, - ), + "error_raise_exception.py::TestSomething::test_a": { + "test": "error_raise_exception.py::TestSomething::test_a", "outcome": "error", "message": "ERROR MESSAGE", "traceback": "TRACEBACK", @@ -242,60 +172,44 @@ # ├── TestClass # │ └── test_class_function_a: skipped # │ └── test_class_function_b: skipped - -skip_tests_path = TEST_DATA_PATH / "skip_tests.py" skip_tests_execution_expected_output = { - get_absolute_test_id("skip_tests.py::test_something", skip_tests_path): { - "test": get_absolute_test_id("skip_tests.py::test_something", skip_tests_path), + "skip_tests.py::test_something": { + "test": "skip_tests.py::test_something", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path): { - "test": get_absolute_test_id( - "skip_tests.py::test_another_thing", skip_tests_path - ), + "skip_tests.py::test_another_thing": { + "test": "skip_tests.py::test_another_thing", "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path): { - "test": get_absolute_test_id( - "skip_tests.py::test_decorator_thing", skip_tests_path - ), + "skip_tests.py::test_decorator_thing": { + "test": "skip_tests.py::test_decorator_thing", "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path): { - "test": get_absolute_test_id( - "skip_tests.py::test_decorator_thing_2", skip_tests_path - ), + "skip_tests.py::test_decorator_thing_2": { + "test": "skip_tests.py::test_decorator_thing_2", "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "skip_tests.py::TestClass::test_class_function_a", skip_tests_path - ): { - "test": get_absolute_test_id( - "skip_tests.py::TestClass::test_class_function_a", skip_tests_path - ), + "skip_tests.py::TestClass::test_class_function_a": { + "test": "skip_tests.py::TestClass::test_class_function_a", "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "skip_tests.py::TestClass::test_class_function_b", skip_tests_path - ): { - "test": get_absolute_test_id( - "skip_tests.py::TestClass::test_class_function_b", skip_tests_path - ), + "skip_tests.py::TestClass::test_class_function_b": { + "test": "skip_tests.py::TestClass::test_class_function_b", "outcome": "skipped", "message": None, "traceback": None, @@ -313,59 +227,30 @@ # └── test_bottom_folder.py # └── test_bottom_function_t: success # └── test_bottom_function_f: failure -dual_level_nested_folder_top_path = ( - TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" -) -dual_level_nested_folder_bottom_path = ( - TEST_DATA_PATH - / "dual_level_nested_folder" - / "nested_folder_one" - / "test_bottom_folder.py" -) dual_level_nested_folder_execution_expected_output = { - get_absolute_test_id( - "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path - ): { - "test": get_absolute_test_id( - "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path - ), + "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path - ): { - "test": get_absolute_test_id( - "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path - ), + "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - dual_level_nested_folder_bottom_path, - ): { - "test": get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - dual_level_nested_folder_bottom_path, - ), + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - dual_level_nested_folder_bottom_path, - ): { - "test": get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - dual_level_nested_folder_bottom_path, - ), + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -379,59 +264,38 @@ # └── folder_a # └── test_nest.py # └── test_function: success - -nested_folder_path = ( - TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" -) double_nested_folder_expected_execution_output = { - get_absolute_test_id( - "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path - ): { - "test": get_absolute_test_id( - "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path - ), + "folder_a/folder_b/folder_a/test_nest.py::test_function": { + "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", "outcome": "success", "message": None, "traceback": None, "subtest": None, } } + # This is the expected output for the nested_folder tests. # └── parametrize_tests.py # └── test_adding[3+5-8]: success # └── test_adding[2+4-6]: success # └── test_adding[6+9-16]: failure -parametrize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" - parametrize_tests_expected_execution_output = { - get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path - ): { - "test": get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path - ), + "parametrize_tests.py::test_adding[3+5-8]": { + "test": "parametrize_tests.py::test_adding[3+5-8]", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path - ): { - "test": get_absolute_test_id( - "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path - ), + "parametrize_tests.py::test_adding[2+4-6]": { + "test": "parametrize_tests.py::test_adding[2+4-6]", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path - ): { - "test": get_absolute_test_id( - "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path - ), + "parametrize_tests.py::test_adding[6+9-16]": { + "test": "parametrize_tests.py::test_adding[6+9-16]", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -443,12 +307,8 @@ # └── parametrize_tests.py # └── test_adding[3+5-8]: success single_parametrize_tests_expected_execution_output = { - get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path - ): { - "test": get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path - ), + "parametrize_tests.py::test_adding[3+5-8]": { + "test": "parametrize_tests.py::test_adding[3+5-8]", "outcome": "success", "message": None, "traceback": None, @@ -459,12 +319,9 @@ # This is the expected output for the single parameterized tests. # └── text_docstring.txt # └── text_docstring: success -doc_test_path = TEST_DATA_PATH / "text_docstring.txt" doctest_pytest_expected_execution_output = { - get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path): { - "test": get_absolute_test_id( - "text_docstring.txt::text_docstring.txt", doc_test_path - ), + "text_docstring.txt::text_docstring.txt": { + "test": "text_docstring.txt::text_docstring.txt", "outcome": "success", "message": None, "traceback": None, @@ -473,127 +330,68 @@ } # Will run all tests in the cwd that fit the test file naming pattern. -folder_a_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" -dual_level_nested_folder_top_path = ( - TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" -) -dual_level_nested_folder_bottom_path = ( - TEST_DATA_PATH - / "dual_level_nested_folder" - / "nested_folder_one" - / "test_bottom_folder.py" -) -unittest_folder_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" -unittest_folder_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" - no_test_ids_pytest_execution_expected_output = { - get_absolute_test_id("test_function", folder_a_path): { - "test": get_absolute_test_id("test_function", folder_a_path), + "folder_a/folder_b/folder_a/test_nest.py::test_function": { + "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path): { - "test": get_absolute_test_id( - "test_top_function_t", dual_level_nested_folder_top_path - ), + "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path): { - "test": get_absolute_test_id( - "test_top_function_f", dual_level_nested_folder_top_path - ), + "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - get_absolute_test_id( - "test_bottom_function_t", dual_level_nested_folder_bottom_path - ): { - "test": get_absolute_test_id( - "test_bottom_function_t", dual_level_nested_folder_bottom_path - ), + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "test_bottom_function_f", dual_level_nested_folder_bottom_path - ): { - "test": get_absolute_test_id( - "test_bottom_function_f", dual_level_nested_folder_bottom_path - ), + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - get_absolute_test_id( - "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path - ): { - "test": get_absolute_test_id( - "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path - ), + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers": { + "test": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path - ): { - "test": get_absolute_test_id( - "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path - ), + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers": { + "test": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "TestSubtractFunction::test_subtract_negative_numbers", - unittest_folder_subtract_path, - ): { - "test": get_absolute_test_id( - "TestSubtractFunction::test_subtract_negative_numbers", - unittest_folder_subtract_path, - ), + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers": { + "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - get_absolute_test_id( - "TestSubtractFunction::test_subtract_positive_numbers", - unittest_folder_subtract_path, - ): { - "test": get_absolute_test_id( - "TestSubtractFunction::test_subtract_positive_numbers", - unittest_folder_subtract_path, - ), + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers": { + "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, } - -# This is the expected output for the root folder with the config file referenced. -# └── test_a.py -# └── test_a_function: success -test_add_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" -config_file_pytest_expected_execution_output = { - get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path): { - "test": get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path), - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - } -} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index 28feb6282b92..c3e01d52170a 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -16,13 +16,6 @@ from typing_extensions import TypedDict -def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: - split_id = test_id.split("::")[1:] - absolute_test_id = "::".join([str(testPath), *split_id]) - print("absolute path", absolute_test_id) - return absolute_test_id - - def create_server( host: str = "127.0.0.1", port: int = 0, @@ -111,13 +104,6 @@ def process_rpc_json(data: str) -> List[Dict[str, Any]]: def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: - """Run the pytest discovery and return the JSON data from the server.""" - return runner_with_cwd(args, TEST_DATA_PATH) - - -def runner_with_cwd( - args: List[str], path: pathlib.Path -) -> Optional[List[Dict[str, Any]]]: """Run the pytest discovery and return the JSON data from the server.""" process_args: List[str] = [ sys.executable, @@ -148,7 +134,7 @@ def runner_with_cwd( t2 = threading.Thread( target=_run_test_code, - args=(process_args, env, path, completed), + args=(process_args, env, TEST_DATA_PATH, completed), ) t2.start() diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 8d785be27c8b..5288c7ad769e 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -7,7 +7,7 @@ import pytest from . import expected_discovery_test_output -from .helpers import TEST_DATA_PATH, runner, runner_with_cwd +from .helpers import TEST_DATA_PATH, runner def test_import_error(tmp_path): @@ -153,53 +153,3 @@ def test_pytest_collect(file, expected_const): assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) assert actual["tests"] == expected_const - - -def test_pytest_root_dir(): - """ - Test to test pytest discovery with the command line arg --rootdir specified to be a subfolder - of the workspace root. Discovery should succeed and testids should be relative to workspace root. - """ - rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" - actual = runner_with_cwd( - [ - "--collect-only", - rd, - ], - TEST_DATA_PATH / "root", - ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") - assert ( - actual["tests"] - == expected_discovery_test_output.root_with_config_expected_output - ) - - -def test_pytest_config_file(): - """ - Test to test pytest discovery with the command line arg -c with a specified config file which - changes the workspace root. Discovery should succeed and testids should be relative to workspace root. - """ - actual = runner_with_cwd( - [ - "--collect-only", - "-c", - "tests/pytest.ini", - ], - TEST_DATA_PATH / "root", - ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") - assert ( - actual["tests"] - == expected_discovery_test_output.root_with_config_expected_output - ) diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 2be4886c24c1..ffc84955bf54 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -1,56 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json import os import shutil import pytest from tests.pytestadapter import expected_execution_test_output -from .helpers import TEST_DATA_PATH, runner, runner_with_cwd - - -def test_config_file(): - """Test pytest execution when a config file is specified.""" - args = [ - "-c", - "tests/pytest.ini", - str(TEST_DATA_PATH / "root" / "tests" / "test_a.py::test_a_function"), - ] - new_cwd = TEST_DATA_PATH / "root" - actual = runner_with_cwd(args, new_cwd) - expected_const = ( - expected_execution_test_output.config_file_pytest_expected_execution_output - ) - assert actual - assert len(actual) == len(expected_const) - actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const - - -def test_rootdir_specified(): - """Test pytest execution when a --rootdir is specified.""" - rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" - args = [rd, "tests/test_a.py::test_a_function"] - new_cwd = TEST_DATA_PATH / "root" - actual = runner_with_cwd(args, new_cwd) - expected_const = ( - expected_execution_test_output.config_file_pytest_expected_execution_output - ) - assert actual - assert len(actual) == len(expected_const) - actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const +from .helpers import TEST_DATA_PATH, runner def test_syntax_error_execution(tmp_path): diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 49d429662e3a..1ac287a8410a 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -69,7 +69,8 @@ def pytest_exception_interact(node, call, report): """ # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. - # If it is during discovery, then add the error to error logs. + # See if it is during discovery or execution. + # if discovery, then add the error to error logs. if type(report) == pytest.CollectReport: if call.excinfo and call.excinfo.typename != "AssertionError": if report.outcome == "skipped" and "SkipTest" in str(call): @@ -82,11 +83,11 @@ def pytest_exception_interact(node, call, report): report.longreprtext + "\n Check Python Test Logs for more details." ) else: - # If during execution, send this data that the given node failed. + # if execution, send this data that the given node failed. report_value = "error" if call.excinfo.typename == "AssertionError": report_value = "failure" - node_id = get_absolute_test_id(node.nodeid, get_node_path(node)) + node_id = str(node.nodeid) if node_id not in collected_tests_so_far: collected_tests_so_far.append(node_id) item_result = create_test_outcome( @@ -105,22 +106,6 @@ def pytest_exception_interact(node, call, report): ) -def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: - """A function that returns the absolute test id. This is necessary because testIds are relative to the rootdir. - This does not work for our case since testIds when referenced during run time are relative to the instantiation - location. Absolute paths for testIds are necessary for the test tree ensures configurations that change the rootdir - of pytest are handled correctly. - - Keyword arguments: - test_id -- the pytest id of the test which is relative to the rootdir. - testPath -- the path to the file the test is located in, as a pathlib.Path object. - """ - split_id = test_id.split("::")[1:] - absolute_test_id = "::".join([str(testPath), *split_id]) - print("absolute path", absolute_test_id) - return absolute_test_id - - def pytest_keyboard_interrupt(excinfo): """A pytest hook that is called when a keyboard interrupt is raised. @@ -145,7 +130,7 @@ class TestOutcome(Dict): def create_test_outcome( - testid: str, + test: str, outcome: str, message: Union[str, None], traceback: Union[str, None], @@ -153,7 +138,7 @@ def create_test_outcome( ) -> TestOutcome: """A function that creates a TestOutcome object.""" return TestOutcome( - test=testid, + test=test, outcome=outcome, message=message, traceback=traceback, # TODO: traceback @@ -169,7 +154,6 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): IS_DISCOVERY = False -map_id_to_path = dict() def pytest_load_initial_conftests(early_config, parser, args): @@ -200,21 +184,17 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - node_path = map_id_to_path[report.nodeid] - if not node_path: - node_path = cwd - # Calculate the absolute test id and use this as the ID moving forward. - absolute_node_id = get_absolute_test_id(report.nodeid, node_path) - if absolute_node_id not in collected_tests_so_far: - collected_tests_so_far.append(absolute_node_id) + node_id = str(report.nodeid) + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) item_result = create_test_outcome( - absolute_node_id, + node_id, report_value, message, traceback, ) collected_test = testRunResultDict() - collected_test[absolute_node_id] = item_result + collected_test[node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -231,22 +211,21 @@ def pytest_report_teststatus(report, config): def pytest_runtest_protocol(item, nextitem): - map_id_to_path[item.nodeid] = get_node_path(item) skipped = check_skipped_wrapper(item) if skipped: - absolute_node_id = get_absolute_test_id(item.nodeid, get_node_path(item)) + node_id = str(item.nodeid) report_value = "skipped" cwd = pathlib.Path.cwd() - if absolute_node_id not in collected_tests_so_far: - collected_tests_so_far.append(absolute_node_id) + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) item_result = create_test_outcome( - absolute_node_id, + node_id, report_value, None, None, ) collected_test = testRunResultDict() - collected_test[absolute_node_id] = item_result + collected_test[node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -492,14 +471,13 @@ def create_test_node( test_case_loc: str = ( str(test_case.location[1] + 1) if (test_case.location[1] is not None) else "" ) - absolute_test_id = get_absolute_test_id(test_case.nodeid, get_node_path(test_case)) return { "name": test_case.name, "path": get_node_path(test_case), "lineno": test_case_loc, "type_": "test", - "id_": absolute_test_id, - "runID": absolute_test_id, + "id_": test_case.nodeid, + "runID": test_case.nodeid, } From f454515463845a6c230d76164d9c9a06a44f0d98 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 2 Aug 2023 17:14:01 -0700 Subject: [PATCH 206/336] Update release plan to document what to do with `main` during endgame week (#21743) --- .github/release_plan.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/release_plan.md b/.github/release_plan.md index e02d7ee45abf..b4ceef69abe4 100644 --- a/.github/release_plan.md +++ b/.github/release_plan.md @@ -1,6 +1,6 @@ All dates should align with VS Code's [iteration](https://github.com/microsoft/vscode/labels/iteration-plan) and [endgame](https://github.com/microsoft/vscode/labels/endgame-plan) plans. -Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. +Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. At that point, commits to `main` should only be in response to bugs found during endgame testing until the release candidate is ready. NOTE: the number of this release is in the issue title and can be substituted in wherever you see [YYYY.minor]. @@ -51,6 +51,7 @@ NOTE: this PR should make all CI relating to `main` be passing again (such as th - [ ] Manually add/fix any 3rd-party licenses as appropriate based on what the internal build pipeline detects. - [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython). - [ ] Contact the PM team to begin drafting a blog post. +- [ ] Announce to the development team that `main` is open again. # Release (Wednesday, XXX XX) From 40bb62ae6af4ddfdf94931f3e11738b262e16684 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 3 Aug 2023 13:21:53 -0700 Subject: [PATCH 207/336] fix spelling for get-pip.py (#21752) fix spelling from get_pip to get-pip as advised. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ec46f481e79b..046d01588573 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ cucumber-report.json port.txt precommit.hook pythonFiles/lib/** -pythonFiles/get_pip.py +pythonFiles/get-pip.py debug_coverage*/** languageServer/** languageServer.*/** From 23353bbd1bbe49a2a95617f69933ee99bf7d04f5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 08:52:05 -0700 Subject: [PATCH 208/336] Improvements to `pythonTerminalEnvVarActivation` experiment (#21751) --- src/client/interpreter/activation/service.ts | 51 ++++++++-- .../terminalEnvVarCollectionService.ts | 58 ++++++----- src/client/interpreter/activation/types.ts | 2 + ...rminalEnvVarCollectionService.unit.test.ts | 97 ++++++++----------- 4 files changed, 118 insertions(+), 90 deletions(-) diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index e5da57227b19..4364cc825f78 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -19,8 +19,8 @@ import { ICurrentProcess, IDisposable, Resource } from '../../common/types'; import { sleep } from '../../common/utils/async'; import { InMemoryCache } from '../../common/utils/cacheUtils'; import { OSType } from '../../common/utils/platform'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IInterpreterService } from '../contracts'; @@ -38,6 +38,7 @@ import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda import { StopWatch } from '../../common/utils/stopWatch'; import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; +import { cache } from '../../common/utils/decorators'; const ENVIRONMENT_PREFIX = 'e8b39361-0157-4923-80e1-22d70d46dee6'; const CACHE_DURATION = 10 * 60 * 1000; @@ -154,11 +155,11 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } // Cache only if successful, else keep trying & failing if necessary. - const cache = new InMemoryCache(CACHE_DURATION); + const memCache = new InMemoryCache(CACHE_DURATION); return this.getActivatedEnvironmentVariablesImpl(resource, interpreter, allowExceptions, shell) .then((vars) => { - cache.data = vars; - this.activatedEnvVariablesCache.set(cacheKey, cache); + memCache.data = vars; + this.activatedEnvVariablesCache.set(cacheKey, memCache); sendTelemetryEvent( EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, stopWatch.elapsedTime, @@ -176,6 +177,35 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi }); } + @cache(-1, true) + public async getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise { + // Try to get the process environment variables using Python by printing variables, that can be little different + // from `process.env` and is preferred when calculating diff. + const globalInterpreters = this.interpreterService + .getInterpreters() + .filter((i) => !virtualEnvTypes.includes(i.envType)); + const interpreterPath = + globalInterpreters.length > 0 && globalInterpreters[0] ? globalInterpreters[0].path : 'python'; + try { + const [args, parse] = internalScripts.printEnvVariables(); + args.forEach((arg, i) => { + args[i] = arg.toCommandArgumentForPythonExt(); + }); + const command = `${interpreterPath} ${args.join(' ')}`; + const processService = await this.processServiceFactory.create(resource); + const result = await processService.shellExec(command, { + shell, + timeout: ENVIRONMENT_TIMEOUT, + maxBuffer: 1000 * 1000, + throwOnStdErr: false, + }); + const returnedEnv = this.parseEnvironmentOutput(result.stdout, parse); + return returnedEnv ?? process.env; + } catch (ex) { + return process.env; + } + } + public async getEnvironmentActivationShellCommands( resource: Resource, interpreter?: PythonEnvironment, @@ -231,7 +261,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi ); traceVerbose(`Activation Commands received ${activationCommands} for shell ${shellInfo.shell}`); if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { - if (interpreter?.envType === EnvironmentType.Venv) { + if (interpreter && [EnvironmentType.Venv, EnvironmentType.Pyenv].includes(interpreter?.envType)) { const key = getSearchPathEnvVarNames()[0]; if (env[key]) { env[key] = `${path.dirname(interpreter.path)}${path.delimiter}${env[key]}`; @@ -247,7 +277,14 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi const activationCommand = fixActivationCommands(activationCommands).join(' && '); // In order to make sure we know where the environment output is, // put in a dummy echo we can look for - command = `${activationCommand} && echo '${ENVIRONMENT_PREFIX}' && python ${args.join(' ')}`; + const commandSeparator = [TerminalShellType.powershell, TerminalShellType.powershellCore].includes( + shellInfo.shellType, + ) + ? ';' + : '&&'; + command = `${activationCommand} ${commandSeparator} echo '${ENVIRONMENT_PREFIX}' ${commandSeparator} python ${args.join( + ' ', + )}`; } // Make sure python warnings don't interfere with getting the environment. However diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 85b393425836..f4c29f03707d 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -33,6 +33,7 @@ import { defaultShells } from './service'; import { IEnvironmentActivationService } from './types'; import { EnvironmentType } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; +import { EnvironmentVariables } from '../../common/variables/types'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService { @@ -45,7 +46,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ private registeredOnce = false; - private previousEnvVars = _normCaseKeys(process.env); + /** + * Carries default environment variables for the currently selected shell. + */ + private processEnvVars: EnvironmentVariables | undefined; constructor( @inject(IPlatformService) private readonly platform: IPlatformService, @@ -90,6 +94,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ this.applicationEnvironment.onDidChangeShell( async (shell: string) => { this.showProgress(); + this.processEnvVars = undefined; // Pass in the shell where known instead of relying on the application environment, because of bug // on VSCode: https://github.com/microsoft/vscode/issues/160694 await this._applyCollection(undefined, shell).ignoreErrors(); @@ -106,6 +111,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); + const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); + // Clear any previously set env vars from collection. + envVarCollection.clear(); if (!settings.terminal.activateEnvironment) { traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); return; @@ -116,7 +124,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ undefined, shell, ); - const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); if (!env) { const shellType = identifyShellFromShellPath(shell); const defaultShell = defaultShells[this.platform.osType]; @@ -126,32 +133,38 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ await this._applyCollection(resource, defaultShell?.shell); return; } - envVarCollection.clear(); - this.previousEnvVars = _normCaseKeys(process.env); + this.processEnvVars = undefined; return; } - const previousEnv = this.previousEnvVars; - this.previousEnvVars = env; + if (!this.processEnvVars) { + this.processEnvVars = await this.environmentActivationService.getProcessEnvironmentVariables( + resource, + shell, + ); + } + const processEnv = this.processEnvVars; Object.keys(env).forEach((key) => { + if (shouldSkip(key)) { + return; + } const value = env[key]; - const prevValue = previousEnv[key]; + const prevValue = processEnv[key]; if (prevValue !== value) { if (value !== undefined) { + if (key === 'PS1') { + traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); + envVarCollection.prepend(key, value, { + applyAtShellIntegration: true, + applyAtProcessCreation: false, + }); + return; + } traceVerbose(`Setting environment variable ${key} in collection to ${value}`); envVarCollection.replace(key, value, { applyAtShellIntegration: true }); - } else { - traceVerbose(`Clearing environment variable ${key} from collection`); - envVarCollection.delete(key); } } }); - Object.keys(previousEnv).forEach((key) => { - // If the previous env var is not in the current env, clear it from collection. - if (!(key in env)) { - traceVerbose(`Clearing environment variable ${key} from collection`); - envVarCollection.delete(key); - } - }); + const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); envVarCollection.description = description; @@ -224,13 +237,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } -export function _normCaseKeys(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { - const result: NodeJS.ProcessEnv = {}; - Object.keys(env).forEach((key) => { - // `os.environ` script used to get env vars normalizes keys to upper case: - // https://github.com/python/cpython/issues/101754 - // So convert `process.env` keys to upper case to match. - result[key.toUpperCase()] = env[key]; - }); - return result; +function shouldSkip(env: string) { + return ['_', 'SHLVL'].includes(env); } diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts index d8e4ae16dbca..e00ef9b62b3f 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -4,10 +4,12 @@ 'use strict'; import { Resource } from '../../common/types'; +import { EnvironmentVariables } from '../../common/variables/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; export const IEnvironmentActivationService = Symbol('IEnvironmentActivationService'); export interface IEnvironmentActivationService { + getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise; getActivatedEnvironmentVariables( resource: Resource, interpreter?: PythonEnvironment, diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index cb0b6b02f288..458e2b28c181 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -5,10 +5,10 @@ import * as sinon from 'sinon'; import { assert, expect } from 'chai'; -import { cloneDeep } from 'lodash'; import { mock, instance, when, anything, verify, reset } from 'ts-mockito'; import { EnvironmentVariableCollection, + EnvironmentVariableMutatorOptions, EnvironmentVariableScope, ProgressLocation, Uri, @@ -31,10 +31,7 @@ import { import { Interpreters } from '../../../client/common/utils/localize'; import { OSType, getOSType } from '../../../client/common/utils/platform'; import { defaultShells } from '../../../client/interpreter/activation/service'; -import { - TerminalEnvVarCollectionService, - _normCaseKeys, -} from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; +import { TerminalEnvVarCollectionService } from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; @@ -89,11 +86,16 @@ suite('Terminal Environment Variable Collection Service', () => { }) .thenResolve(); environmentActivationService = mock(); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + process.env, + ); configService = mock(); when(configService.getSettings(anything())).thenReturn(({ terminal: { activateEnvironment: true }, pythonPath: displayPath, } as unknown) as IPythonSettings); + when(collection.clear()).thenResolve(); + when(scopedCollection.clear()).thenResolve(); terminalEnvVarCollectionService = new TerminalEnvVarCollectionService( instance(platform), instance(interpreterService), @@ -169,8 +171,27 @@ suite('Terminal Environment Variable Collection Service', () => { }); test('If activated variables are returned for custom shell, apply it correctly to the collection', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; - delete envVars.PATH; + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + }); + + test('If activated variables contain PS1, prefix it using shell integration', async () => { + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env, PS1: '(prompt)' }; when( environmentActivationService.getActivatedEnvironmentVariables( anything(), @@ -182,16 +203,20 @@ suite('Terminal Environment Variable Collection Service', () => { when(collection.replace(anything(), anything(), anything())).thenResolve(); when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PS1', '(prompt)', anything())).thenCall((_, _v, o) => { + opts = o; + }); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + verify(collection.clear()).once(); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete('PATH')).once(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); test('Verify envs are not applied if env activation is disabled', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; - delete envVars.PATH; + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; when( environmentActivationService.getActivatedEnvironmentVariables( anything(), @@ -211,13 +236,12 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + verify(collection.clear()).once(); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).never(); - verify(collection.delete('PATH')).never(); }); test('Verify correct options are used when applying envs and setting description', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; - delete envVars.PATH; + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; const resource = Uri.file('a'); const workspaceFolder: WorkspaceFolder = { uri: Uri.file('workspacePath'), @@ -233,51 +257,11 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(options, { applyAtShellIntegration: true }); return Promise.resolve(); }); - when(scopedCollection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(resource, customShell); + verify(scopedCollection.clear()).once(); verify(scopedCollection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(scopedCollection.delete('PATH')).once(); - }); - - test('Only relative changes to previously applied variables are applied to the collection', async () => { - const envVars: NodeJS.ProcessEnv = { - RANDOM_VAR: 'random', - CONDA_PREFIX: 'prefix/to/conda', - ..._normCaseKeys(process.env), - }; - when( - environmentActivationService.getActivatedEnvironmentVariables( - anything(), - undefined, - undefined, - customShell, - ), - ).thenResolve(envVars); - - when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything())).thenResolve(); - - await terminalEnvVarCollectionService._applyCollection(undefined, customShell); - - const newEnvVars = cloneDeep(envVars); - delete newEnvVars.CONDA_PREFIX; - newEnvVars.RANDOM_VAR = undefined; // Deleting the variable from the collection is the same as setting it to undefined. - reset(environmentActivationService); - when( - environmentActivationService.getActivatedEnvironmentVariables( - anything(), - undefined, - undefined, - customShell, - ), - ).thenResolve(newEnvVars); - - await terminalEnvVarCollectionService._applyCollection(undefined, customShell); - - verify(collection.delete('CONDA_PREFIX')).once(); - verify(collection.delete('RANDOM_VAR')).once(); }); test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { @@ -289,7 +273,7 @@ suite('Terminal Environment Variable Collection Service', () => { customShell, ), ).thenResolve(undefined); - const envVars = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; + const envVars = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; when( environmentActivationService.getActivatedEnvironmentVariables( anything(), @@ -300,12 +284,11 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete(anything())).never(); + verify(collection.clear()).twice(); }); test('If no activated variables are returned for default shell, clear collection', async () => { From 40ff6e9fc1b07fef713ca552d94c86bddf2dd249 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 10:22:31 -0700 Subject: [PATCH 209/336] Remove private Jupyter APIs from public API types (#21761) For https://github.com/microsoft/vscode-jupyter/issues/13986 --- pythonExtensionApi/package-lock.json | 4 +-- pythonExtensionApi/package.json | 2 +- src/client/api.ts | 7 +++++ src/client/api/types.ts | 46 +--------------------------- 4 files changed, 11 insertions(+), 48 deletions(-) diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index a03c33ab5c0d..4c5e98adbc16 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vscode/python-extension", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "devDependencies": { "@types/vscode": "^1.78.0", diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index dd0e921f8abe..271542f161ec 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.2", + "version": "1.0.3", "author": { "name": "Microsoft Corporation" }, diff --git a/src/client/api.ts b/src/client/api.ts index 2371a4d88de1..23b2553c93d2 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -35,6 +35,13 @@ export function buildApi( const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); const api: PythonExtension & { + /** + * Internal API just for Jupyter, hence don't include in the official types. + */ + jupyter: { + registerHooks(): void; + }; + } & { /** * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an * iteration or two. diff --git a/src/client/api/types.ts b/src/client/api/types.ts index 63954a16d868..4de554bf5a24 100644 --- a/src/client/api/types.ts +++ b/src/client/api/types.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem, extensions } from 'vscode'; +import { CancellationToken, Event, Uri, WorkspaceFolder, extensions } from 'vscode'; /* * Do not introduce any breaking changes to this API. @@ -12,9 +12,6 @@ export interface PythonExtension { * Promise indicating whether all parts of the extension have completed loading or not. */ ready: Promise; - jupyter: { - registerHooks(): void; - }; debug: { /** * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. @@ -109,47 +106,6 @@ export interface PythonExtension { }; } -interface IJupyterServerUri { - baseUrl: string; - token: string; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationHeader: any; // JSON object for authorization header. - expiration?: Date; // Date/time when header expires and should be refreshed. - displayName: string; -} - -type JupyterServerUriHandle = string; - -export interface IJupyterUriProvider { - readonly id: string; // Should be a unique string (like a guid) - getQuickPickEntryItems(): QuickPickItem[]; - handleQuickPick(item: QuickPickItem, backEnabled: boolean): Promise; - getServerUri(handle: JupyterServerUriHandle): Promise; -} - -interface IDataFrameInfo { - columns?: { key: string; type: ColumnType }[]; - indexColumn?: string; - rowCount?: number; -} - -export interface IDataViewerDataProvider { - dispose(): void; - getDataFrameInfo(): Promise; - getAllRows(): Promise; - getRows(start: number, end: number): Promise; -} - -enum ColumnType { - String = 'string', - Number = 'number', - Bool = 'bool', -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type IRowsResponse = any[]; - export type RefreshOptions = { /** * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so From dd20561bf36a60938e848d1bf6e058883ec7ae69 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 4 Aug 2023 10:31:15 -0700 Subject: [PATCH 210/336] revert due to buffer overflow on subprocess (#21762) revert https://github.com/microsoft/vscode-python/pull/21667 because it causes buffer overflow in the python testing subprocess when larger repos are used. Specifically seen on pytest discovery with >200 tests. Revert to align with the stable release and put in a fix next week. --- .../testing/testController/common/server.ts | 27 +- .../testing/testController/common/types.ts | 7 +- .../pytest/pytestDiscoveryAdapter.ts | 27 +- .../pytest/pytestExecutionAdapter.ts | 45 ++-- .../unittest/testDiscoveryAdapter.ts | 11 +- .../unittest/testExecutionAdapter.ts | 21 +- src/test/mocks/mockChildProcess.ts | 238 ----------------- .../pytestDiscoveryAdapter.unit.test.ts | 76 ++---- .../pytestExecutionAdapter.unit.test.ts | 118 ++------- .../testController/server.unit.test.ts | 248 +++++------------- .../workspaceTestAdapter.unit.test.ts | 3 +- 11 files changed, 157 insertions(+), 664 deletions(-) delete mode 100644 src/test/mocks/mockChildProcess.ts diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 564bd82f2ef6..f854371ffc35 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -3,11 +3,10 @@ import * as net from 'net'; import * as crypto from 'crypto'; -import { Disposable, Event, EventEmitter, TestRun } from 'vscode'; +import { Disposable, Event, EventEmitter } from 'vscode'; import * as path from 'path'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -16,7 +15,6 @@ import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; -import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); @@ -142,12 +140,7 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDataReceived.event; } - async sendCommand( - options: TestCommandOptions, - runTestIdPort?: string, - runInstance?: TestRun, - callback?: () => void, - ): Promise { + async sendCommand(options: TestCommandOptions, runTestIdPort?: string, callback?: () => void): Promise { const { uuid } = options; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; @@ -161,7 +154,7 @@ export class PythonTestServer implements ITestServer, Disposable { }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; - const isRun = runTestIdPort !== undefined; + const isRun = !options.testIds; // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, @@ -202,19 +195,7 @@ export class PythonTestServer implements ITestServer, Disposable { // This means it is running discovery traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } - const deferred = createDeferred>(); - - const result = execService.execObservable(args, spawnOptions); - - runInstance?.token.onCancellationRequested(() => { - result?.proc?.kill(); - }); - result?.proc?.on('close', () => { - traceLog('Exec server closed.', uuid); - deferred.resolve({ stdout: '', stderr: '' }); - callback?.(); - }); - await deferred.promise; + await execService.exec(args, spawnOptions); } } catch (ex) { this.uuids = this.uuids.filter((u) => u !== uuid); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 16c0bd0e3cee..d4e54951bfd7 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -174,12 +174,7 @@ export interface ITestServer { readonly onDataReceived: Event; readonly onRunDataReceived: Event; readonly onDiscoveryDataReceived: Event; - sendCommand( - options: TestCommandOptions, - runTestIdsPort?: string, - runInstance?: TestRun, - callback?: () => void, - ): Promise; + sendCommand(options: TestCommandOptions, runTestIdsPort?: string, callback?: () => void): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 810fae0fa11c..b83224d4161b 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -4,14 +4,13 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceVerbose } from '../../../logging'; +import { traceError, traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, @@ -49,7 +48,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); @@ -79,15 +78,17 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. - const deferredExec = createDeferred>(); - const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); - const result = execService?.execObservable(execArgs, spawnOptions); - - result?.proc?.on('close', () => { - deferredExec.resolve({ stdout: '', stderr: '' }); - this.testServer.deleteUUID(uuid); - deferred.resolve(); - }); - await deferredExec.promise; + execService + ?.exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) + .then(() => { + this.testServer.deleteUUID(uuid); + return deferred.resolve(); + }) + .catch((err) => { + traceError(`Error while trying to run pytest discovery, \n${err}\r\n\r\n`); + this.testServer.deleteUUID(uuid); + return deferred.reject(err); + }); + return deferred.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index b05fa21fc046..a75a6089627c 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -15,7 +15,6 @@ import { } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -23,7 +22,13 @@ import { removePositionalFoldersAndFiles } from './arguments'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; -import * as utils from '../common/utils'; +import { startTestIdServer } from '../common/utils'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +// (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; +/** + * Wrapper Class for pytest test execution.. + */ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( @@ -43,20 +48,18 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); traceVerbose(uri, testIds, debugBool); - const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - const dispose = function (testServer: ITestServer) { - testServer.deleteUUID(uuid); - disposedDataReceived.dispose(); - }; - runInstance?.token.onCancellationRequested(() => { - dispose(this.testServer); - }); - await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, executionFactory, debugLauncher); - + try { + await this.runTestsNew(uri, testIds, uuid, debugBool, executionFactory, debugLauncher); + } finally { + this.testServer.deleteUUID(uuid); + disposable.dispose(); + // confirm with testing that this gets called (it must clean this up) + } // placeholder until after the rewrite is adopted // TODO: remove after adoption. const executionPayload: ExecutionTestPayload = { @@ -71,7 +74,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, - runInstance?: TestRun, debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, @@ -122,7 +124,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); - const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); + const pytestRunTestIdsPort = await startTestIdServer(testIds); if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); @@ -141,7 +143,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { deferred.resolve(); - this.testServer.deleteUUID(uuid); }); } else { // combine path to run script with run args @@ -149,19 +150,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const runArgs = [scriptPath, ...testArgs]; traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); - const deferredExec = createDeferred>(); - const result = execService?.execObservable(runArgs, spawnOptions); - - runInstance?.token.onCancellationRequested(() => { - result?.proc?.kill(); - }); - - result?.proc?.on('close', () => { - deferredExec.resolve({ stdout: '', stderr: '' }); - this.testServer.deleteUUID(uuid); - deferred.resolve(); - }); - await deferredExec.promise; + await execService?.exec(runArgs, spawnOptions); } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index b49ac3dabd0e..6deca55117ea 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -46,11 +46,12 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); }); - - await this.callSendCommand(options, () => { + try { + await this.callSendCommand(options); + } finally { this.testServer.deleteUUID(uuid); disposable.dispose(); - }); + } // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { @@ -60,8 +61,8 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { - await this.testServer.sendCommand(options, undefined, undefined, callback); + private async callSendCommand(options: TestCommandOptions): Promise { + await this.testServer.sendCommand(options); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 4cd392f93a43..4cab941c2608 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -37,19 +37,18 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - const dispose = function () { - disposedDataReceived.dispose(); - }; - runInstance?.token.onCancellationRequested(() => { + try { + await this.runTestsNew(uri, testIds, uuid, debugBool); + } finally { this.testServer.deleteUUID(uuid); - dispose(); - }); - await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, dispose); + disposable.dispose(); + // confirm with testing that this gets called (it must clean this up) + } const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } @@ -58,9 +57,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, - runInstance?: TestRun, debugBool?: boolean, - dispose?: () => void, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -83,10 +80,8 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, () => { - this.testServer.deleteUUID(uuid); + await this.testServer.sendCommand(options, runTestIdsPort.toString(), () => { deferred.resolve(); - dispose?.(); }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts deleted file mode 100644 index c038c0f845ab..000000000000 --- a/src/test/mocks/mockChildProcess.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Serializable, SendHandle, MessageOptions } from 'child_process'; -import { Writable, Readable, Pipe } from 'stream'; -import { EventEmitter } from 'node:events'; - -export class MockChildProcess extends EventEmitter { - constructor(spawnfile: string, spawnargs: string[]) { - super(); - this.spawnfile = spawnfile; - this.spawnargs = spawnargs; - this.stdin = new Writable(); - this.stdout = new Readable(); - this.stderr = null; - this.channel = null; - this.stdio = [this.stdin, this.stdout, this.stdout, this.stderr, null]; - this.killed = false; - this.connected = false; - this.exitCode = null; - this.signalCode = null; - this.eventMap = new Map(); - } - - stdin: Writable | null; - - stdout: Readable | null; - - stderr: Readable | null; - - eventMap: Map; - - readonly channel?: Pipe | null | undefined; - - readonly stdio: [ - Writable | null, - // stdin - Readable | null, - // stdout - Readable | null, - // stderr - Readable | Writable | null | undefined, - // extra - Readable | Writable | null | undefined, // extra - ]; - - readonly killed: boolean; - - readonly pid?: number | undefined; - - readonly connected: boolean; - - readonly exitCode: number | null; - - readonly signalCode: NodeJS.Signals | null; - - readonly spawnargs: string[]; - - readonly spawnfile: string; - - signal?: NodeJS.Signals | number; - - send(message: Serializable, callback?: (error: Error | null) => void): boolean; - - send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean; - - send( - message: Serializable, - sendHandle?: SendHandle, - options?: MessageOptions, - callback?: (error: Error | null) => void, - ): boolean; - - send( - message: Serializable, - _sendHandleOrCallback?: SendHandle | ((error: Error | null) => void), - _optionsOrCallback?: MessageOptions | ((error: Error | null) => void), - _callback?: (error: Error | null) => void, - ): boolean { - // Implementation of the send method - // For example, you might want to emit a 'message' event - this.stdout?.push(message.toString()); - return true; - } - - // eslint-disable-next-line class-methods-use-this - disconnect(): void { - /* noop */ - } - - // eslint-disable-next-line class-methods-use-this - unref(): void { - /* noop */ - } - - // eslint-disable-next-line class-methods-use-this - ref(): void { - /* noop */ - } - - addListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - addListener(event: 'disconnect', listener: () => void): this; - - addListener(event: 'error', listener: (err: Error) => void): this; - - addListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - addListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; - - addListener(event: 'spawn', listener: () => void): this; - - addListener(event: string, listener: (...args: any[]) => void): this { - if (this.eventMap.has(event)) { - this.eventMap.get(event).push(listener); - } else { - this.eventMap.set(event, [listener]); - } - return this; - } - - emit(event: 'close', code: number | null, signal: NodeJS.Signals | null): boolean; - - emit(event: 'disconnect'): boolean; - - emit(event: 'error', err: Error): boolean; - - emit(event: 'exit', code: number | null, signal: NodeJS.Signals | null): boolean; - - emit(event: 'message', message: Serializable, sendHandle: SendHandle): boolean; - - emit(event: 'spawn', listener: () => void): boolean; - - emit(event: string | symbol, ...args: unknown[]): boolean { - if (this.eventMap.has(event.toString())) { - this.eventMap.get(event.toString()).forEach((listener: (arg0: unknown) => void) => { - const argsArray = Array.isArray(args) ? args : [args]; - listener(argsArray); - }); - } - return true; - } - - on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - on(event: 'disconnect', listener: () => void): this; - - on(event: 'error', listener: (err: Error) => void): this; - - on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - on(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; - - on(event: 'spawn', listener: () => void): this; - - on(event: string, listener: (...args: any[]) => void): this { - if (this.eventMap.has(event)) { - this.eventMap.get(event).push(listener); - } else { - this.eventMap.set(event, [listener]); - } - return this; - } - - once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - once(event: 'disconnect', listener: () => void): this; - - once(event: 'error', listener: (err: Error) => void): this; - - once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - once(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; - - once(event: 'spawn', listener: () => void): this; - - once(event: string, listener: (...args: any[]) => void): this { - if (this.eventMap.has(event)) { - this.eventMap.get(event).push(listener); - } else { - this.eventMap.set(event, [listener]); - } - return this; - } - - prependListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - prependListener(event: 'disconnect', listener: () => void): this; - - prependListener(event: 'error', listener: (err: Error) => void): this; - - prependListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - prependListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; - - prependListener(event: 'spawn', listener: () => void): this; - - prependListener(event: string, listener: (...args: any[]) => void): this { - if (this.eventMap.has(event)) { - this.eventMap.get(event).push(listener); - } else { - this.eventMap.set(event, [listener]); - } - return this; - } - - prependOnceListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - prependOnceListener(event: 'disconnect', listener: () => void): this; - - prependOnceListener(event: 'error', listener: (err: Error) => void): this; - - prependOnceListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - prependOnceListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; - - prependOnceListener(event: 'spawn', listener: () => void): this; - - prependOnceListener(event: string, listener: (...args: any[]) => void): this { - if (this.eventMap.has(event)) { - this.eventMap.get(event).push(listener); - } else { - this.eventMap.set(event, [listener]); - } - return this; - } - - trigger(event: string): Array { - if (this.eventMap.has(event)) { - return this.eventMap.get(event); - } - return []; - } - - kill(_signal?: NodeJS.Signals | number): boolean { - this.stdout?.destroy(); - return true; - } -} diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 8ba7dd9a6f00..18212b2d1032 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -5,7 +5,6 @@ import * as assert from 'assert'; import { Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; import { ITestServer } from '../../../../client/testing/testController/common/types'; @@ -13,11 +12,9 @@ import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions, - Output, } from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { MockChildProcess } from '../../../mocks/mockChildProcess'; -import { Deferred, createDeferred } from '../../../../client/common/utils/async'; suite('pytest test discovery adapter', () => { let testServer: typeMoq.IMock; @@ -32,7 +29,6 @@ suite('pytest test discovery adapter', () => { let expectedPath: string; let uri: Uri; let expectedExtraVariables: Record; - let mockProc: MockChildProcess; setup(() => { const mockExtensionRootDir = typeMoq.Mock.ofType(); @@ -70,46 +66,32 @@ suite('pytest test discovery adapter', () => { }), } as unknown) as IConfigurationService; - // set up exec service with child process - mockProc = new MockChildProcess('', ['']); - execService = typeMoq.Mock.ofType(); - const output = new Observable>(() => { - /* no op */ - }); - execService - .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => ({ - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - })); - execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); - outputChannel = typeMoq.Mock.ofType(); - }); - test('Discovery should call exec with correct basic args', async () => { - // set up exec mock - deferred = createDeferred(); + // set up exec factory execFactory = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + + // set up exec service + execService = typeMoq.Mock.ofType(); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => { deferred.resolve(); - return Promise.resolve(execService.object); + return Promise.resolve({ stdout: '{}' }); }); - + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + outputChannel = typeMoq.Mock.ofType(); + }); + test('Discovery should call exec with correct basic args', async () => { adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - adapter.discoverTests(uri, execFactory.object); - // add in await and trigger - await deferred.promise; - mockProc.trigger('close'); - - // verification + await adapter.discoverTests(uri, execFactory.object); const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; + execService.verify( (x) => - x.execObservable( + x.exec( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); @@ -126,34 +108,16 @@ suite('pytest test discovery adapter', () => { const expectedPathNew = path.join('other', 'path'); const configServiceNew: IConfigurationService = ({ getSettings: () => ({ - testing: { - pytestArgs: ['.', 'abc', 'xyz'], - cwd: expectedPathNew, - }, + testing: { pytestArgs: ['.', 'abc', 'xyz'], cwd: expectedPathNew }, }), } as unknown) as IConfigurationService; - // set up exec mock - deferred = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve(execService.object); - }); - adapter = new PytestTestDiscoveryAdapter(testServer.object, configServiceNew, outputChannel.object); - adapter.discoverTests(uri, execFactory.object); - // add in await and trigger - await deferred.promise; - mockProc.trigger('close'); - - // verification + await adapter.discoverTests(uri, execFactory.object); const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.', 'abc', 'xyz']; execService.verify( (x) => - x.execObservable( + x.exec( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 43b763f56e6c..44116fd753b0 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -6,13 +6,11 @@ import { TestRun, Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as sinon from 'sinon'; import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { ITestServer } from '../../../../client/testing/testController/common/types'; import { IPythonExecutionFactory, IPythonExecutionService, - Output, SpawnOptions, } from '../../../../client/common/process/types'; import { createDeferred, Deferred } from '../../../../client/common/utils/async'; @@ -20,7 +18,6 @@ import { PytestTestExecutionAdapter } from '../../../../client/testing/testContr import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; import * as util from '../../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { MockChildProcess } from '../../../mocks/mockChildProcess'; suite('pytest test execution adapter', () => { let testServer: typeMoq.IMock; @@ -32,8 +29,8 @@ suite('pytest test execution adapter', () => { let debugLauncher: typeMoq.IMock; (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; - let mockProc: MockChildProcess; - let utilsStub: sinon.SinonStub; + let startTestIdServerStub: sinon.SinonStub; + setup(() => { testServer = typeMoq.Mock.ofType(); testServer.setup((t) => t.getPort()).returns(() => 12345); @@ -50,24 +47,8 @@ suite('pytest test execution adapter', () => { }), isTestExecution: () => false, } as unknown) as IConfigurationService; - - // set up exec service with child process - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - execService = typeMoq.Mock.ofType(); - execService - .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => ({ - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - })); execFactory = typeMoq.Mock.ofType(); - utilsStub = sinon.stub(util, 'startTestIdServer'); + execService = typeMoq.Mock.ofType(); debugLauncher = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) @@ -85,6 +66,7 @@ suite('pytest test execution adapter', () => { deferred.resolve(); return Promise.resolve(); }); + startTestIdServerStub = sinon.stub(util, 'startTestIdServer').returns(Promise.resolve(54321)); execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); @@ -95,25 +77,10 @@ suite('pytest test execution adapter', () => { sinon.restore(); }); test('startTestIdServer called with correct testIds', async () => { - const deferred2 = createDeferred(); - const deferred3 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred2.resolve(); - return Promise.resolve(execService.object); - }); - utilsStub.callsFake(() => { - deferred3.resolve(); - return Promise.resolve(54321); - }); - const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -121,38 +88,19 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - const testIds = ['test1id', 'test2id']; - adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); - // add in await and trigger - await deferred2.promise; - await deferred3.promise; - mockProc.trigger('close'); + const testIds = ['test1id', 'test2id']; + await adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); - // assert - sinon.assert.calledWithExactly(utilsStub, testIds); + sinon.assert.calledWithExactly(startTestIdServerStub, testIds); }); test('pytest execution called with correct args', async () => { - const deferred2 = createDeferred(); - const deferred3 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred2.resolve(); - return Promise.resolve(execService.object); - }); - utilsStub.callsFake(() => { - deferred3.resolve(); - return Promise.resolve(54321); - }); - const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -160,12 +108,9 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - adapter.runTests(uri, [], false, testRun.object, execFactory.object); - - await deferred2.promise; - await deferred3.promise; - mockProc.trigger('close'); + await adapter.runTests(uri, [], false, testRun.object, execFactory.object); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -178,7 +123,7 @@ suite('pytest test execution adapter', () => { // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); execService.verify( (x) => - x.execObservable( + x.exec( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -194,21 +139,6 @@ suite('pytest test execution adapter', () => { ); }); test('pytest execution respects settings.testing.cwd when present', async () => { - const deferred2 = createDeferred(); - const deferred3 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred2.resolve(); - return Promise.resolve(execService.object); - }); - utilsStub.callsFake(() => { - deferred3.resolve(); - return Promise.resolve(54321); - }); - const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const newCwd = path.join('new', 'path'); configService = ({ getSettings: () => ({ @@ -219,7 +149,7 @@ suite('pytest test execution adapter', () => { const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -227,12 +157,9 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - adapter.runTests(uri, [], false, testRun.object, execFactory.object); - - await deferred2.promise; - await deferred3.promise; - mockProc.trigger('close'); + await adapter.runTests(uri, [], false, testRun.object, execFactory.object); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -245,7 +172,7 @@ suite('pytest test execution adapter', () => { execService.verify( (x) => - x.execObservable( + x.exec( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -261,17 +188,10 @@ suite('pytest test execution adapter', () => { ); }); test('Debug launched correctly for pytest', async () => { - const deferred3 = createDeferred(); - utilsStub.callsFake(() => { - deferred3.resolve(); - return Promise.resolve(54321); - }); - const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -279,9 +199,9 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); await adapter.runTests(uri, [], true, testRun.object, execFactory.object, debugLauncher.object); - await deferred3.promise; debugLauncher.verify( (x) => x.launchDebugger( diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 53c2b72e40f7..1131c26c6444 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -1,24 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -/* eslint-disable @typescript-eslint/no-explicit-any */ import * as assert from 'assert'; import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; import { OutputChannel, Uri } from 'vscode'; -import { Observable } from 'rxjs'; -import * as typeMoq from 'typemoq'; -import { - IPythonExecutionFactory, - IPythonExecutionService, - Output, - SpawnOptions, -} from '../../../client/common/process/types'; +import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions } from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; -import { Deferred, createDeferred } from '../../../client/common/utils/async'; -import { MockChildProcess } from '../../mocks/mockChildProcess'; +import { createDeferred } from '../../../client/common/utils/async'; suite('Python Test Server', () => { const fakeUuid = 'fake-uuid'; @@ -27,12 +18,10 @@ suite('Python Test Server', () => { let stubExecutionService: IPythonExecutionService; let server: PythonTestServer; let sandbox: sinon.SinonSandbox; + let execArgs: string[]; + let spawnOptions: SpawnOptions; let v4Stub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; - let mockProc: MockChildProcess; - let execService: typeMoq.IMock; - let deferred: Deferred; - let execFactory = typeMoq.Mock.ofType(); setup(() => { sandbox = sinon.createSandbox(); @@ -40,42 +29,27 @@ suite('Python Test Server', () => { v4Stub.returns(fakeUuid); stubExecutionService = ({ - execObservable: () => Promise.resolve({ stdout: '', stderr: '' }), + exec: (args: string[], spawnOptionsProvided: SpawnOptions) => { + execArgs = args; + spawnOptions = spawnOptionsProvided; + return Promise.resolve({ stdout: '', stderr: '' }); + }, } as unknown) as IPythonExecutionService; stubExecutionFactory = ({ createActivatedEnvironment: () => Promise.resolve(stubExecutionService), } as unknown) as IPythonExecutionFactory; - - // set up exec service with child process - mockProc = new MockChildProcess('', ['']); - execService = typeMoq.Mock.ofType(); - const output = new Observable>(() => { - /* no op */ - }); - execService - .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => ({ - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - })); - execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); }); teardown(() => { sandbox.restore(); + execArgs = []; server.dispose(); }); test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { const options = { - command: { - script: 'myscript', - args: ['-foo', 'foo'], - }, + command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, @@ -85,31 +59,17 @@ suite('Python Test Server', () => { outputChannel: undefined, token: undefined, throwOnStdErr: true, - extraVariables: { - PYTHONPATH: '/foo/bar', - RUN_TEST_IDS_PORT: '56789', - }, + extraVariables: { PYTHONPATH: '/foo/bar', RUN_TEST_IDS_PORT: '56789' }, } as SpawnOptions; - const deferred2 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred2.resolve(); - return Promise.resolve(execService.object); - }); - server = new PythonTestServer(execFactory.object, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); - server.sendCommand(options, '56789'); - // add in await and trigger - await deferred2.promise; - mockProc.trigger('close'); - + await server.sendCommand(options, '56789'); const port = server.getPort(); - const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']; - execService.verify((x) => x.execObservable(expectedArgs, expectedSpawnOptions), typeMoq.Times.once()); + + assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); + assert.deepStrictEqual(spawnOptions, expectedSpawnOptions); }); test('sendCommand should write to an output channel if it is provided as an option', async () => { @@ -120,31 +80,17 @@ suite('Python Test Server', () => { }, } as OutputChannel; const options = { - command: { - script: 'myscript', - args: ['-foo', 'foo'], - }, + command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, outChannel, }; - deferred = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve(execService.object); - }); - server = new PythonTestServer(execFactory.object, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); - server.sendCommand(options); - // add in await and trigger - await deferred.promise; - mockProc.trigger('close'); + await server.sendCommand(options); const port = server.getPort(); const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); @@ -153,12 +99,13 @@ suite('Python Test Server', () => { }); test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { - let eventData: { status: string; errors: string[] } | undefined; + let eventData: { status: string; errors: string[] }; stubExecutionService = ({ - execObservable: () => { + exec: () => { throw new Error('Failed to execute'); }, } as unknown) as IPythonExecutionService; + const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -175,43 +122,30 @@ suite('Python Test Server', () => { await server.sendCommand(options); - assert.notEqual(eventData, undefined); - assert.deepStrictEqual(eventData?.status, 'error'); - assert.deepStrictEqual(eventData?.errors, ['Failed to execute']); + assert.deepStrictEqual(eventData!.status, 'error'); + assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); }); test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { let eventData: string | undefined; const client = new net.Socket(); + const deferred = createDeferred(); + const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { + + stubExecutionService = ({ + exec: async () => { client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; + return Promise.resolve({ stdout: '', stderr: '' }); }, } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - - deferred = createDeferred(); - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -227,17 +161,16 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - server.sendCommand(options); - // add in await and trigger + await server.sendCommand(options); await deferred.promise; - mockProc.trigger('close'); - assert.deepStrictEqual(eventData, ''); }); test('If the server doesnt recognize the UUID it should ignore it', async () => { let eventData: string | undefined; const client = new net.Socket(); + const deferred = createDeferred(); + const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -245,28 +178,14 @@ suite('Python Test Server', () => { uuid: fakeUuid, }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { + stubExecutionService = ({ + exec: async () => { client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; + return Promise.resolve({ stdout: '', stderr: '' }); }, } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -282,7 +201,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - server.sendCommand(options); + await server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -293,34 +212,23 @@ suite('Python Test Server', () => { test('Error if payload does not have a content length header', async () => { let eventData: string | undefined; const client = new net.Socket(); + const deferred = createDeferred(); + const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { + + stubExecutionService = ({ + exec: async () => { client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; + return Promise.resolve({ stdout: '', stderr: '' }); }, } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -336,7 +244,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - server.sendCommand(options); + await server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -359,6 +267,7 @@ Request-uuid: UUID_HERE // Your test logic here let eventData: string | undefined; const client = new net.Socket(); + const deferred = createDeferred(); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, @@ -366,28 +275,15 @@ Request-uuid: UUID_HERE cwd: '/foo/bar', uuid: fakeUuid, }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { + + stubExecutionService = ({ + exec: async () => { client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; + return Promise.resolve({ stdout: '', stderr: '' }); }, } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); payload = payload.replace('UUID_HERE', uuid); @@ -405,7 +301,7 @@ Request-uuid: UUID_HERE console.log('Socket connection error:', error); }); - server.sendCommand(options); + await server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, expectedResult); }); @@ -414,29 +310,8 @@ Request-uuid: UUID_HERE test('Calls run resolver if the result header is in the payload', async () => { let eventData: string | undefined; const client = new net.Socket(); + const deferred = createDeferred(); - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -444,6 +319,14 @@ Request-uuid: UUID_HERE uuid: fakeUuid, }; + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); server.onRunDataReceived(({ data }) => { @@ -466,8 +349,9 @@ Request-uuid: ${uuid} console.log('Socket connection error:', error); }); - server.sendCommand(options); + await server.sendCommand(options); await deferred.promise; + console.log('event data', eventData); const expectedResult = '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; assert.deepStrictEqual(eventData, expectedResult); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index 41cd1bbd7ef2..5a2e48130746 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -164,7 +164,8 @@ suite('Workspace test adapter', () => { const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); const testProvider = 'unittest'; - await workspaceTestAdapter.discoverTests(testController); + const abc = await workspaceTestAdapter.discoverTests(testController); + console.log(abc); sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); From cabdf39f66fa85c23b64bef429e815454094922a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 11:52:30 -0700 Subject: [PATCH 211/336] Use `optionalDependencies` instead of `peerDependencies` for `@vscode/python-extension` npm package (#21763) Closes https://github.com/microsoft/vscode-python/issues/21720 --- pythonExtensionApi/package-lock.json | 2 +- pythonExtensionApi/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index 4c5e98adbc16..f882b489d793 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -16,7 +16,7 @@ "node": ">=16.17.1", "vscode": "^1.78.0" }, - "peerDependencies": { + "optionalDependencies": { "@types/vscode": "^1.78.0" } }, diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index 271542f161ec..6359a7fe0fae 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -25,7 +25,7 @@ "bugs": { "url": "https://github.com/Microsoft/vscode-python/issues" }, - "peerDependencies": { + "optionalDependencies": { "@types/vscode": "^1.78.0" }, "devDependencies": { From 8e0e59be81eae58c8120af5453ba178e3aa40aec Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 13:37:44 -0700 Subject: [PATCH 212/336] Remove optionalDependencies from API npm package and document to install vscode types separately (#21764) Closes It still leads to conflicts due to double installation of vscode types when testing through the cases, removing vscode types as dependencies altogether and documenting to install it separately instead. --- pythonExtensionApi/README.md | 5 ++++- pythonExtensionApi/package-lock.json | 3 --- pythonExtensionApi/package.json | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md index 1587635aae40..4acd26008b24 100644 --- a/pythonExtensionApi/README.md +++ b/pythonExtensionApi/README.md @@ -17,11 +17,14 @@ First we need to define a `package.json` for the extension that wants to use the // Depend on the Python extension facade npm module to get easier API access to the // core extension. "dependencies": { - "@vscode/python-extension": "..." + "@vscode/python-extension": "...", + "@types/vscode": "..." }, } ``` +Update `"@types/vscode"` to [a recent version](https://code.visualstudio.com/updates/) of VS Code, say `"^1.81.0"` for VS Code version `"1.81"`, in case there are any conflicts. + The actual source code to get the active environment to run some script could look like this: ```typescript diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index f882b489d793..0b2abfd1895b 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -15,9 +15,6 @@ "engines": { "node": ">=16.17.1", "vscode": "^1.78.0" - }, - "optionalDependencies": { - "@types/vscode": "^1.78.0" } }, "node_modules/@types/vscode": { diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index 6359a7fe0fae..ee61029b07d2 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -25,9 +25,6 @@ "bugs": { "url": "https://github.com/Microsoft/vscode-python/issues" }, - "optionalDependencies": { - "@types/vscode": "^1.78.0" - }, "devDependencies": { "typescript": "5.0.4", "@types/vscode": "^1.78.0" From c490339df7f4de2651f18205399023e85f64ec43 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 14:35:11 -0700 Subject: [PATCH 213/336] Update version of npm package (#21765) --- pythonExtensionApi/package-lock.json | 4 ++-- pythonExtensionApi/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index 0b2abfd1895b..9b4847457b20 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vscode/python-extension", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "devDependencies": { "@types/vscode": "^1.78.0", diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index ee61029b07d2..86ac58f42f20 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.3", + "version": "1.0.4", "author": { "name": "Microsoft Corporation" }, From 3fed49f0b79feb500e2f262d55d5d214282dde22 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:38:50 -0700 Subject: [PATCH 214/336] Add Ruff to CI (#21739) Add Ruff to (lint > action.yml) CI #21738 --- .github/actions/lint/action.yml | 7 +++++ pythonFiles/installed_check.py | 8 ++--- pythonFiles/normalizeSelection.py | 2 +- pythonFiles/pyproject.toml | 31 +++++++++++++++++++ pythonFiles/shell_exec.py | 3 +- .../testing_tools/adapter/pytest/__init__.py | 4 +-- .../expected_execution_test_output.py | 4 ++- pythonFiles/tests/pytestadapter/helpers.py | 2 +- .../tests/pytestadapter/test_execution.py | 5 +-- pythonFiles/tests/test_create_microvenv.py | 3 +- pythonFiles/tests/test_create_venv.py | 5 +-- .../.data/discovery_error/file_one.py | 2 +- .../tests/unittestadapter/test_utils.py | 4 ++- pythonFiles/unittestadapter/execution.py | 7 +++-- pythonFiles/unittestadapter/utils.py | 7 +++-- .../tests/logParser.py | 9 +++--- 16 files changed, 73 insertions(+), 30 deletions(-) diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml index 9478550c107b..1efa6aab79a5 100644 --- a/.github/actions/lint/action.yml +++ b/.github/actions/lint/action.yml @@ -47,3 +47,10 @@ runs: python -m black . --check working-directory: pythonFiles shell: bash + + - name: Run Ruff + run: | + python -m pip install -U ruff + python -m ruff check . + working-directory: pythonFiles + shell: bash diff --git a/pythonFiles/installed_check.py b/pythonFiles/installed_check.py index f0e1c268d270..df5d3cb9d082 100644 --- a/pythonFiles/installed_check.py +++ b/pythonFiles/installed_check.py @@ -36,7 +36,7 @@ def parse_requirements(line: str) -> Optional[Requirement]: return req elif req.marker.evaluate(): return req - except: + except Exception: return None @@ -51,7 +51,7 @@ def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, in try: # Check if package is installed metadata(req.name) - except: + except Exception: diagnostics.append( { "line": n, @@ -79,7 +79,7 @@ def process_pyproject(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]] try: raw_text = req_file.read_text(encoding="utf-8") pyproject = tomli.loads(raw_text) - except: + except Exception: return diagnostics lines = raw_text.splitlines() @@ -91,7 +91,7 @@ def process_pyproject(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]] try: # Check if package is installed metadata(req.name) - except: + except Exception: diagnostics.append( { "line": n, diff --git a/pythonFiles/normalizeSelection.py b/pythonFiles/normalizeSelection.py index 35bc42d6e6fe..0363702717ab 100644 --- a/pythonFiles/normalizeSelection.py +++ b/pythonFiles/normalizeSelection.py @@ -118,7 +118,7 @@ def normalize_lines(selection): # Insert a newline between each top-level statement, and append a newline to the selection. source = "\n".join(statements) + "\n" - except: + except Exception: # If there's a problem when parsing statements, # append a blank line to end the block and send it as-is. source = selection + "\n\n" diff --git a/pythonFiles/pyproject.toml b/pythonFiles/pyproject.toml index 56237999e603..d865e27418e0 100644 --- a/pythonFiles/pyproject.toml +++ b/pythonFiles/pyproject.toml @@ -34,3 +34,34 @@ ignore = [ 'tests/testing_tools/adapter/pytest/test_cli.py', 'tests/testing_tools/adapter/pytest/test_discovery.py', ] + +[tool.ruff] +line-length = 140 +ignore = ["E402"] +exclude = [ + # Ignore testing_tools files same as Pyright way + 'get-pip.py', + 'install_debugpy.py', + 'tensorboard_launcher.py', + 'testlauncher.py', + 'visualstudio_py_testlauncher.py', + 'testing_tools/unittest_discovery.py', + 'testing_tools/adapter/util.py', + 'testing_tools/adapter/pytest/_discovery.py', + 'testing_tools/adapter/pytest/_pytest_item.py', + 'tests/debug_adapter/test_install_debugpy.py', + 'tests/testing_tools/adapter/.data', + 'tests/testing_tools/adapter/test___main__.py', + 'tests/testing_tools/adapter/test_discovery.py', + 'tests/testing_tools/adapter/test_functional.py', + 'tests/testing_tools/adapter/test_report.py', + 'tests/testing_tools/adapter/test_util.py', + 'tests/testing_tools/adapter/pytest/test_cli.py', + 'tests/testing_tools/adapter/pytest/test_discovery.py', + 'pythonFiles/testing_tools/*', + 'pythonFiles/testing_tools/adapter/pytest/__init__.py', + 'pythonFiles/tests/pytestadapter/expected_execution_test_output.py', + 'pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py', + 'pythonFiles/tests/unittestadapter/test_utils.py', + +] diff --git a/pythonFiles/shell_exec.py b/pythonFiles/shell_exec.py index c521586ca31b..4987399a53ea 100644 --- a/pythonFiles/shell_exec.py +++ b/pythonFiles/shell_exec.py @@ -1,9 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import os -import sys import subprocess +import sys # This is a simple solution to waiting for completion of commands sent to terminal. # 1. Intercept commands send to a terminal diff --git a/pythonFiles/testing_tools/adapter/pytest/__init__.py b/pythonFiles/testing_tools/adapter/pytest/__init__.py index e894f7bcdb8e..89b7c066a459 100644 --- a/pythonFiles/testing_tools/adapter/pytest/__init__.py +++ b/pythonFiles/testing_tools/adapter/pytest/__init__.py @@ -3,5 +3,5 @@ from __future__ import absolute_import -from ._cli import add_subparser as add_cli_subparser -from ._discovery import discover +from ._cli import add_subparser as add_cli_subparser # noqa: F401 +from ._discovery import discover # noqa: F401 diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index fe1d40a55b43..dd8f458d792e 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -5,7 +5,9 @@ TEST_ADD_FUNCTION = "unittest_folder/test_add.py::TestAddFunction::" SUCCESS = "success" FAILURE = "failure" -TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" + +TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" # noqa: E501 + # This is the expected output for the unittest_folder execute tests # └── unittest_folder diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index c3e01d52170a..47b4f75d6d60 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -10,7 +10,7 @@ import sys import threading import uuid -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index ffc84955bf54..e3b00386882d 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -4,6 +4,7 @@ import shutil import pytest + from tests.pytestadapter import expected_execution_test_output from .helpers import TEST_DATA_PATH, runner @@ -161,7 +162,7 @@ def test_pytest_execution(test_ids, expected_const): Keyword arguments: test_ids -- an array of test_ids to run. expected_const -- a dictionary of the expected output from running pytest discovery on the files. - """ + """ # noqa: E501 args = test_ids actual = runner(args) assert actual @@ -179,6 +180,6 @@ def test_pytest_execution(test_ids, expected_const): or actual_result_dict[key]["outcome"] == "error" ): actual_result_dict[key]["message"] = "ERROR MESSAGE" - if actual_result_dict[key]["traceback"] != None: + if actual_result_dict[key]["traceback"] is not None: actual_result_dict[key]["traceback"] = "TRACEBACK" assert actual_result_dict == expected_const diff --git a/pythonFiles/tests/test_create_microvenv.py b/pythonFiles/tests/test_create_microvenv.py index f123052c491c..e5d4e68802e9 100644 --- a/pythonFiles/tests/test_create_microvenv.py +++ b/pythonFiles/tests/test_create_microvenv.py @@ -6,7 +6,6 @@ import sys import create_microvenv -import pytest def test_create_microvenv(): @@ -26,4 +25,4 @@ def run_process(args, error_message): create_microvenv.run_process = run_process create_microvenv.main() - assert run_process_called == True + assert run_process_called is True diff --git a/pythonFiles/tests/test_create_venv.py b/pythonFiles/tests/test_create_venv.py index bebe304c13c3..ae3f18be6f3c 100644 --- a/pythonFiles/tests/test_create_venv.py +++ b/pythonFiles/tests/test_create_venv.py @@ -5,9 +5,10 @@ import os import sys -import create_venv import pytest +import create_venv + @pytest.mark.skipif( sys.platform == "win32", reason="Windows does not have micro venv fallback." @@ -35,7 +36,7 @@ def run_process(args, error_message): create_venv.main(["--name", ".test_venv"]) # run_process is called when the venv does not exist - assert run_process_called == True + assert run_process_called is True @pytest.mark.skipif( diff --git a/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py b/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py index 42f84f046760..031b6f6c9d68 100644 --- a/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py +++ b/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py @@ -3,7 +3,7 @@ import unittest -import something_else # type: ignore +import something_else # type: ignore # noqa: F401 class DiscoveryErrorOne(unittest.TestCase): diff --git a/pythonFiles/tests/unittestadapter/test_utils.py b/pythonFiles/tests/unittestadapter/test_utils.py index a3bc1dd7693c..e262f877d52c 100644 --- a/pythonFiles/tests/unittestadapter/test_utils.py +++ b/pythonFiles/tests/unittestadapter/test_utils.py @@ -6,6 +6,7 @@ import unittest import pytest + from unittestadapter.utils import ( TestNode, TestNodeTypeEnum, @@ -284,7 +285,8 @@ def test_build_decorated_tree() -> None: def test_build_empty_tree() -> None: - """The build_test_tree function should return None if there are no discovered test suites, and an empty list of errors if there are none in the discovered data.""" + """The build_test_tree function should return None if there are no discovered test suites, + and an empty list of errors if there are none in the discovered data.""" start_dir = os.fsdecode(TEST_DATA_PATH) pattern = "does_not_exist*" diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index dfb6928a2074..f239f81c2d87 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -17,8 +17,9 @@ sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from testing_tools import process_json_util, socket_manager from typing_extensions import NotRequired, TypeAlias, TypedDict + +from testing_tools import process_json_util, socket_manager from unittestadapter.utils import parse_unittest_args DEFAULT_PORT = "45454" @@ -194,12 +195,12 @@ def run_tests( # Discover tests at path with the file name as a pattern (if any). loader = unittest.TestLoader() - args = { + args = { # noqa: F841 "start_dir": start_dir, "pattern": pattern, "top_level_dir": top_level_dir, } - suite = loader.discover(start_dir, pattern, top_level_dir) + suite = loader.discover(start_dir, pattern, top_level_dir) # noqa: F841 # Run tests. runner = unittest.TextTestRunner(resultclass=UnittestTestResult) diff --git a/pythonFiles/unittestadapter/utils.py b/pythonFiles/unittestadapter/utils.py index 78000e2a945f..64f08217f38f 100644 --- a/pythonFiles/unittestadapter/utils.py +++ b/pythonFiles/unittestadapter/utils.py @@ -60,11 +60,11 @@ def get_source_line(obj) -> str: """Get the line number of a test case start line.""" try: sourcelines, lineno = inspect.getsourcelines(obj) - except: + except Exception: try: # tornado-specific, see https://github.com/microsoft/vscode-python/issues/17285. sourcelines, lineno = inspect.getsourcelines(obj.orig_method) - except: + except Exception: return "*" # Return the line number of the first line of the test case definition. @@ -226,7 +226,8 @@ def parse_unittest_args(args: List[str]) -> Tuple[str, str, Union[str, None]]: The returned tuple contains the following items - start_directory: The directory where to start discovery, defaults to . - pattern: The pattern to match test files, defaults to test*.py - - top_level_directory: The top-level directory of the project, defaults to None, and unittest will use start_directory behind the scenes. + - top_level_directory: The top-level directory of the project, defaults to None, + and unittest will use start_directory behind the scenes. """ arg_parser = argparse.ArgumentParser() diff --git a/pythonFiles/vscode_datascience_helpers/tests/logParser.py b/pythonFiles/vscode_datascience_helpers/tests/logParser.py index 767f837c5136..e021853fee7a 100644 --- a/pythonFiles/vscode_datascience_helpers/tests/logParser.py +++ b/pythonFiles/vscode_datascience_helpers/tests/logParser.py @@ -1,11 +1,10 @@ -from io import TextIOWrapper -import sys import argparse import os +from io import TextIOWrapper os.system("color") -from pathlib import Path import re +from pathlib import Path parser = argparse.ArgumentParser(description="Parse a test log into its parts") parser.add_argument("testlog", type=str, nargs=1, help="Log to parse") @@ -63,14 +62,14 @@ def splitByPid(testlog): pid = int(match.group(1)) # See if we've created a log for this pid or not - if not pid in pids: + if pid not in pids: pids.add(pid) logFile = "{}_{}.log".format(baseFile, pid) print("Writing to new log: " + logFile) logs[pid] = Path(logFile).open(mode="w") # Add this line to the log - if pid != None: + if pid is not None: logs[pid].write(line) # Close all of the open logs for key in logs: From 9ac14b893291a883338dd93ab84b59bc82f77280 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 15:52:29 -0700 Subject: [PATCH 215/336] Update README.md for npm package (#21766) Fix indent for https://www.npmjs.com/package/@vscode/python-extension?activeTab=readme --- pythonExtensionApi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md index 4acd26008b24..5208d90cdfa5 100644 --- a/pythonExtensionApi/README.md +++ b/pythonExtensionApi/README.md @@ -18,7 +18,7 @@ First we need to define a `package.json` for the extension that wants to use the // core extension. "dependencies": { "@vscode/python-extension": "...", - "@types/vscode": "..." + "@types/vscode": "..." }, } ``` From 0a2c28501d5bead69f8a05fcf40b0e4c0e0fd9b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 16:51:59 -0700 Subject: [PATCH 216/336] Bump brettcannon/check-for-changed-files from 1.1.1 to 1.2.0 (#21772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [brettcannon/check-for-changed-files](https://github.com/brettcannon/check-for-changed-files) from 1.1.1 to 1.2.0.
Release notes

Sourced from brettcannon/check-for-changed-files's releases.

v1.2.0

What's Changed

New Contributors

Full Changelog: https://github.com/brettcannon/check-for-changed-files/compare/v1...v1.2.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=brettcannon/check-for-changed-files&package-manager=github_actions&previous-version=1.1.1&new-version=1.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-file-check.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index 2d227ea00399..ba019c790e99 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'package-lock.json matches package.json' - uses: brettcannon/check-for-changed-files@v1.1.1 + uses: brettcannon/check-for-changed-files@v1.2.0 with: prereq-pattern: 'package.json' file-pattern: 'package-lock.json' @@ -25,7 +25,7 @@ jobs: failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - name: 'package.json matches package-lock.json' - uses: brettcannon/check-for-changed-files@v1.1.1 + uses: brettcannon/check-for-changed-files@v1.2.0 with: prereq-pattern: 'package-lock.json' file-pattern: 'package.json' @@ -33,7 +33,7 @@ jobs: failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - name: 'Tests' - uses: brettcannon/check-for-changed-files@v1.1.1 + uses: brettcannon/check-for-changed-files@v1.2.0 with: prereq-pattern: src/**/*.ts file-pattern: | From fbbf987c3affb475c2360164f4ecc1eb86e83f7b Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 9 Aug 2023 18:20:26 -0700 Subject: [PATCH 217/336] Use updated API to fetch scoped env collection (#21788) For https://github.com/microsoft/vscode/issues/171173 https://github.com/microsoft/vscode-python/issues/20822 To be merged tomorrow when latest insiders is released. Blocked on https://github.com/microsoft/vscode/pull/189979. --- package-lock.json | 16 ++++----- package.json | 4 +-- .../terminalEnvVarCollectionService.ts | 34 ++++++------------- ...rminalEnvVarCollectionService.unit.test.ts | 20 +++++------ ...scode.proposed.envCollectionWorkspace.d.ts | 33 +++++++++--------- 5 files changed, 46 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01f03cb31661..56b94d1dd44c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "@types/stack-trace": "0.0.29", "@types/tmp": "^0.0.33", "@types/uuid": "^8.3.4", - "@types/vscode": "^1.75.0", + "@types/vscode": "^1.81.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.79.0-20230526" + "vscode": "^1.81.0-20230809" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1491,9 +1491,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.75.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.75.1.tgz", - "integrity": "sha512-emg7wdsTFzdi+elvoyoA+Q8keEautdQHyY5LNmHVM4PTpY8JgOTVADrGVyXGepJ6dVW2OS5/xnLUWh+nZxvdiA==", + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.81.0.tgz", + "integrity": "sha512-YIaCwpT+O2E7WOMq0eCgBEABE++SX3Yl/O02GoMIF2DO3qAtvw7m6BXFYsxnc6XyzwZgh6/s/UG78LSSombl2w==", "dev": true }, "node_modules/@types/which": { @@ -16508,9 +16508,9 @@ "dev": true }, "@types/vscode": { - "version": "1.75.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.75.1.tgz", - "integrity": "sha512-emg7wdsTFzdi+elvoyoA+Q8keEautdQHyY5LNmHVM4PTpY8JgOTVADrGVyXGepJ6dVW2OS5/xnLUWh+nZxvdiA==", + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.81.0.tgz", + "integrity": "sha512-YIaCwpT+O2E7WOMq0eCgBEABE++SX3Yl/O02GoMIF2DO3qAtvw7m6BXFYsxnc6XyzwZgh6/s/UG78LSSombl2w==", "dev": true }, "@types/which": { diff --git a/package.json b/package.json index b6a4b65b42a9..2d78d407a0c9 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.79.0-20230526" + "vscode": "^1.81.0-20230809" }, "enableTelemetry": false, "keywords": [ @@ -2151,7 +2151,7 @@ "@types/stack-trace": "0.0.29", "@types/tmp": "^0.0.33", "@types/uuid": "^8.3.4", - "@types/vscode": "^1.75.0", + "@types/vscode": "^1.81.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index f4c29f03707d..2e5c26be0222 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -3,14 +3,7 @@ import * as path from 'path'; import { inject, injectable } from 'inversify'; -import { - ProgressOptions, - ProgressLocation, - MarkdownString, - WorkspaceFolder, - EnvironmentVariableCollection, - EnvironmentVariableScope, -} from 'vscode'; +import { ProgressOptions, ProgressLocation, MarkdownString, WorkspaceFolder } from 'vscode'; import { pathExists } from 'fs-extra'; import { IExtensionActivationService } from '../../activation/types'; import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; @@ -67,7 +60,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async activate(resource: Resource): Promise { if (!inTerminalEnvVarExperiment(this.experimentService)) { - this.context.environmentVariableCollection.clear(); + const workspaceFolder = this.getWorkspaceFolder(resource); + this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); await this.handleMicroVenv(resource); if (!this.registeredOnce) { this.interpreterService.onDidChangeInterpreter( @@ -111,8 +105,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); - const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); - // Clear any previously set env vars from collection. + const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); + // Clear any previously set env vars from collection envVarCollection.clear(); if (!settings.terminal.activateEnvironment) { traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); @@ -160,7 +154,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ return; } traceVerbose(`Setting environment variable ${key} in collection to ${value}`); - envVarCollection.replace(key, value, { applyAtShellIntegration: true }); + envVarCollection.replace(key, value, { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }); } } }); @@ -170,22 +167,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ envVarCollection.description = description; } - private getEnvironmentVariableCollection(workspaceFolder?: WorkspaceFolder) { - const envVarCollection = this.context.environmentVariableCollection as EnvironmentVariableCollection & { - getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; - }; - return workspaceFolder - ? envVarCollection.getScopedEnvironmentVariableCollection({ workspaceFolder }) - : envVarCollection; - } - private async handleMicroVenv(resource: Resource) { const workspaceFolder = this.getWorkspaceFolder(resource); const interpreter = await this.interpreterService.getActiveInterpreter(resource); if (interpreter?.envType === EnvironmentType.Venv) { const activatePath = path.join(path.dirname(interpreter.path), 'activate'); if (!(await pathExists(activatePath))) { - const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); + const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); const pathVarName = getSearchPathEnvVarNames()[0]; envVarCollection.replace( 'PATH', @@ -195,7 +183,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ return; } } - this.context.environmentVariableCollection.clear(); + this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); } private getWorkspaceFolder(resource: Resource): WorkspaceFolder | undefined { diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 458e2b28c181..73c9e82274de 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -45,7 +45,6 @@ suite('Terminal Environment Variable Collection Service', () => { let collection: EnvironmentVariableCollection & { getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; }; - let scopedCollection: EnvironmentVariableCollection; let applicationEnvironment: IApplicationEnvironment; let environmentActivationService: IEnvironmentActivationService; let workspaceService: IWorkspaceService; @@ -73,9 +72,7 @@ suite('Terminal Environment Variable Collection Service', () => { getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; } >(); - scopedCollection = mock(); - when(collection.getScopedEnvironmentVariableCollection(anything())).thenReturn(instance(scopedCollection)); - when(context.environmentVariableCollection).thenReturn(instance(collection)); + when(context.getEnvironmentVariableCollection(anything())).thenReturn(instance(collection)); experimentService = mock(); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); applicationEnvironment = mock(); @@ -95,7 +92,6 @@ suite('Terminal Environment Variable Collection Service', () => { pythonPath: displayPath, } as unknown) as IPythonSettings); when(collection.clear()).thenResolve(); - when(scopedCollection.clear()).thenResolve(); terminalEnvVarCollectionService = new TerminalEnvVarCollectionService( instance(platform), instance(interpreterService), @@ -253,15 +249,17 @@ suite('Terminal Environment Variable Collection Service', () => { environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, customShell), ).thenResolve(envVars); - when(scopedCollection.replace(anything(), anything(), anything())).thenCall((_e, _v, options) => { - assert.deepEqual(options, { applyAtShellIntegration: true }); - return Promise.resolve(); - }); + when(collection.replace(anything(), anything(), anything())).thenCall( + (_e, _v, options: EnvironmentVariableMutatorOptions) => { + assert.deepEqual(options, { applyAtShellIntegration: true, applyAtProcessCreation: true }); + return Promise.resolve(); + }, + ); await terminalEnvVarCollectionService._applyCollection(resource, customShell); - verify(scopedCollection.clear()).once(); - verify(scopedCollection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); }); test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { diff --git a/types/vscode.proposed.envCollectionWorkspace.d.ts b/types/vscode.proposed.envCollectionWorkspace.d.ts index d778e53e5086..406e5b98cd47 100644 --- a/types/vscode.proposed.envCollectionWorkspace.d.ts +++ b/types/vscode.proposed.envCollectionWorkspace.d.ts @@ -7,15 +7,22 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/182069 - // export interface ExtensionContext { - // /** - // * Gets the extension's environment variable collection for this workspace, enabling changes - // * to be applied to terminal environment variables. - // * - // * @param scope The scope to which the environment variable collection applies to. - // */ - // readonly environmentVariableCollection: EnvironmentVariableCollection & { getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection }; - // } + export interface ExtensionContext { + /** + * Gets the extension's environment variable collection for this workspace, enabling changes + * to be applied to terminal environment variables. + * + * @deprecated Use {@link getEnvironmentVariableCollection} instead. + */ + readonly environmentVariableCollection: EnvironmentVariableCollection; + /** + * Gets the extension's environment variable collection for this workspace, enabling changes + * to be applied to terminal environment variables. + * + * @param scope The scope to which the environment variable collection applies to. + */ + getEnvironmentVariableCollection(scope?: EnvironmentVariableScope): EnvironmentVariableCollection; + } export type EnvironmentVariableScope = { /** @@ -23,12 +30,4 @@ declare module 'vscode' { */ workspaceFolder?: WorkspaceFolder; }; - - export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { - /** - * A description for the environment variable collection, this will be used to describe the - * changes in the UI. - */ - description: string | MarkdownString | undefined; - } } From 835eab5c131bc2993dd7e0e1b449a11f023c3a0a Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 10 Aug 2023 14:33:06 -0700 Subject: [PATCH 218/336] Add setting to control severity of missing package diagnostic. (#21794) Closes https://github.com/microsoft/vscode-python/issues/21792 --- package.json | 15 ++++++ package.nls.json | 3 +- pythonFiles/installed_check.py | 13 +++-- pythonFiles/tests/test_installed_check.py | 53 ++++++++++++++++++- .../creation/common/installCheckUtils.ts | 22 +++++++- .../common/installCheckUtils.unit.test.ts | 51 +++++++++++++++++- 6 files changed, 148 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 2d78d407a0c9..3f98b3d08b6c 100644 --- a/package.json +++ b/package.json @@ -1147,6 +1147,21 @@ "scope": "machine", "type": "string" }, + "python.missingPackage.severity":{ + "default": "Hint", + "description": "%python.missingPackage.severity.description%", + "enum": [ + "Error", + "Hint", + "Information", + "Warning" + ], + "scope": "resource", + "type": "string", + "tags": [ + "experimental" + ] + }, "python.pipenvPath": { "default": "pipenv", "description": "%python.pipenvPath.description%", diff --git a/package.nls.json b/package.nls.json index 79609f02e83a..b8e82b150e76 100644 --- a/package.nls.json +++ b/package.nls.json @@ -200,6 +200,7 @@ "python.linting.pylintPath.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.logging.level.description": "The logging level the extension logs at, defaults to 'error'", "python.logging.level.deprecation": "This setting is deprecated. Please use command `Developer: Set Log Level...` to set logging level.", + "python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", "python.sortImports.args.description": "Arguments passed in. Each argument is a separate item in the array.", @@ -265,4 +266,4 @@ "walkthrough.step.python.createNewNotebook.altText": "Creating a new Jupyter notebook", "walkthrough.step.python.openInteractiveWindow.altText": "Opening Python interactive window", "walkthrough.step.python.dataScienceLearnMore.altText": "Image representing our documentation page and mailing list resources." -} \ No newline at end of file +} diff --git a/pythonFiles/installed_check.py b/pythonFiles/installed_check.py index df5d3cb9d082..4a43a8bc8b30 100644 --- a/pythonFiles/installed_check.py +++ b/pythonFiles/installed_check.py @@ -15,7 +15,11 @@ from importlib_metadata import metadata from packaging.requirements import Requirement -DEFAULT_SEVERITY = 3 +DEFAULT_SEVERITY = "3" # 'Hint' +try: + SEVERITY = int(os.getenv("VSCODE_MISSING_PGK_SEVERITY", DEFAULT_SEVERITY)) +except ValueError: + SEVERITY = int(DEFAULT_SEVERITY) def parse_args(argv: Optional[Sequence[str]] = None): @@ -37,7 +41,8 @@ def parse_requirements(line: str) -> Optional[Requirement]: elif req.marker.evaluate(): return req except Exception: - return None + pass + return None def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: @@ -60,7 +65,7 @@ def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, in "endCharacter": len(req.name), "package": req.name, "code": "not-installed", - "severity": DEFAULT_SEVERITY, + "severity": SEVERITY, } ) return diagnostics @@ -100,7 +105,7 @@ def process_pyproject(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]] "endCharacter": end, "package": req.name, "code": "not-installed", - "severity": DEFAULT_SEVERITY, + "severity": SEVERITY, } ) return diagnostics diff --git a/pythonFiles/tests/test_installed_check.py b/pythonFiles/tests/test_installed_check.py index f76070d197be..dae019359e08 100644 --- a/pythonFiles/tests/test_installed_check.py +++ b/pythonFiles/tests/test_installed_check.py @@ -9,7 +9,7 @@ import sys import pytest -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union SCRIPT_PATH = pathlib.Path(__file__).parent.parent / "installed_check.py" TEST_DATA = pathlib.Path(__file__).parent / "test_data" @@ -29,7 +29,12 @@ def generate_file(base_file: pathlib.Path): os.unlink(str(fullpath)) -def run_on_file(file_path: pathlib.Path) -> List[Dict[str, Union[str, int]]]: +def run_on_file( + file_path: pathlib.Path, severity: Optional[str] = None +) -> List[Dict[str, Union[str, int]]]: + env = os.environ.copy() + if severity: + env["VSCODE_MISSING_PGK_SEVERITY"] = severity result = subprocess.run( [ sys.executable, @@ -39,6 +44,7 @@ def run_on_file(file_path: pathlib.Path) -> List[Dict[str, Union[str, int]]]: stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, + env=env, ) assert result.returncode == 0 assert result.stderr == b"" @@ -88,3 +94,46 @@ def test_installed_check(test_name: str): with generate_file(base_file) as file_path: result = run_on_file(file_path) assert result == EXPECTED_DATA[test_name] + + +EXPECTED_DATA2 = { + "missing-deps": [ + { + "line": 6, + "character": 0, + "endLine": 6, + "endCharacter": 10, + "package": "flake8-csv", + "code": "not-installed", + "severity": 0, + }, + { + "line": 10, + "character": 0, + "endLine": 10, + "endCharacter": 11, + "package": "levenshtein", + "code": "not-installed", + "severity": 0, + }, + ], + "pyproject-missing-deps": [ + { + "line": 8, + "character": 34, + "endLine": 8, + "endCharacter": 44, + "package": "flake8-csv", + "code": "not-installed", + "severity": 0, + } + ], +} + + +@pytest.mark.parametrize("test_name", EXPECTED_DATA2.keys()) +def test_with_severity(test_name: str): + base_file = TEST_DATA / f"{test_name}.data" + with generate_file(base_file) as file_path: + result = run_on_file(file_path, severity="0") + assert result == EXPECTED_DATA2[test_name] diff --git a/src/client/pythonEnvironments/creation/common/installCheckUtils.ts b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts index 8c9817f83b7a..5d51b6186b11 100644 --- a/src/client/pythonEnvironments/creation/common/installCheckUtils.ts +++ b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts @@ -6,6 +6,7 @@ import { installedCheckScript } from '../../../common/process/internal/scripts'; import { plainExec } from '../../../common/process/rawProcessApis'; import { IInterpreterPathService } from '../../../common/types'; import { traceInfo, traceVerbose, traceError } from '../../../logging'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; interface PackageDiagnostic { package: string; @@ -39,6 +40,21 @@ function parseDiagnostics(data: string): Diagnostic[] { return diagnostics; } +function getMissingPackageSeverity(doc: TextDocument): number { + const config = getConfiguration('python', doc.uri); + const severity: string = config.get('missingPackage.severity', 'Hint'); + if (severity === 'Error') { + return DiagnosticSeverity.Error; + } + if (severity === 'Warning') { + return DiagnosticSeverity.Warning; + } + if (severity === 'Information') { + return DiagnosticSeverity.Information; + } + return DiagnosticSeverity.Hint; +} + export async function getInstalledPackagesDiagnostics( interpreterPathService: IInterpreterPathService, doc: TextDocument, @@ -47,7 +63,11 @@ export async function getInstalledPackagesDiagnostics( const scriptPath = installedCheckScript(); try { traceInfo('Running installed packages checker: ', interpreter, scriptPath, doc.uri.fsPath); - const result = await plainExec(interpreter, [scriptPath, doc.uri.fsPath]); + const result = await plainExec(interpreter, [scriptPath, doc.uri.fsPath], { + env: { + VSCODE_MISSING_PGK_SEVERITY: `${getMissingPackageSeverity(doc)}`, + }, + }); traceVerbose('Installed packages check result:\n', result.stdout); if (result.stderr) { traceError('Installed packages check error:\n', result.stderr); diff --git a/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts index de8e263fc3fe..4763b54a730a 100644 --- a/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts +++ b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts @@ -5,10 +5,12 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; -import { Diagnostic, TextDocument, Range, Uri } from 'vscode'; +import { Diagnostic, TextDocument, Range, Uri, WorkspaceConfiguration, ConfigurationScope } from 'vscode'; import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; import { getInstalledPackagesDiagnostics } from '../../../../client/pythonEnvironments/creation/common/installCheckUtils'; import { IInterpreterPathService } from '../../../../client/common/types'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { SpawnOptions } from '../../../../client/common/process/types'; chaiUse(chaiAsPromised); @@ -37,10 +39,20 @@ const MISSING_PACKAGES: Diagnostic[] = [ suite('Install check diagnostics tests', () => { let plainExecStub: sinon.SinonStub; let interpreterPathService: typemoq.IMock; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock; setup(() => { + configMock = typemoq.Mock.ofType(); plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); interpreterPathService = typemoq.Mock.ofType(); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + getConfigurationStub.callsFake((section?: string, _scope?: ConfigurationScope | null) => { + if (section === 'python') { + return configMock.object; + } + return undefined; + }); }); teardown(() => { @@ -48,18 +60,55 @@ suite('Install check diagnostics tests', () => { }); test('Test parse diagnostics', async () => { + configMock + .setup((c) => c.get('missingPackage.severity', 'Hint')) + .returns(() => 'Error') + .verifiable(typemoq.Times.atLeastOnce()); plainExecStub.resolves({ stdout: MISSING_PACKAGES_STR, stderr: '' }); const someFile = getSomeRequirementFile(); const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object); assert.deepStrictEqual(result, MISSING_PACKAGES); + configMock.verifyAll(); }); test('Test parse empty diagnostics', async () => { + configMock + .setup((c) => c.get('missingPackage.severity', 'Hint')) + .returns(() => 'Error') + .verifiable(typemoq.Times.atLeastOnce()); plainExecStub.resolves({ stdout: '', stderr: '' }); const someFile = getSomeRequirementFile(); const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object); assert.deepStrictEqual(result, []); + configMock.verifyAll(); + }); + + [ + ['Error', '0'], + ['Warning', '1'], + ['Information', '2'], + ['Hint', '3'], + ].forEach((severityType: string[]) => { + const setting = severityType[0]; + const expected = severityType[1]; + test(`Test missing package severity: ${setting}`, async () => { + configMock + .setup((c) => c.get('missingPackage.severity', 'Hint')) + .returns(() => setting) + .verifiable(typemoq.Times.atLeastOnce()); + let severity: string | undefined; + plainExecStub.callsFake((_cmd: string, _args: string[], options: SpawnOptions) => { + severity = options.env?.VSCODE_MISSING_PGK_SEVERITY; + return { stdout: '', stderr: '' }; + }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object); + + assert.deepStrictEqual(result, []); + assert.deepStrictEqual(severity, expected); + configMock.verifyAll(); + }); }); }); From ab8d3b22652cd08889839927442d66228d537b4c Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 10 Aug 2023 15:07:08 -0700 Subject: [PATCH 219/336] Update VS Code engine (#21799) For #11039 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56b94d1dd44c..84357451dc3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.81.0-20230809" + "vscode": "^1.82.0-20230809" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index 3f98b3d08b6c..fe247f8b25c1 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.81.0-20230809" + "vscode": "^1.82.0-20230809" }, "enableTelemetry": false, "keywords": [ From 71d6dab02ac6bf129e775afe6c5787a080f45e93 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:46:42 -0700 Subject: [PATCH 220/336] Add one more property to load event (#21800) This PR adds app name to the editor_load telemetry event --- src/client/startupTelemetry.ts | 3 +++ src/client/telemetry/index.ts | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/client/startupTelemetry.ts b/src/client/startupTelemetry.ts index f4d3fc254b67..43cf09c23c37 100644 --- a/src/client/startupTelemetry.ts +++ b/src/client/startupTelemetry.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as vscode from 'vscode'; import { IWorkspaceService } from './common/application/types'; import { isTestExecution } from './common/constants'; import { ITerminalHelper } from './common/terminal/types'; @@ -81,6 +82,7 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer): // TODO: If any one of these parts fails we send no info. We should // be able to partially populate as much as possible instead // (through granular try-catch statements). + const appName = vscode.env.appName; const workspaceService = serviceContainer.get(IWorkspaceService); const workspaceFolderCount = workspaceService.workspaceFolders?.length || 0; const terminalHelper = serviceContainer.get(ITerminalHelper); @@ -129,5 +131,6 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer): hasPythonThree, usingUserDefinedInterpreter, usingGlobalInterpreter, + appName, }; } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index f0b57a4043d9..3fcc177edea7 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -729,6 +729,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "editor.load" : { + "appName" : {"classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud"}, "codeloadingtime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, "condaversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "owner": "luabud" }, @@ -747,6 +748,10 @@ export interface IEventNamePropertyMapping { } */ [EventName.EDITOR_LOAD]: { + /** + * The name of the application where the Python extension is running + */ + appName?: string | undefined; /** * The conda version if selected */ @@ -1549,7 +1554,7 @@ export interface IEventNamePropertyMapping { * This event also has a measure, "resultLength", which records the number of completions provided. */ /* __GDPR__ - "jedi_language_server.request" : { + "jedi_language_server.request" : { "method": {"classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig"} } */ From 385bb370ce21e3b15a92fbb80067f89690cbde74 Mon Sep 17 00:00:00 2001 From: Erik De Bonte Date: Mon, 14 Aug 2023 13:53:56 -0700 Subject: [PATCH 221/336] Add `language_server.jinja_usage` to `pylance.ts` (#21809) --- src/client/telemetry/pylance.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index c59d279b98c2..afa6b5298084 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -6,6 +6,12 @@ "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ +/* __GDPR__ + "language_server.jinja_usage" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "openfileextensions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ /* __GDPR__ "language_server.ready" : { "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, From bd749aaae4519b10f493e71f179aa6535c6e987a Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 14 Aug 2023 16:24:16 -0700 Subject: [PATCH 222/336] Fix `service.test.ts` to stop disposing of all services (#21811) file `service.test.ts` was calling to dispose of all items related to the service container for clean up. This led to services in later tests failing since they were close already. Fixes here allow for new tests in the test adapter to be written. fix helps https://github.com/microsoft/vscode-python/pull/21803 --- src/test/common/configuration/service.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/test/common/configuration/service.test.ts b/src/test/common/configuration/service.test.ts index ff47500db731..c57617b2a610 100644 --- a/src/test/common/configuration/service.test.ts +++ b/src/test/common/configuration/service.test.ts @@ -2,8 +2,7 @@ // Licensed under the MIT License. import { expect } from 'chai'; import { workspace } from 'vscode'; -import { IConfigurationService, IDisposableRegistry } from '../../../client/common/types'; -import { disposeAll } from '../../../client/common/utils/resourceLifecycle'; +import { IConfigurationService, IDisposableRegistry, IExtensionContext } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { getExtensionSettings } from '../../extensionSettings'; import { initialize } from '../../initialize'; @@ -23,15 +22,17 @@ suite('Configuration Service', () => { test('Ensure async registry works', async () => { const asyncRegistry = serviceContainer.get(IDisposableRegistry); - let disposed = false; + let subs = serviceContainer.get(IExtensionContext).subscriptions; + const oldLength = subs.length; const disposable = { dispose(): Promise { - disposed = true; return Promise.resolve(); }, }; asyncRegistry.push(disposable); - await disposeAll(asyncRegistry); - expect(disposed).to.be.equal(true, "Didn't dispose during async registry cleanup"); + subs = serviceContainer.get(IExtensionContext).subscriptions; + const newLength = subs.length; + expect(newLength).to.be.equal(oldLength + 1, 'Subscription not added'); + // serviceContainer subscriptions are not disposed of as this breaks other tests that use the service container. }); }); From b447bf1cf0439e2eb7c1164220aadc7399a18606 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 14 Aug 2023 17:22:53 -0700 Subject: [PATCH 223/336] Feature branch testing overflow bug fix (#21812) This merges in two PRs that were reverted because of a bug introduced that caused subprocess overflow. reverted PRs: https://github.com/microsoft/vscode-python/pull/21667, https://github.com/microsoft/vscode-python/pull/21682 This now implements these two PRs allowing for absolute testIds and an execObservable for the subprocess. This PR also adds a bug fix and functional tests to ensure this doesn't happen again. Since this PR is large, all items in it have already been reviewed as they were merged into the feature branch. --- .../pytestadapter/.data/root/tests/pytest.ini | 0 .../pytestadapter/.data/root/tests/test_a.py | 6 + .../pytestadapter/.data/root/tests/test_b.py | 6 + .../expected_discovery_test_output.py | 516 +++++++++++++----- .../expected_execution_test_output.py | 365 ++++++++++--- pythonFiles/tests/pytestadapter/helpers.py | 16 +- .../tests/pytestadapter/test_discovery.py | 52 +- .../tests/pytestadapter/test_execution.py | 45 +- pythonFiles/vscode_pytest/__init__.py | 58 +- .../testing/testController/common/server.ts | 36 +- .../testing/testController/common/types.ts | 7 +- .../pytest/pytestDiscoveryAdapter.ts | 48 +- .../pytest/pytestExecutionAdapter.ts | 64 ++- .../unittest/testDiscoveryAdapter.ts | 20 +- .../unittest/testExecutionAdapter.ts | 22 +- src/test/linters/lint.functional.test.ts | 6 - src/test/mocks/helper.ts | 11 + src/test/mocks/mockChildProcess.ts | 239 ++++++++ .../testing/common/testingAdapter.test.ts | 428 +++++++++++++++ .../pytestDiscoveryAdapter.unit.test.ts | 76 ++- .../pytestExecutionAdapter.unit.test.ts | 118 +++- .../testController/server.unit.test.ts | 248 ++++++--- .../workspaceTestAdapter.unit.test.ts | 3 +- .../test_parameterized_subtest.py | 16 + .../smallWorkspace/test_simple.py | 12 + 25 files changed, 1993 insertions(+), 425 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py create mode 100644 src/test/mocks/helper.ts create mode 100644 src/test/mocks/mockChildProcess.ts create mode 100644 src/test/testing/common/testingAdapter.test.ts create mode 100644 src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py create mode 100644 src/testTestingRootWkspc/smallWorkspace/test_simple.py diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini b/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py new file mode 100644 index 000000000000..3ec3dd9626cb --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_a_function(): # test_marker--test_a_function + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py new file mode 100644 index 000000000000..0d3148641f85 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_b_function(): # test_marker--test_b_function + assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 91c1453dfc77..2b2c07ab8ea7 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -1,6 +1,7 @@ import os -from .helpers import TEST_DATA_PATH, find_test_line_number + +from .helpers import TEST_DATA_PATH, find_test_line_number, get_absolute_test_id # This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. @@ -18,7 +19,7 @@ # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function -simple_test_file_path = os.fspath(TEST_DATA_PATH / "simple_pytest.py") +simple_test_file_path = TEST_DATA_PATH / "simple_pytest.py" simple_discovery_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -26,20 +27,24 @@ "children": [ { "name": "simple_pytest.py", - "path": simple_test_file_path, + "path": os.fspath(simple_test_file_path), "type_": "file", - "id_": simple_test_file_path, + "id_": os.fspath(simple_test_file_path), "children": [ { "name": "test_function", - "path": simple_test_file_path, + "path": os.fspath(simple_test_file_path), "lineno": find_test_line_number( "test_function", simple_test_file_path, ), "type_": "test", - "id_": "simple_pytest.py::test_function", - "runID": "simple_pytest.py::test_function", + "id_": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), + "runID": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), } ], } @@ -52,7 +57,7 @@ # ├── TestExample # │ └── test_true_unittest # └── test_true_pytest -unit_pytest_same_file_path = os.fspath(TEST_DATA_PATH / "unittest_pytest_same_file.py") +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" unit_pytest_same_file_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -60,39 +65,51 @@ "children": [ { "name": "unittest_pytest_same_file.py", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "type_": "file", - "id_": unit_pytest_same_file_path, + "id_": os.fspath(unit_pytest_same_file_path), "children": [ { "name": "TestExample", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "type_": "class", "children": [ { "name": "test_true_unittest", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "lineno": find_test_line_number( "test_true_unittest", - unit_pytest_same_file_path, + os.fspath(unit_pytest_same_file_path), ), "type_": "test", - "id_": "unittest_pytest_same_file.py::TestExample::test_true_unittest", - "runID": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), } ], "id_": "unittest_pytest_same_file.py::TestExample", }, { "name": "test_true_pytest", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "lineno": find_test_line_number( "test_true_pytest", unit_pytest_same_file_path, ), "type_": "test", - "id_": "unittest_pytest_same_file.py::test_true_pytest", - "runID": "unittest_pytest_same_file.py::test_true_pytest", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), }, ], } @@ -124,9 +141,9 @@ # └── test_subtract_positive_numbers # │ └── TestDuplicateFunction # │ └── test_dup_s -unittest_folder_path = os.fspath(TEST_DATA_PATH / "unittest_folder") -test_add_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_add.py") -test_subtract_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_subtract.py") +unittest_folder_path = TEST_DATA_PATH / "unittest_folder" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" unittest_folder_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -134,61 +151,79 @@ "children": [ { "name": "unittest_folder", - "path": unittest_folder_path, + "path": os.fspath(unittest_folder_path), "type_": "folder", - "id_": unittest_folder_path, + "id_": os.fspath(unittest_folder_path), "children": [ { "name": "test_add.py", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "file", - "id_": test_add_path, + "id_": os.fspath(test_add_path), "children": [ { "name": "TestAddFunction", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "class", "children": [ { "name": "test_add_negative_numbers", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_add_negative_numbers", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", - "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), }, { "name": "test_add_positive_numbers", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_add_positive_numbers", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), }, ], "id_": "unittest_folder/test_add.py::TestAddFunction", }, { "name": "TestDuplicateFunction", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "class", "children": [ { "name": "test_dup_a", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_dup_a", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", - "runID": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), }, ], "id_": "unittest_folder/test_add.py::TestDuplicateFunction", @@ -197,55 +232,73 @@ }, { "name": "test_subtract.py", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "file", - "id_": test_subtract_path, + "id_": os.fspath(test_subtract_path), "children": [ { "name": "TestSubtractFunction", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "class", "children": [ { "name": "test_subtract_negative_numbers", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_subtract_negative_numbers", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", - "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), }, { "name": "test_subtract_positive_numbers", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_subtract_positive_numbers", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", - "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), }, ], "id_": "unittest_folder/test_subtract.py::TestSubtractFunction", }, { "name": "TestDuplicateFunction", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "class", "children": [ { "name": "test_dup_s", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_dup_s", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", - "runID": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), }, ], "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction", @@ -268,20 +321,23 @@ # └── test_bottom_folder.py # └── test_bottom_function_t # └── test_bottom_function_f -dual_level_nested_folder_path = os.fspath(TEST_DATA_PATH / "dual_level_nested_folder") -test_top_folder_path = os.fspath( +dual_level_nested_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" +test_top_folder_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" ) -test_nested_folder_one_path = os.fspath( + +test_nested_folder_one_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" ) -test_bottom_folder_path = os.fspath( + +test_bottom_folder_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" ) + dual_level_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -289,73 +345,97 @@ "children": [ { "name": "dual_level_nested_folder", - "path": dual_level_nested_folder_path, + "path": os.fspath(dual_level_nested_folder_path), "type_": "folder", - "id_": dual_level_nested_folder_path, + "id_": os.fspath(dual_level_nested_folder_path), "children": [ { "name": "test_top_folder.py", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "type_": "file", - "id_": test_top_folder_path, + "id_": os.fspath(test_top_folder_path), "children": [ { "name": "test_top_function_t", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "lineno": find_test_line_number( "test_top_function_t", test_top_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), }, { "name": "test_top_function_f", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "lineno": find_test_line_number( "test_top_function_f", test_top_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), }, ], }, { "name": "nested_folder_one", - "path": test_nested_folder_one_path, + "path": os.fspath(test_nested_folder_one_path), "type_": "folder", - "id_": test_nested_folder_one_path, + "id_": os.fspath(test_nested_folder_one_path), "children": [ { "name": "test_bottom_folder.py", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "type_": "file", - "id_": test_bottom_folder_path, + "id_": os.fspath(test_bottom_folder_path), "children": [ { "name": "test_bottom_function_t", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "lineno": find_test_line_number( "test_bottom_function_t", test_bottom_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), }, { "name": "test_bottom_function_f", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "lineno": find_test_line_number( "test_bottom_function_f", test_bottom_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), }, ], } @@ -374,12 +454,10 @@ # └── test_nest.py # └── test_function -folder_a_path = os.fspath(TEST_DATA_PATH / "folder_a") -folder_b_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b") -folder_a_nested_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a") -test_nest_path = os.fspath( - TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" -) +folder_a_path = TEST_DATA_PATH / "folder_a" +folder_b_path = TEST_DATA_PATH / "folder_a" / "folder_b" +folder_a_nested_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" +test_nest_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" double_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -387,38 +465,44 @@ "children": [ { "name": "folder_a", - "path": folder_a_path, + "path": os.fspath(folder_a_path), "type_": "folder", - "id_": folder_a_path, + "id_": os.fspath(folder_a_path), "children": [ { "name": "folder_b", - "path": folder_b_path, + "path": os.fspath(folder_b_path), "type_": "folder", - "id_": folder_b_path, + "id_": os.fspath(folder_b_path), "children": [ { "name": "folder_a", - "path": folder_a_nested_path, + "path": os.fspath(folder_a_nested_path), "type_": "folder", - "id_": folder_a_nested_path, + "id_": os.fspath(folder_a_nested_path), "children": [ { "name": "test_nest.py", - "path": test_nest_path, + "path": os.fspath(test_nest_path), "type_": "file", - "id_": test_nest_path, + "id_": os.fspath(test_nest_path), "children": [ { "name": "test_function", - "path": test_nest_path, + "path": os.fspath(test_nest_path), "lineno": find_test_line_number( "test_function", test_nest_path, ), "type_": "test", - "id_": "folder_a/folder_b/folder_a/test_nest.py::test_function", - "runID": "folder_a/folder_b/folder_a/test_nest.py::test_function", + "id_": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), + "runID": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), } ], } @@ -438,7 +522,7 @@ # └── [3+5-8] # └── [2+4-6] # └── [6+9-16] -parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") +parameterize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" parametrize_tests_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -446,77 +530,107 @@ "children": [ { "name": "parametrize_tests.py", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "file", - "id_": parameterize_tests_path, + "id_": os.fspath(parameterize_tests_path), "children": [ { "name": "test_adding", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "function", "id_": "parametrize_tests.py::test_adding", "children": [ { "name": "[3+5-8]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[3+5-8]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[3+5-8]", - "runID": "parametrize_tests.py::test_adding[3+5-8]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", + parameterize_tests_path, + ), }, { "name": "[2+4-6]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[2+4-6]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[2+4-6]", - "runID": "parametrize_tests.py::test_adding[2+4-6]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", + parameterize_tests_path, + ), }, { "name": "[6+9-16]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[6+9-16]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[6+9-16]", - "runID": "parametrize_tests.py::test_adding[6+9-16]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", + parameterize_tests_path, + ), }, ], }, { "name": "test_under_ten", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "function", "children": [ { "name": "[1]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_under_ten[1]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_under_ten[1]", - "runID": "parametrize_tests.py::test_under_ten[1]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[1]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[1]", + parameterize_tests_path, + ), }, { "name": "[2]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_under_ten[2]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_under_ten[2]", - "runID": "parametrize_tests.py::test_under_ten[2]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[2]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[2]", + parameterize_tests_path, + ), }, ], "id_": "parametrize_tests.py::test_under_ten", @@ -529,7 +643,7 @@ # This is the expected output for the text_docstring.txt tests. # └── text_docstring.txt -text_docstring_path = os.fspath(TEST_DATA_PATH / "text_docstring.txt") +text_docstring_path = TEST_DATA_PATH / "text_docstring.txt" doctest_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -537,20 +651,24 @@ "children": [ { "name": "text_docstring.txt", - "path": text_docstring_path, + "path": os.fspath(text_docstring_path), "type_": "file", - "id_": text_docstring_path, + "id_": os.fspath(text_docstring_path), "children": [ { "name": "text_docstring.txt", - "path": text_docstring_path, + "path": os.fspath(text_docstring_path), "lineno": find_test_line_number( "text_docstring.txt", - text_docstring_path, + os.fspath(text_docstring_path), ), "type_": "test", - "id_": "text_docstring.txt::text_docstring.txt", - "runID": "text_docstring.txt::text_docstring.txt", + "id_": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), + "runID": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), } ], } @@ -570,8 +688,8 @@ # └── [1] # └── [2] # └── [3] -param1_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param1.py") -param2_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param2.py") +param1_path = TEST_DATA_PATH / "param_same_name" / "test_param1.py" +param2_path = TEST_DATA_PATH / "param_same_name" / "test_param2.py" param_same_name_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -585,38 +703,56 @@ "children": [ { "name": "test_param1.py", - "path": param1_path, + "path": os.fspath(param1_path), "type_": "file", - "id_": param1_path, + "id_": os.fspath(param1_path), "children": [ { "name": "test_odd_even", - "path": param1_path, + "path": os.fspath(param1_path), "type_": "function", "children": [ { "name": "[a]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[a]", - "runID": "param_same_name/test_param1.py::test_odd_even[a]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), }, { "name": "[b]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[b]", - "runID": "param_same_name/test_param1.py::test_odd_even[b]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), }, { "name": "[c]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[c]", - "runID": "param_same_name/test_param1.py::test_odd_even[c]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), }, ], "id_": "param_same_name/test_param1.py::test_odd_even", @@ -625,38 +761,56 @@ }, { "name": "test_param2.py", - "path": param2_path, + "path": os.fspath(param2_path), "type_": "file", - "id_": param2_path, + "id_": os.fspath(param2_path), "children": [ { "name": "test_odd_even", - "path": param2_path, + "path": os.fspath(param2_path), "type_": "function", "children": [ { "name": "[1]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[1]", - "runID": "param_same_name/test_param2.py::test_odd_even[1]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), }, { "name": "[2]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[2]", - "runID": "param_same_name/test_param2.py::test_odd_even[2]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), }, { "name": "[3]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[3]", - "runID": "param_same_name/test_param2.py::test_odd_even[3]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), }, ], "id_": "param_same_name/test_param2.py::test_odd_even", @@ -668,3 +822,67 @@ ], "id_": TEST_DATA_PATH_STR, } + +tests_path = TEST_DATA_PATH / "root" / "tests" +tests_a_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +tests_b_path = TEST_DATA_PATH / "root" / "tests" / "test_b.py" +# This is the expected output for the root folder tests. +# └── tests +# └── test_a.py +# └── test_a_function +# └── test_b.py +# └── test_b_function +root_with_config_expected_output = { + "name": "tests", + "path": os.fspath(tests_path), + "type_": "folder", + "children": [ + { + "name": "test_a.py", + "path": os.fspath(tests_a_path), + "type_": "file", + "id_": os.fspath(tests_a_path), + "children": [ + { + "name": "test_a_function", + "path": os.fspath(os.path.join(tests_path, "test_a.py")), + "lineno": find_test_line_number( + "test_a_function", + os.path.join(tests_path, "test_a.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_a.py::test_a_function", tests_a_path + ), + "runID": get_absolute_test_id( + "tests/test_a.py::test_a_function", tests_a_path + ), + } + ], + }, + { + "name": "test_b.py", + "path": os.fspath(tests_b_path), + "type_": "file", + "id_": os.fspath(tests_b_path), + "children": [ + { + "name": "test_b_function", + "path": os.fspath(os.path.join(tests_path, "test_b.py")), + "lineno": find_test_line_number( + "test_b_function", + os.path.join(tests_path, "test_b.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_b.py::test_b_function", tests_b_path + ), + "runID": get_absolute_test_id( + "tests/test_b.py::test_b_function", tests_b_path + ), + } + ], + }, + ], + "id_": os.fspath(tests_path), +} diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index dd8f458d792e..76d21b3e2518 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -1,14 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from .helpers import TEST_DATA_PATH, get_absolute_test_id TEST_SUBTRACT_FUNCTION = "unittest_folder/test_subtract.py::TestSubtractFunction::" TEST_ADD_FUNCTION = "unittest_folder/test_add.py::TestAddFunction::" SUCCESS = "success" FAILURE = "failure" -TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" # noqa: E501 - - # This is the expected output for the unittest_folder execute tests # └── unittest_folder # ├── test_add.py @@ -19,30 +17,52 @@ # └── TestSubtractFunction # ├── test_subtract_negative_numbers: failure # └── test_subtract_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" uf_execution_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers": { - "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ), "outcome": FAILURE, "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers": { - "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -57,16 +77,26 @@ # │ └── TestAddFunction # │ ├── test_add_negative_numbers: success # │ └── test_add_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + uf_single_file_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -74,19 +104,24 @@ }, } + # This is the expected output for the unittest_folder execute only signle method # └── unittest_folder # ├── test_add.py # │ └── TestAddFunction # │ └── test_add_positive_numbers: success uf_single_method_execution_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, - } + }, } # This is the expected output for the unittest_folder tests run where two tests @@ -98,18 +133,28 @@ # └── test_subtract.py # └── TestSubtractFunction # └── test_subtract_positive_numbers: success +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + uf_non_adjacent_tests_execution_expected_output = { - TEST_SUBTRACT_FUNCTION - + "test_subtract_positive_numbers": { - "test": TEST_SUBTRACT_FUNCTION + "test_subtract_positive_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", test_subtract_path + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - TEST_ADD_FUNCTION - + "test_add_positive_numbers": { - "test": TEST_ADD_FUNCTION + "test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -117,12 +162,15 @@ }, } + # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function: success +simple_pytest_path = TEST_DATA_PATH / "unittest_folder" / "simple_pytest.py" + simple_execution_pytest_expected_output = { - "simple_pytest.py::test_function": { - "test": "simple_pytest.py::test_function", + get_absolute_test_id("test_function", simple_pytest_path): { + "test": get_absolute_test_id("test_function", simple_pytest_path), "outcome": "success", "message": None, "traceback": None, @@ -130,21 +178,34 @@ } } + # This is the expected output for the unittest_pytest_same_file.py file. # ├── unittest_pytest_same_file.py # ├── TestExample # │ └── test_true_unittest: success # └── test_true_pytest: success +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" unit_pytest_same_file_execution_expected_output = { - "unittest_pytest_same_file.py::TestExample::test_true_unittest": { - "test": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_pytest_same_file.py::test_true_pytest": { - "test": "unittest_pytest_same_file.py::test_true_pytest", + get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", unit_pytest_same_file_path + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), "outcome": "success", "message": None, "traceback": None, @@ -156,9 +217,15 @@ # └── error_raise_exception.py # ├── TestSomething # │ └── test_a: failure +error_raised_exception_path = TEST_DATA_PATH / "error_raise_exception.py" error_raised_exception_execution_expected_output = { - "error_raise_exception.py::TestSomething::test_a": { - "test": "error_raise_exception.py::TestSomething::test_a", + get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", error_raised_exception_path + ): { + "test": get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", + error_raised_exception_path, + ), "outcome": "error", "message": "ERROR MESSAGE", "traceback": "TRACEBACK", @@ -174,44 +241,60 @@ # ├── TestClass # │ └── test_class_function_a: skipped # │ └── test_class_function_b: skipped + +skip_tests_path = TEST_DATA_PATH / "skip_tests.py" skip_tests_execution_expected_output = { - "skip_tests.py::test_something": { - "test": "skip_tests.py::test_something", + get_absolute_test_id("skip_tests.py::test_something", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_something", skip_tests_path), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_another_thing": { - "test": "skip_tests.py::test_another_thing", + get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_another_thing", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_decorator_thing": { - "test": "skip_tests.py::test_decorator_thing", + get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_decorator_thing", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_decorator_thing_2": { - "test": "skip_tests.py::test_decorator_thing_2", + get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_decorator_thing_2", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::TestClass::test_class_function_a": { - "test": "skip_tests.py::TestClass::test_class_function_a", + get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_a", skip_tests_path + ): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_a", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::TestClass::test_class_function_b": { - "test": "skip_tests.py::TestClass::test_class_function_b", + get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_b", skip_tests_path + ): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_b", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, @@ -229,30 +312,59 @@ # └── test_bottom_folder.py # └── test_bottom_function_t: success # └── test_bottom_function_f: failure +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH + / "dual_level_nested_folder" + / "nested_folder_one" + / "test_bottom_folder.py" +) dual_level_nested_folder_execution_expected_output = { - "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -266,38 +378,59 @@ # └── folder_a # └── test_nest.py # └── test_function: success + +nested_folder_path = ( + TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +) double_nested_folder_expected_execution_output = { - "folder_a/folder_b/folder_a/test_nest.py::test_function": { - "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", + get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ): { + "test": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, } } - # This is the expected output for the nested_folder tests. # └── parametrize_tests.py # └── test_adding[3+5-8]: success # └── test_adding[2+4-6]: success # └── test_adding[6+9-16]: failure +parametrize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" + parametrize_tests_expected_execution_output = { - "parametrize_tests.py::test_adding[3+5-8]": { - "test": "parametrize_tests.py::test_adding[3+5-8]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "parametrize_tests.py::test_adding[2+4-6]": { - "test": "parametrize_tests.py::test_adding[2+4-6]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "parametrize_tests.py::test_adding[6+9-16]": { - "test": "parametrize_tests.py::test_adding[6+9-16]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -309,8 +442,12 @@ # └── parametrize_tests.py # └── test_adding[3+5-8]: success single_parametrize_tests_expected_execution_output = { - "parametrize_tests.py::test_adding[3+5-8]": { - "test": "parametrize_tests.py::test_adding[3+5-8]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, @@ -321,9 +458,12 @@ # This is the expected output for the single parameterized tests. # └── text_docstring.txt # └── text_docstring: success +doc_test_path = TEST_DATA_PATH / "text_docstring.txt" doctest_pytest_expected_execution_output = { - "text_docstring.txt::text_docstring.txt": { - "test": "text_docstring.txt::text_docstring.txt", + get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path): { + "test": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", doc_test_path + ), "outcome": "success", "message": None, "traceback": None, @@ -332,68 +472,127 @@ } # Will run all tests in the cwd that fit the test file naming pattern. +folder_a_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH + / "dual_level_nested_folder" + / "nested_folder_one" + / "test_bottom_folder.py" +) +unittest_folder_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +unittest_folder_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" + no_test_ids_pytest_execution_expected_output = { - "folder_a/folder_b/folder_a/test_nest.py::test_function": { - "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", + get_absolute_test_id("test_function", folder_a_path): { + "test": get_absolute_test_id("test_function", folder_a_path), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id( + "test_top_function_t", dual_level_nested_folder_top_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id( + "test_top_function_f", dual_level_nested_folder_top_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + get_absolute_test_id( + "test_bottom_function_t", dual_level_nested_folder_bottom_path + ): { + "test": get_absolute_test_id( + "test_bottom_function_t", dual_level_nested_folder_bottom_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + get_absolute_test_id( + "test_bottom_function_f", dual_level_nested_folder_bottom_path + ): { + "test": get_absolute_test_id( + "test_bottom_function_f", dual_level_nested_folder_bottom_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers": { - "test": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + get_absolute_test_id( + "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path + ): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers": { - "test": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + get_absolute_test_id( + "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path + ): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers": { - "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers": { - "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, } + +# This is the expected output for the root folder with the config file referenced. +# └── test_a.py +# └── test_a_function: success +test_add_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +config_file_pytest_expected_execution_output = { + get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path): { + "test": get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index 47b4f75d6d60..7195cfe43ea5 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -16,6 +16,13 @@ from typing_extensions import TypedDict +def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: + split_id = test_id.split("::")[1:] + absolute_test_id = "::".join([str(testPath), *split_id]) + print("absolute path", absolute_test_id) + return absolute_test_id + + def create_server( host: str = "127.0.0.1", port: int = 0, @@ -104,6 +111,13 @@ def process_rpc_json(data: str) -> List[Dict[str, Any]]: def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: + """Run the pytest discovery and return the JSON data from the server.""" + return runner_with_cwd(args, TEST_DATA_PATH) + + +def runner_with_cwd( + args: List[str], path: pathlib.Path +) -> Optional[List[Dict[str, Any]]]: """Run the pytest discovery and return the JSON data from the server.""" process_args: List[str] = [ sys.executable, @@ -134,7 +148,7 @@ def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: t2 = threading.Thread( target=_run_test_code, - args=(process_args, env, TEST_DATA_PATH, completed), + args=(process_args, env, path, completed), ) t2.start() diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 5288c7ad769e..8d785be27c8b 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -7,7 +7,7 @@ import pytest from . import expected_discovery_test_output -from .helpers import TEST_DATA_PATH, runner +from .helpers import TEST_DATA_PATH, runner, runner_with_cwd def test_import_error(tmp_path): @@ -153,3 +153,53 @@ def test_pytest_collect(file, expected_const): assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) assert actual["tests"] == expected_const + + +def test_pytest_root_dir(): + """ + Test to test pytest discovery with the command line arg --rootdir specified to be a subfolder + of the workspace root. Discovery should succeed and testids should be relative to workspace root. + """ + rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" + actual = runner_with_cwd( + [ + "--collect-only", + rd, + ], + TEST_DATA_PATH / "root", + ) + if actual: + actual = actual[0] + assert actual + assert all(item in actual for item in ("status", "cwd", "tests")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert ( + actual["tests"] + == expected_discovery_test_output.root_with_config_expected_output + ) + + +def test_pytest_config_file(): + """ + Test to test pytest discovery with the command line arg -c with a specified config file which + changes the workspace root. Discovery should succeed and testids should be relative to workspace root. + """ + actual = runner_with_cwd( + [ + "--collect-only", + "-c", + "tests/pytest.ini", + ], + TEST_DATA_PATH / "root", + ) + if actual: + actual = actual[0] + assert actual + assert all(item in actual for item in ("status", "cwd", "tests")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert ( + actual["tests"] + == expected_discovery_test_output.root_with_config_expected_output + ) diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index e3b00386882d..07354b01709b 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -7,7 +7,50 @@ from tests.pytestadapter import expected_execution_test_output -from .helpers import TEST_DATA_PATH, runner +from .helpers import TEST_DATA_PATH, runner, runner_with_cwd + + +def test_config_file(): + """Test pytest execution when a config file is specified.""" + args = [ + "-c", + "tests/pytest.ini", + str(TEST_DATA_PATH / "root" / "tests" / "test_a.py::test_a_function"), + ] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = ( + expected_execution_test_output.config_file_pytest_expected_execution_output + ) + assert actual + assert len(actual) == len(expected_const) + actual_result_dict = dict() + for a in actual: + assert all(item in a for item in ("status", "cwd", "result")) + assert a["status"] == "success" + assert a["cwd"] == os.fspath(new_cwd) + actual_result_dict.update(a["result"]) + assert actual_result_dict == expected_const + + +def test_rootdir_specified(): + """Test pytest execution when a --rootdir is specified.""" + rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" + args = [rd, "tests/test_a.py::test_a_function"] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = ( + expected_execution_test_output.config_file_pytest_expected_execution_output + ) + assert actual + assert len(actual) == len(expected_const) + actual_result_dict = dict() + for a in actual: + assert all(item in a for item in ("status", "cwd", "result")) + assert a["status"] == "success" + assert a["cwd"] == os.fspath(new_cwd) + actual_result_dict.update(a["result"]) + assert actual_result_dict == expected_const def test_syntax_error_execution(tmp_path): diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 1ac287a8410a..49d429662e3a 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -69,8 +69,7 @@ def pytest_exception_interact(node, call, report): """ # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. - # See if it is during discovery or execution. - # if discovery, then add the error to error logs. + # If it is during discovery, then add the error to error logs. if type(report) == pytest.CollectReport: if call.excinfo and call.excinfo.typename != "AssertionError": if report.outcome == "skipped" and "SkipTest" in str(call): @@ -83,11 +82,11 @@ def pytest_exception_interact(node, call, report): report.longreprtext + "\n Check Python Test Logs for more details." ) else: - # if execution, send this data that the given node failed. + # If during execution, send this data that the given node failed. report_value = "error" if call.excinfo.typename == "AssertionError": report_value = "failure" - node_id = str(node.nodeid) + node_id = get_absolute_test_id(node.nodeid, get_node_path(node)) if node_id not in collected_tests_so_far: collected_tests_so_far.append(node_id) item_result = create_test_outcome( @@ -106,6 +105,22 @@ def pytest_exception_interact(node, call, report): ) +def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: + """A function that returns the absolute test id. This is necessary because testIds are relative to the rootdir. + This does not work for our case since testIds when referenced during run time are relative to the instantiation + location. Absolute paths for testIds are necessary for the test tree ensures configurations that change the rootdir + of pytest are handled correctly. + + Keyword arguments: + test_id -- the pytest id of the test which is relative to the rootdir. + testPath -- the path to the file the test is located in, as a pathlib.Path object. + """ + split_id = test_id.split("::")[1:] + absolute_test_id = "::".join([str(testPath), *split_id]) + print("absolute path", absolute_test_id) + return absolute_test_id + + def pytest_keyboard_interrupt(excinfo): """A pytest hook that is called when a keyboard interrupt is raised. @@ -130,7 +145,7 @@ class TestOutcome(Dict): def create_test_outcome( - test: str, + testid: str, outcome: str, message: Union[str, None], traceback: Union[str, None], @@ -138,7 +153,7 @@ def create_test_outcome( ) -> TestOutcome: """A function that creates a TestOutcome object.""" return TestOutcome( - test=test, + test=testid, outcome=outcome, message=message, traceback=traceback, # TODO: traceback @@ -154,6 +169,7 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): IS_DISCOVERY = False +map_id_to_path = dict() def pytest_load_initial_conftests(early_config, parser, args): @@ -184,17 +200,21 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - node_id = str(report.nodeid) - if node_id not in collected_tests_so_far: - collected_tests_so_far.append(node_id) + node_path = map_id_to_path[report.nodeid] + if not node_path: + node_path = cwd + # Calculate the absolute test id and use this as the ID moving forward. + absolute_node_id = get_absolute_test_id(report.nodeid, node_path) + if absolute_node_id not in collected_tests_so_far: + collected_tests_so_far.append(absolute_node_id) item_result = create_test_outcome( - node_id, + absolute_node_id, report_value, message, traceback, ) collected_test = testRunResultDict() - collected_test[node_id] = item_result + collected_test[absolute_node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -211,21 +231,22 @@ def pytest_report_teststatus(report, config): def pytest_runtest_protocol(item, nextitem): + map_id_to_path[item.nodeid] = get_node_path(item) skipped = check_skipped_wrapper(item) if skipped: - node_id = str(item.nodeid) + absolute_node_id = get_absolute_test_id(item.nodeid, get_node_path(item)) report_value = "skipped" cwd = pathlib.Path.cwd() - if node_id not in collected_tests_so_far: - collected_tests_so_far.append(node_id) + if absolute_node_id not in collected_tests_so_far: + collected_tests_so_far.append(absolute_node_id) item_result = create_test_outcome( - node_id, + absolute_node_id, report_value, None, None, ) collected_test = testRunResultDict() - collected_test[node_id] = item_result + collected_test[absolute_node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -471,13 +492,14 @@ def create_test_node( test_case_loc: str = ( str(test_case.location[1] + 1) if (test_case.location[1] is not None) else "" ) + absolute_test_id = get_absolute_test_id(test_case.nodeid, get_node_path(test_case)) return { "name": test_case.name, "path": get_node_path(test_case), "lineno": test_case_loc, "type_": "test", - "id_": test_case.nodeid, - "runID": test_case.nodeid, + "id_": absolute_test_id, + "runID": absolute_test_id, } diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index f854371ffc35..661290bf4988 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -3,10 +3,11 @@ import * as net from 'net'; import * as crypto from 'crypto'; -import { Disposable, Event, EventEmitter } from 'vscode'; +import { Disposable, Event, EventEmitter, TestRun } from 'vscode'; import * as path from 'path'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -15,6 +16,7 @@ import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; +import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); @@ -140,7 +142,12 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDataReceived.event; } - async sendCommand(options: TestCommandOptions, runTestIdPort?: string, callback?: () => void): Promise { + async sendCommand( + options: TestCommandOptions, + runTestIdPort?: string, + runInstance?: TestRun, + callback?: () => void, + ): Promise { const { uuid } = options; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; @@ -154,7 +161,7 @@ export class PythonTestServer implements ITestServer, Disposable { }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; - const isRun = !options.testIds; + const isRun = runTestIdPort !== undefined; // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, @@ -195,7 +202,28 @@ export class PythonTestServer implements ITestServer, Disposable { // This means it is running discovery traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } - await execService.exec(args, spawnOptions); + const deferred = createDeferred>(); + + const result = execService.execObservable(args, spawnOptions); + + runInstance?.token.onCancellationRequested(() => { + result?.proc?.kill(); + }); + + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + result?.proc?.stdout?.on('data', (data) => { + spawnOptions?.outputChannel?.append(data); + }); + result?.proc?.stderr?.on('data', (data) => { + spawnOptions?.outputChannel?.append(data); + }); + result?.proc?.on('exit', () => { + traceLog('Exec server closed.', uuid); + deferred.resolve({ stdout: '', stderr: '' }); + callback?.(); + }); + await deferred.promise; } } catch (ex) { this.uuids = this.uuids.filter((u) => u !== uuid); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index d4e54951bfd7..16c0bd0e3cee 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -174,7 +174,12 @@ export interface ITestServer { readonly onDataReceived: Event; readonly onRunDataReceived: Event; readonly onDiscoveryDataReceived: Event; - sendCommand(options: TestCommandOptions, runTestIdsPort?: string, callback?: () => void): Promise; + sendCommand( + options: TestCommandOptions, + runTestIdsPort?: string, + runInstance?: TestRun, + callback?: () => void, + ): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index b83224d4161b..44ab3746dde4 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -4,13 +4,14 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceError, traceVerbose } from '../../../logging'; +import { traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, @@ -32,27 +33,30 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { const settings = this.configSettings.getSettings(uri); + const uuid = this.testServer.createUUID(uri.fsPath); const { pytestArgs } = settings.testing; traceVerbose(pytestArgs); - const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { - // cancelation token ? + const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); }); + const disposeDataReceiver = function (testServer: ITestServer) { + testServer.deleteUUID(uuid); + dataReceivedDisposable.dispose(); + }; try { - await this.runPytestDiscovery(uri, executionFactory); + await this.runPytestDiscovery(uri, uuid, executionFactory); } finally { - disposable.dispose(); + disposeDataReceiver(this.testServer); } // this is only a placeholder to handle function overloading until rewrite is finished const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; return discoveryPayload; } - async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + async runPytestDiscovery(uri: Uri, uuid: string, executionFactory?: IPythonExecutionFactory): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - const uuid = this.testServer.createUUID(uri.fsPath); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; @@ -78,17 +82,23 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. - execService - ?.exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) - .then(() => { - this.testServer.deleteUUID(uuid); - return deferred.resolve(); - }) - .catch((err) => { - traceError(`Error while trying to run pytest discovery, \n${err}\r\n\r\n`); - this.testServer.deleteUUID(uuid); - return deferred.reject(err); - }); - return deferred.promise; + const deferredExec = createDeferred>(); + const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + const result = execService?.execObservable(execArgs, spawnOptions); + + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + result?.proc?.stdout?.on('data', (data) => { + spawnOptions.outputChannel?.append(data); + }); + result?.proc?.stderr?.on('data', (data) => { + spawnOptions.outputChannel?.append(data); + }); + result?.proc?.on('exit', () => { + deferredExec.resolve({ stdout: '', stderr: '' }); + deferred.resolve(); + }); + + await deferredExec.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index a75a6089627c..4a9a57b16fed 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -15,6 +15,7 @@ import { } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -22,13 +23,7 @@ import { removePositionalFoldersAndFiles } from './arguments'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; -import { startTestIdServer } from '../common/utils'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; -/** - * Wrapper Class for pytest test execution.. - */ +import * as utils from '../common/utils'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( @@ -48,18 +43,29 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); traceVerbose(uri, testIds, debugBool); - const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const dataReceivedDisposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - try { - await this.runTestsNew(uri, testIds, uuid, debugBool, executionFactory, debugLauncher); - } finally { - this.testServer.deleteUUID(uuid); - disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) - } + const disposeDataReceiver = function (testServer: ITestServer) { + testServer.deleteUUID(uuid); + dataReceivedDisposable.dispose(); + }; + runInstance?.token.onCancellationRequested(() => { + disposeDataReceiver(this.testServer); + }); + await this.runTestsNew( + uri, + testIds, + uuid, + runInstance, + debugBool, + executionFactory, + debugLauncher, + disposeDataReceiver, + ); + // placeholder until after the rewrite is adopted // TODO: remove after adoption. const executionPayload: ExecutionTestPayload = { @@ -74,9 +80,11 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, + runInstance?: TestRun, debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, + disposeDataReceiver?: (testServer: ITestServer) => void, ): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; @@ -124,7 +132,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); - const pytestRunTestIdsPort = await startTestIdServer(testIds); + const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); @@ -143,6 +151,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { deferred.resolve(); + this.testServer.deleteUUID(uuid); }); } else { // combine path to run script with run args @@ -150,7 +159,28 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const runArgs = [scriptPath, ...testArgs]; traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); - await execService?.exec(runArgs, spawnOptions); + const deferredExec = createDeferred>(); + const result = execService?.execObservable(runArgs, spawnOptions); + + runInstance?.token.onCancellationRequested(() => { + result?.proc?.kill(); + }); + + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + result?.proc?.stdout?.on('data', (data) => { + this.outputChannel?.append(data); + }); + result?.proc?.stderr?.on('data', (data) => { + this.outputChannel?.append(data); + }); + + result?.proc?.on('exit', () => { + deferredExec.resolve({ stdout: '', stderr: '' }); + deferred.resolve(); + disposeDataReceiver?.(this.testServer); + }); + await deferredExec.promise; } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 6deca55117ea..1cbad7ef65ef 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -43,15 +43,17 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { outChannel: this.outputChannel, }; - const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { + const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); }); - try { - await this.callSendCommand(options); - } finally { - this.testServer.deleteUUID(uuid); - disposable.dispose(); - } + const disposeDataReceiver = function (testServer: ITestServer) { + testServer.deleteUUID(uuid); + dataReceivedDisposable.dispose(); + }; + + await this.callSendCommand(options, () => { + disposeDataReceiver(this.testServer); + }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { @@ -61,8 +63,8 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - private async callSendCommand(options: TestCommandOptions): Promise { - await this.testServer.sendCommand(options); + private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { + await this.testServer.sendCommand(options, undefined, undefined, callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 4cab941c2608..9af9e593c246 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -37,18 +37,19 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - try { - await this.runTestsNew(uri, testIds, uuid, debugBool); - } finally { - this.testServer.deleteUUID(uuid); - disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) - } + const disposeDataReceiver = function (testServer: ITestServer) { + testServer.deleteUUID(uuid); + disposedDataReceived.dispose(); + }; + runInstance?.token.onCancellationRequested(() => { + disposeDataReceiver(this.testServer); + }); + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, disposeDataReceiver); const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } @@ -57,7 +58,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, + runInstance?: TestRun, debugBool?: boolean, + disposeDataReceiver?: (testServer: ITestServer) => void, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -80,8 +83,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), () => { + await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, () => { deferred.resolve(); + disposeDataReceiver?.(this.testServer); }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/test/linters/lint.functional.test.ts b/src/test/linters/lint.functional.test.ts index a3dc70b7c21e..9887cbc5605a 100644 --- a/src/test/linters/lint.functional.test.ts +++ b/src/test/linters/lint.functional.test.ts @@ -4,7 +4,6 @@ 'use strict'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; @@ -780,11 +779,6 @@ suite('Linting Functional Tests', () => { teardown(() => { sinon.restore(); }); - - const pythonPath = childProcess.execSync(`"${PYTHON_PATH}" -c "import sys;print(sys.executable)"`); - - console.log(`Testing linter with python ${pythonPath}`); - // These are integration tests that mock out everything except // the filesystem and process execution. diff --git a/src/test/mocks/helper.ts b/src/test/mocks/helper.ts new file mode 100644 index 000000000000..24d7a8290b18 --- /dev/null +++ b/src/test/mocks/helper.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Readable } from 'stream'; + +export class FakeReadableStream extends Readable { + _read(_size: unknown): void | null { + // custom reading logic here + this.push(null); // end the stream + } +} diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts new file mode 100644 index 000000000000..a46d66d79ca0 --- /dev/null +++ b/src/test/mocks/mockChildProcess.ts @@ -0,0 +1,239 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Serializable, SendHandle, MessageOptions } from 'child_process'; +import { EventEmitter } from 'node:events'; +import { Writable, Readable, Pipe } from 'stream'; +import { FakeReadableStream } from './helper'; + +export class MockChildProcess extends EventEmitter { + constructor(spawnfile: string, spawnargs: string[]) { + super(); + this.spawnfile = spawnfile; + this.spawnargs = spawnargs; + this.stdin = new Writable(); + this.stdout = new FakeReadableStream(); + this.stderr = new FakeReadableStream(); + this.channel = null; + this.stdio = [this.stdin, this.stdout, this.stdout, this.stderr, null]; + this.killed = false; + this.connected = false; + this.exitCode = null; + this.signalCode = null; + this.eventMap = new Map(); + } + + stdin: Writable | null; + + stdout: Readable | null; + + stderr: Readable | null; + + eventMap: Map; + + readonly channel?: Pipe | null | undefined; + + readonly stdio: [ + Writable | null, + // stdin + Readable | null, + // stdout + Readable | null, + // stderr + Readable | Writable | null | undefined, + // extra + Readable | Writable | null | undefined, // extra + ]; + + readonly killed: boolean; + + readonly pid?: number | undefined; + + readonly connected: boolean; + + readonly exitCode: number | null; + + readonly signalCode: NodeJS.Signals | null; + + readonly spawnargs: string[]; + + readonly spawnfile: string; + + signal?: NodeJS.Signals | number; + + send(message: Serializable, callback?: (error: Error | null) => void): boolean; + + send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean; + + send( + message: Serializable, + sendHandle?: SendHandle, + options?: MessageOptions, + callback?: (error: Error | null) => void, + ): boolean; + + send( + message: Serializable, + _sendHandleOrCallback?: SendHandle | ((error: Error | null) => void), + _optionsOrCallback?: MessageOptions | ((error: Error | null) => void), + _callback?: (error: Error | null) => void, + ): boolean { + // Implementation of the send method + // For example, you might want to emit a 'message' event + this.stdout?.push(message.toString()); + return true; + } + + // eslint-disable-next-line class-methods-use-this + disconnect(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + unref(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + ref(): void { + /* noop */ + } + + addListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'disconnect', listener: () => void): this; + + addListener(event: 'error', listener: (err: Error) => void): this; + + addListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + addListener(event: 'spawn', listener: () => void): this; + + addListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + emit(event: 'close', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'disconnect'): boolean; + + emit(event: 'error', err: Error): boolean; + + emit(event: 'exit', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'message', message: Serializable, sendHandle: SendHandle): boolean; + + emit(event: 'spawn', listener: () => void): boolean; + + emit(event: string | symbol, ...args: unknown[]): boolean { + if (this.eventMap.has(event.toString())) { + this.eventMap.get(event.toString()).forEach((listener: (arg0: unknown) => void) => { + const argsArray = Array.isArray(args) ? args : [args]; + listener(argsArray); + }); + } + return true; + } + + on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'disconnect', listener: () => void): this; + + on(event: 'error', listener: (err: Error) => void): this; + + on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + on(event: 'spawn', listener: () => void): this; + + on(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'disconnect', listener: () => void): this; + + once(event: 'error', listener: (err: Error) => void): this; + + once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + once(event: 'spawn', listener: () => void): this; + + once(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'disconnect', listener: () => void): this; + + prependListener(event: 'error', listener: (err: Error) => void): this; + + prependListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependListener(event: 'spawn', listener: () => void): this; + + prependListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependOnceListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'disconnect', listener: () => void): this; + + prependOnceListener(event: 'error', listener: (err: Error) => void): this; + + prependOnceListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependOnceListener(event: 'spawn', listener: () => void): this; + + prependOnceListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + trigger(event: string): Array { + if (this.eventMap.has(event)) { + return this.eventMap.get(event); + } + return []; + } + + kill(_signal?: NodeJS.Signals | number): boolean { + this.stdout?.destroy(); + return true; + } +} diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts new file mode 100644 index 000000000000..5c92c7cf3941 --- /dev/null +++ b/src/test/testing/common/testingAdapter.test.ts @@ -0,0 +1,428 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { TestRun, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as path from 'path'; +import * as assert from 'assert'; +import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import { ITestResultResolver, ITestServer } from '../../../client/testing/testController/common/types'; +import { PythonTestServer } from '../../../client/testing/testController/common/server'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; +import { traceError, traceLog } from '../../../client/logging'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; + +suite('End to End Tests: test adapters', () => { + let resultResolver: typeMoq.IMock; + let pythonTestServer: ITestServer; + let pythonExecFactory: IPythonExecutionFactory; + let debugLauncher: ITestDebugLauncher; + let configService: IConfigurationService; + let testOutputChannel: ITestOutputChannel; + let serviceContainer: IServiceContainer; + let workspaceUri: Uri; + const rootPathSmallWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'smallWorkspace', + ); + const rootPathLargeWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'largeWorkspace', + ); + suiteSetup(async () => { + serviceContainer = (await initialize()).serviceContainer; + }); + + setup(async () => { + // create objects that were injected + configService = serviceContainer.get(IConfigurationService); + pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); + debugLauncher = serviceContainer.get(ITestDebugLauncher); + testOutputChannel = serviceContainer.get(ITestOutputChannel); + + // create mock resultResolver object + resultResolver = typeMoq.Mock.ofType(); + + // create objects that were not injected + pythonTestServer = new PythonTestServer(pythonExecFactory, debugLauncher); + await pythonTestServer.serverReady(); + }); + test('unittest discovery adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + tests: unknown; + }; + resultResolver + .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveDiscovery ${data}`); + actualData = data; + return Promise.resolve(); + }); + + // set workspace to test workspace folder and set up settings + workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run unittest discovery + const discoveryAdapter = new UnittestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + + await discoveryAdapter.discoverTests(workspaceUri).finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + }); + }); + + test('unittest discovery adapter large workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + tests: unknown; + }; + resultResolver + .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveDiscovery ${data}`); + actualData = data; + return Promise.resolve(); + }); + + // set settings to work for the given workspace + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run discovery + const discoveryAdapter = new UnittestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + + await discoveryAdapter.discoverTests(workspaceUri).finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + }); + }); + test('pytest discovery adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + tests: unknown; + }; + resultResolver + .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveDiscovery ${data}`); + actualData = data; + return Promise.resolve(); + }); + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + }); + }); + test('pytest discovery adapter large workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + tests: unknown; + }; + resultResolver + .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveDiscovery ${data}`); + actualData = data; + return Promise.resolve(); + }); + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + }); + }); + test('unittest execution adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + result: unknown; + }; + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveExecution ${data}`); + actualData = data; + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run execution + const executionAdapter = new UnittestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests(workspaceUri, ['test_simple.SimpleClass.test_simple_unit'], false, testRun.object) + .finally(() => { + // verification after execution is complete + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm tests are found + assert.ok(actualData.result, 'Expected results to be present'); + }); + }); + test('unittest execution adapter large workspace', async () => { + // result resolver and saved data for assertions + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceError(`resolveExecution ${data}`); + console.log(`resolveExecution ${data}`); + traceLog(`resolveExecution ${data}`); + // do the following asserts for each time resolveExecution is called, should be called once per test. + // 1. Check the status, can be subtest success or failure + assert( + data.status === 'subtest-success' || data.status === 'subtest-failure', + "Expected status to be 'subtest-success' or 'subtest-failure'", + ); + // 2. Confirm tests are found + assert.ok(data.result, 'Expected results to be present'); + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run unittest execution + const executionAdapter = new UnittestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests(workspaceUri, ['test_parameterized_subtest.NumbersTest.test_even'], false, testRun.object) + .finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.exactly(200), + ); + }); + }); + test('pytest execution adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + result: unknown; + }; + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveExecution ${data}`); + actualData = data; + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathSmallWorkspace}/test_simple.py::test_a`], + false, + testRun.object, + pythonExecFactory, + ) + .finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error, null, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.result, 'Expected results to be present'); + }); + }); + test('pytest execution adapter large workspace', async () => { + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveExecution ${data}`); + // do the following asserts for each time resolveExecution is called, should be called once per test. + // 1. Check the status is "success" + assert.strictEqual(data.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(data.error, null, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(data.result, 'Expected results to be present'); + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + + // generate list of test_ids + const testIds: string[] = []; + for (let i = 0; i < 200; i = i + 1) { + const testId = `${rootPathLargeWorkspace}/test_parameterized_subtest.py::test_odd_even[${i}]`; + testIds.push(testId); + } + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { + // resolve execution should be called 200 times since there are 200 tests run. + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.exactly(200), + ); + }); + }); +}); diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 18212b2d1032..8ba7dd9a6f00 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; import { ITestServer } from '../../../../client/testing/testController/common/types'; @@ -12,9 +13,11 @@ import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions, + Output, } from '../../../../client/common/process/types'; -import { createDeferred, Deferred } from '../../../../client/common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { Deferred, createDeferred } from '../../../../client/common/utils/async'; suite('pytest test discovery adapter', () => { let testServer: typeMoq.IMock; @@ -29,6 +32,7 @@ suite('pytest test discovery adapter', () => { let expectedPath: string; let uri: Uri; let expectedExtraVariables: Record; + let mockProc: MockChildProcess; setup(() => { const mockExtensionRootDir = typeMoq.Mock.ofType(); @@ -66,32 +70,46 @@ suite('pytest test discovery adapter', () => { }), } as unknown) as IConfigurationService; - // set up exec factory - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => Promise.resolve(execService.object)); - - // set up exec service + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); - deferred = createDeferred(); + const output = new Observable>(() => { + /* no op */ + }); execService - .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve({ stdout: '{}' }); - }); + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); outputChannel = typeMoq.Mock.ofType(); }); test('Discovery should call exec with correct basic args', async () => { + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - await adapter.discoverTests(uri, execFactory.object); - const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); + // verification + const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); @@ -108,16 +126,34 @@ suite('pytest test discovery adapter', () => { const expectedPathNew = path.join('other', 'path'); const configServiceNew: IConfigurationService = ({ getSettings: () => ({ - testing: { pytestArgs: ['.', 'abc', 'xyz'], cwd: expectedPathNew }, + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPathNew, + }, }), } as unknown) as IConfigurationService; + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configServiceNew, outputChannel.object); - await adapter.discoverTests(uri, execFactory.object); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); + + // verification const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.', 'abc', 'xyz']; execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 44116fd753b0..43b763f56e6c 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -6,11 +6,13 @@ import { TestRun, Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as sinon from 'sinon'; import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { ITestServer } from '../../../../client/testing/testController/common/types'; import { IPythonExecutionFactory, IPythonExecutionService, + Output, SpawnOptions, } from '../../../../client/common/process/types'; import { createDeferred, Deferred } from '../../../../client/common/utils/async'; @@ -18,6 +20,7 @@ import { PytestTestExecutionAdapter } from '../../../../client/testing/testContr import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; import * as util from '../../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; suite('pytest test execution adapter', () => { let testServer: typeMoq.IMock; @@ -29,8 +32,8 @@ suite('pytest test execution adapter', () => { let debugLauncher: typeMoq.IMock; (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; - let startTestIdServerStub: sinon.SinonStub; - + let mockProc: MockChildProcess; + let utilsStub: sinon.SinonStub; setup(() => { testServer = typeMoq.Mock.ofType(); testServer.setup((t) => t.getPort()).returns(() => 12345); @@ -47,8 +50,24 @@ suite('pytest test execution adapter', () => { }), isTestExecution: () => false, } as unknown) as IConfigurationService; - execFactory = typeMoq.Mock.ofType(); + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execFactory = typeMoq.Mock.ofType(); + utilsStub = sinon.stub(util, 'startTestIdServer'); debugLauncher = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) @@ -66,7 +85,6 @@ suite('pytest test execution adapter', () => { deferred.resolve(); return Promise.resolve(); }); - startTestIdServerStub = sinon.stub(util, 'startTestIdServer').returns(Promise.resolve(54321)); execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); @@ -77,10 +95,25 @@ suite('pytest test execution adapter', () => { sinon.restore(); }); test('startTestIdServer called with correct testIds', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -88,19 +121,38 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - const testIds = ['test1id', 'test2id']; - await adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); + adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); - sinon.assert.calledWithExactly(startTestIdServerStub, testIds); + // add in await and trigger + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); + + // assert + sinon.assert.calledWithExactly(utilsStub, testIds); }); test('pytest execution called with correct args', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -108,9 +160,12 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -123,7 +178,7 @@ suite('pytest test execution adapter', () => { // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -139,6 +194,21 @@ suite('pytest test execution adapter', () => { ); }); test('pytest execution respects settings.testing.cwd when present', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const newCwd = path.join('new', 'path'); configService = ({ getSettings: () => ({ @@ -149,7 +219,7 @@ suite('pytest test execution adapter', () => { const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -157,9 +227,12 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -172,7 +245,7 @@ suite('pytest test execution adapter', () => { execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -188,10 +261,17 @@ suite('pytest test execution adapter', () => { ); }); test('Debug launched correctly for pytest', async () => { + const deferred3 = createDeferred(); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -199,9 +279,9 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); await adapter.runTests(uri, [], true, testRun.object, execFactory.object, debugLauncher.object); + await deferred3.promise; debugLauncher.verify( (x) => x.launchDebugger( diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 1131c26c6444..53c2b72e40f7 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -1,15 +1,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as assert from 'assert'; import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; import { OutputChannel, Uri } from 'vscode'; -import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions } from '../../../client/common/process/types'; +import { Observable } from 'rxjs'; +import * as typeMoq from 'typemoq'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; suite('Python Test Server', () => { const fakeUuid = 'fake-uuid'; @@ -18,10 +27,12 @@ suite('Python Test Server', () => { let stubExecutionService: IPythonExecutionService; let server: PythonTestServer; let sandbox: sinon.SinonSandbox; - let execArgs: string[]; - let spawnOptions: SpawnOptions; let v4Stub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock; + let deferred: Deferred; + let execFactory = typeMoq.Mock.ofType(); setup(() => { sandbox = sinon.createSandbox(); @@ -29,27 +40,42 @@ suite('Python Test Server', () => { v4Stub.returns(fakeUuid); stubExecutionService = ({ - exec: (args: string[], spawnOptionsProvided: SpawnOptions) => { - execArgs = args; - spawnOptions = spawnOptionsProvided; - return Promise.resolve({ stdout: '', stderr: '' }); - }, + execObservable: () => Promise.resolve({ stdout: '', stderr: '' }), } as unknown) as IPythonExecutionService; stubExecutionFactory = ({ createActivatedEnvironment: () => Promise.resolve(stubExecutionService), } as unknown) as IPythonExecutionFactory; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + execService = typeMoq.Mock.ofType(); + const output = new Observable>(() => { + /* no op */ + }); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); }); teardown(() => { sandbox.restore(); - execArgs = []; server.dispose(); }); test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, + command: { + script: 'myscript', + args: ['-foo', 'foo'], + }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, @@ -59,17 +85,31 @@ suite('Python Test Server', () => { outputChannel: undefined, token: undefined, throwOnStdErr: true, - extraVariables: { PYTHONPATH: '/foo/bar', RUN_TEST_IDS_PORT: '56789' }, + extraVariables: { + PYTHONPATH: '/foo/bar', + RUN_TEST_IDS_PORT: '56789', + }, } as SpawnOptions; + const deferred2 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - await server.sendCommand(options, '56789'); - const port = server.getPort(); + server.sendCommand(options, '56789'); + // add in await and trigger + await deferred2.promise; + mockProc.trigger('close'); - assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); - assert.deepStrictEqual(spawnOptions, expectedSpawnOptions); + const port = server.getPort(); + const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']; + execService.verify((x) => x.execObservable(expectedArgs, expectedSpawnOptions), typeMoq.Times.once()); }); test('sendCommand should write to an output channel if it is provided as an option', async () => { @@ -80,17 +120,31 @@ suite('Python Test Server', () => { }, } as OutputChannel; const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, + command: { + script: 'myscript', + args: ['-foo', 'foo'], + }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, outChannel, }; + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - await server.sendCommand(options); + server.sendCommand(options); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); const port = server.getPort(); const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); @@ -99,13 +153,12 @@ suite('Python Test Server', () => { }); test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { - let eventData: { status: string; errors: string[] }; + let eventData: { status: string; errors: string[] } | undefined; stubExecutionService = ({ - exec: () => { + execObservable: () => { throw new Error('Failed to execute'); }, } as unknown) as IPythonExecutionService; - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -122,30 +175,43 @@ suite('Python Test Server', () => { await server.sendCommand(options); - assert.deepStrictEqual(eventData!.status, 'error'); - assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); + assert.notEqual(eventData, undefined); + assert.deepStrictEqual(eventData?.status, 'error'); + assert.deepStrictEqual(eventData?.errors, ['Failed to execute']); }); test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + + deferred = createDeferred(); + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -161,16 +227,17 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); + // add in await and trigger await deferred.promise; + mockProc.trigger('close'); + assert.deepStrictEqual(eventData, ''); }); test('If the server doesnt recognize the UUID it should ignore it', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -178,14 +245,28 @@ suite('Python Test Server', () => { uuid: fakeUuid, }; - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -201,7 +282,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -212,23 +293,34 @@ suite('Python Test Server', () => { test('Error if payload does not have a content length header', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -244,7 +336,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -267,7 +359,6 @@ Request-uuid: UUID_HERE // Your test logic here let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, @@ -275,15 +366,28 @@ Request-uuid: UUID_HERE cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); payload = payload.replace('UUID_HERE', uuid); @@ -301,7 +405,7 @@ Request-uuid: UUID_HERE console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, expectedResult); }); @@ -310,8 +414,29 @@ Request-uuid: UUID_HERE test('Calls run resolver if the result header is in the payload', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { + client.connect(server.getPort()); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -319,14 +444,6 @@ Request-uuid: UUID_HERE uuid: fakeUuid, }; - stubExecutionService = ({ - exec: async () => { - client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); - }, - } as unknown) as IPythonExecutionService; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); server.onRunDataReceived(({ data }) => { @@ -349,9 +466,8 @@ Request-uuid: ${uuid} console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; - console.log('event data', eventData); const expectedResult = '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; assert.deepStrictEqual(eventData, expectedResult); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index 5a2e48130746..41cd1bbd7ef2 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -164,8 +164,7 @@ suite('Workspace test adapter', () => { const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); const testProvider = 'unittest'; - const abc = await workspaceTestAdapter.discoverTests(testController); - console.log(abc); + await workspaceTestAdapter.discoverTests(testController); sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); diff --git a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py new file mode 100644 index 000000000000..3e84df0a2d9f --- /dev/null +++ b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest +import unittest + + +@pytest.mark.parametrize("num", range(0, 200)) +def test_odd_even(num): + return num % 2 == 0 + + +class NumbersTest(unittest.TestCase): + def test_even(self): + for i in range(0, 200): + with self.subTest(i=i): + self.assertEqual(i % 2, 0) diff --git a/src/testTestingRootWkspc/smallWorkspace/test_simple.py b/src/testTestingRootWkspc/smallWorkspace/test_simple.py new file mode 100644 index 000000000000..6b4f7bd2f8a6 --- /dev/null +++ b/src/testTestingRootWkspc/smallWorkspace/test_simple.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + + +def test_a(): + assert 1 == 1 + + +class SimpleClass(unittest.TestCase): + def test_simple_unit(self): + assert True From 9c740b9557d62e21b062a35ff4a945835fe45c64 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 14 Aug 2023 17:50:34 -0700 Subject: [PATCH 224/336] Show notification reaffirming Python extension still handles activation when in `pythonTerminalEnvVarActivation` experiment (#21802) Closes https://github.com/microsoft/vscode-python/issues/21793 Only show notification when terminal prompt does not already indicate that env is activated. --- src/client/activation/extensionSurvey.ts | 4 +- src/client/common/experiments/helpers.ts | 6 + src/client/common/utils/localize.ts | 5 +- .../terminalEnvVarCollectionPrompt.ts | 80 +++++++ .../terminalEnvVarCollectionService.ts | 65 +++++- src/client/interpreter/activation/types.ts | 8 + src/client/interpreter/serviceRegistry.ts | 12 +- src/client/pythonEnvironments/info/index.ts | 3 +- src/client/telemetry/index.ts | 6 +- .../activation/extensionSurvey.unit.test.ts | 2 +- .../checks/macPythonInterpreter.unit.test.ts | 2 +- .../common/installer/installer.unit.test.ts | 12 +- ...erminalEnvVarCollectionPrompt.unit.test.ts | 203 ++++++++++++++++++ ...rminalEnvVarCollectionService.unit.test.ts | 157 ++++++++++++++ .../virtualEnvs/virtualEnvPrompt.unit.test.ts | 2 +- 15 files changed, 545 insertions(+), 22 deletions(-) create mode 100644 src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts create mode 100644 src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts diff --git a/src/client/activation/extensionSurvey.ts b/src/client/activation/extensionSurvey.ts index 6d1d784237ba..11b581a27252 100644 --- a/src/client/activation/extensionSurvey.ts +++ b/src/client/activation/extensionSurvey.ts @@ -83,10 +83,10 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService @traceDecoratorError('Failed to display prompt for extension survey') public async showSurvey() { const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; - const telemetrySelections: ['Yes', 'Maybe later', 'Do not show again'] = [ + const telemetrySelections: ['Yes', 'Maybe later', "Don't show again"] = [ 'Yes', 'Maybe later', - 'Do not show again', + "Don't show again", ]; const selection = await this.appShell.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts); sendTelemetryEvent(EventName.EXTENSION_SURVEY_PROMPT, undefined, { diff --git a/src/client/common/experiments/helpers.ts b/src/client/common/experiments/helpers.ts index 04da948fd15d..bae96b222eb6 100644 --- a/src/client/common/experiments/helpers.ts +++ b/src/client/common/experiments/helpers.ts @@ -3,10 +3,16 @@ 'use strict'; +import { env, workspace } from 'vscode'; import { IExperimentService } from '../types'; import { TerminalEnvVarActivation } from './groups'; +import { isTestExecution } from '../constants'; export function inTerminalEnvVarExperiment(experimentService: IExperimentService): boolean { + if (!isTestExecution() && workspace.workspaceFile && env.remoteName) { + // TODO: Remove this if statement once https://github.com/microsoft/vscode/issues/180486 is fixed. + return false; + } if (!experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)) { return false; } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index b3af0c476957..3ec1829201f8 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -63,7 +63,7 @@ export namespace Common { export const openOutputPanel = l10n.t('Show output'); export const noIWillDoItLater = l10n.t('No, I will do it later'); export const notNow = l10n.t('Not now'); - export const doNotShowAgain = l10n.t('Do not show again'); + export const doNotShowAgain = l10n.t("Don't show again"); export const reload = l10n.t('Reload'); export const moreInfo = l10n.t('More Info'); export const learnMore = l10n.t('Learn more'); @@ -198,6 +198,9 @@ export namespace Interpreters { ); export const activatingTerminals = l10n.t('Reactivating terminals...'); export const activateTerminalDescription = l10n.t('Activated environment for'); + export const terminalEnvVarCollectionPrompt = l10n.t( + 'The Python extension automatically activates all terminals using the selected environment. You can hover over the terminal tab to see more information about the activation. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', + ); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', ); diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts new file mode 100644 index 000000000000..7833f34ce2fb --- /dev/null +++ b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IPersistentStateFactory, +} from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { ITerminalEnvVarCollectionService } from './types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; + +export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; + +@injectable() +export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService, + @inject(ITerminalEnvVarCollectionService) + private readonly terminalEnvVarCollectionService: ITerminalEnvVarCollectionService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) {} + + public async activate(): Promise { + if (!inTerminalEnvVarExperiment(this.experimentService)) { + return; + } + this.disposableRegistry.push( + this.terminalManager.onDidOpenTerminal(async (terminal) => { + const cwd = + 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd + ? terminal.creationOptions.cwd + : this.activeResourceService.getActiveResource(); + const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; + const settings = this.configurationService.getSettings(resource); + if (!settings.terminal.activateEnvironment) { + return; + } + if (this.terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)) { + // No need to show notification if terminal prompt already indicates when env is activated. + return; + } + await this.notifyUsers(); + }), + ); + } + + private async notifyUsers(): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + terminalEnvCollectionPromptKey, + true, + ); + if (!notificationPromptEnabled.value) { + return; + } + const prompts = [Common.doNotShowAgain]; + const selection = await this.appShell.showInformationMessage( + Interpreters.terminalEnvVarCollectionPrompt, + ...prompts, + ); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await notificationPromptEnabled.updateValue(false); + } + } +} diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 2e5c26be0222..a45306132bf4 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -23,13 +23,15 @@ import { Interpreters } from '../../common/utils/localize'; import { traceDecoratorVerbose, traceVerbose } from '../../logging'; import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; -import { IEnvironmentActivationService } from './types'; +import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; import { EnvironmentType } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; +import { TerminalShellType } from '../../common/terminal/types'; +import { OSType } from '../../common/utils/platform'; @injectable() -export class TerminalEnvVarCollectionService implements IExtensionActivationService { +export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false, @@ -127,6 +129,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ await this._applyCollection(resource, defaultShell?.shell); return; } + await this.trackTerminalPrompt(shell, resource, env); this.processEnvVars = undefined; return; } @@ -146,6 +149,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (prevValue !== value) { if (value !== undefined) { if (key === 'PS1') { + // We cannot have the full PS1 without executing in terminal, which we do not. Hence prepend it. traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); envVarCollection.prepend(key, value, { applyAtShellIntegration: true, @@ -165,6 +169,61 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); envVarCollection.description = description; + + await this.trackTerminalPrompt(shell, resource, env); + } + + private isPromptSet = new Map(); + + // eslint-disable-next-line class-methods-use-this + public isTerminalPromptSetCorrectly(resource?: Resource): boolean { + const workspaceFolder = this.getWorkspaceFolder(resource); + return !!this.isPromptSet.get(workspaceFolder?.index); + } + + /** + * Call this once we know terminal prompt is set correctly for terminal owned by this resource. + */ + private terminalPromptIsCorrect(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.set(key, true); + } + + private terminalPromptIsUnknown(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.delete(key); + } + + /** + * Tracks whether prompt for terminal was correctly set. + */ + private async trackTerminalPrompt(shell: string, resource: Resource, env: EnvironmentVariables | undefined) { + this.terminalPromptIsUnknown(resource); + if (!env) { + this.terminalPromptIsCorrect(resource); + return; + } + // Prompts for these shells cannot be set reliably using variables + const exceptionShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.fish, + TerminalShellType.zsh, // TODO: Remove this once https://github.com/microsoft/vscode/issues/188875 is fixed + ]; + const customShellType = identifyShellFromShellPath(shell); + if (exceptionShells.includes(customShellType)) { + return; + } + if (this.platform.osType !== OSType.Windows) { + // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const shouldPS1BeSet = interpreter?.type !== undefined; + if (shouldPS1BeSet && !env.PS1) { + // PS1 should be set but no PS1 was set. + return; + } + } + this.terminalPromptIsCorrect(resource); } private async handleMicroVenv(resource: Resource) { @@ -178,7 +237,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ envVarCollection.replace( 'PATH', `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, - { applyAtShellIntegration: true }, + { applyAtShellIntegration: true, applyAtProcessCreation: true }, ); return; } diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts index e00ef9b62b3f..2b364cbeb862 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -21,3 +21,11 @@ export interface IEnvironmentActivationService { interpreter?: PythonEnvironment, ): Promise; } + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 04af15415b04..018e7abfdc46 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -6,8 +6,9 @@ import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { EnvironmentActivationService } from './activation/service'; +import { TerminalEnvVarCollectionPrompt } from './activation/terminalEnvVarCollectionPrompt'; import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService'; -import { IEnvironmentActivationService } from './activation/types'; +import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types'; @@ -109,8 +110,13 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); - serviceManager.addSingleton( - IExtensionActivationService, + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService, ); + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalEnvVarCollectionPrompt, + ); } diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index 60628f61314e..ee2ff9d7cc22 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -4,6 +4,7 @@ 'use strict'; import { Architecture } from '../../common/utils/platform'; +import { PythonEnvType } from '../base/info'; import { PythonVersion } from './pythonVersion'; /** @@ -85,7 +86,7 @@ export type PythonEnvironment = InterpreterInformation & { envName?: string; envPath?: string; cachedEntry?: boolean; - type?: string; + type?: PythonEnvType; }; /** diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 3fcc177edea7..a068f21d2327 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -950,7 +950,7 @@ export interface IEventNamePropertyMapping { tool?: LinterId; /** * `select` When 'Select linter' option is selected - * `disablePrompt` When 'Do not show again' option is selected + * `disablePrompt` When "Don't show again" option is selected * `install` When 'Install' option is selected * * @type {('select' | 'disablePrompt' | 'install')} @@ -1374,7 +1374,7 @@ export interface IEventNamePropertyMapping { /** * `Yes` When 'Yes' option is selected * `No` When 'No' option is selected - * `Ignore` When 'Do not show again' option is clicked + * `Ignore` When "Don't show again" option is clicked * * @type {('Yes' | 'No' | 'Ignore' | undefined)} */ @@ -1571,7 +1571,7 @@ export interface IEventNamePropertyMapping { /** * Carries the selection of user when they are asked to take the extension survey */ - selection: 'Yes' | 'Maybe later' | 'Do not show again' | undefined; + selection: 'Yes' | 'Maybe later' | "Don't show again" | undefined; }; /** * Telemetry event sent when starting REPL diff --git a/src/test/activation/extensionSurvey.unit.test.ts b/src/test/activation/extensionSurvey.unit.test.ts index 6449eae24f31..ba96b917aff3 100644 --- a/src/test/activation/extensionSurvey.unit.test.ts +++ b/src/test/activation/extensionSurvey.unit.test.ts @@ -355,7 +355,7 @@ suite('Extension survey prompt - showSurvey()', () => { platformService.verifyAll(); }); - test("Disable prompt if 'Do not show again' option is clicked", async () => { + test('Disable prompt if "Don\'t show again" option is clicked', async () => { const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); appShell diff --git a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts index f75375520ec8..ba2436d0ffeb 100644 --- a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts @@ -220,7 +220,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); expect(messagePrompt!.commandPrompts).to.be.deep.equal([ { prompt: 'Select Python Interpreter', command: cmd }, - { prompt: 'Do not show again', command: cmdIgnore }, + { prompt: "Don't show again", command: cmdIgnore }, ]); }); test('Should not display a message if No Interpreters diagnostic has been ignored', async () => { diff --git a/src/test/common/installer/installer.unit.test.ts b/src/test/common/installer/installer.unit.test.ts index bdd7ab32a028..38b9d9174472 100644 --- a/src/test/common/installer/installer.unit.test.ts +++ b/src/test/common/installer/installer.unit.test.ts @@ -307,10 +307,10 @@ suite('Module Installer only', () => { TypeMoq.It.isAnyString(), TypeMoq.It.isValue('Install'), TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue('Do not show again'), + TypeMoq.It.isValue("Don't show again"), ), ) - .returns(async () => 'Do not show again') + .returns(async () => "Don't show again") .verifiable(TypeMoq.Times.once()); const persistVal = TypeMoq.Mock.ofType>(); let mockPersistVal = false; @@ -367,7 +367,7 @@ suite('Module Installer only', () => { TypeMoq.It.isAnyString(), TypeMoq.It.isValue('Install'), TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue('Do not show again'), + TypeMoq.It.isValue("Don't show again"), ), ) .returns(async () => undefined) @@ -864,7 +864,7 @@ suite('Module Installer only', () => { test('Ensure 3 options for pylint', async () => { const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; + const options = ['Select Linter', "Don't show again"]; const productName = ProductNames.get(product)!; await installer.promptToInstallImplementation(product, resource); @@ -875,7 +875,7 @@ suite('Module Installer only', () => { }); test('Ensure select linter command is invoked', async () => { const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; + const options = ['Select Linter', "Don't show again"]; const productName = ProductNames.get(product)!; when( appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), @@ -892,7 +892,7 @@ suite('Module Installer only', () => { }); test('If install button is selected, install linter and return response', async () => { const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; + const options = ['Select Linter', "Don't show again"]; const productName = ProductNames.get(product)!; when( appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts new file mode 100644 index 000000000000..8edaa00dcf73 --- /dev/null +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; +import { EventEmitter, Terminal, Uri } from 'vscode'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../../client/common/application/types'; +import { + IConfigurationService, + IExperimentService, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, +} from '../../../client/common/types'; +import { TerminalEnvVarCollectionPrompt } from '../../../client/interpreter/activation/terminalEnvVarCollectionPrompt'; +import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/activation/types'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; +import { sleep } from '../../core'; + +suite('Terminal Environment Variable Collection Prompt', () => { + let shell: IApplicationShell; + let terminalManager: ITerminalManager; + let experimentService: IExperimentService; + let activeResourceService: IActiveResourceService; + let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; + let persistentStateFactory: IPersistentStateFactory; + let terminalEnvVarCollectionPrompt: TerminalEnvVarCollectionPrompt; + let terminalEventEmitter: EventEmitter; + let notificationEnabled: IPersistentState; + let configurationService: IConfigurationService; + const prompts = [Common.doNotShowAgain]; + const message = Interpreters.terminalEnvVarCollectionPrompt; + + setup(async () => { + shell = mock(); + terminalManager = mock(); + experimentService = mock(); + activeResourceService = mock(); + persistentStateFactory = mock(); + terminalEnvVarCollectionService = mock(); + configurationService = mock(); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: true, + }, + } as unknown) as IPythonSettings); + notificationEnabled = mock>(); + terminalEventEmitter = new EventEmitter(); + when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( + instance(notificationEnabled), + ); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); + terminalEnvVarCollectionPrompt = new TerminalEnvVarCollectionPrompt( + instance(shell), + instance(persistentStateFactory), + instance(terminalManager), + [], + instance(activeResourceService), + instance(terminalEnvVarCollectionService), + instance(configurationService), + instance(experimentService), + ); + }); + + test('Show notification when a new terminal is opened for which there is no prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).once(); + }); + + test('Do not show notification if automatic terminal activation is turned off', async () => { + reset(configurationService); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: false, + }, + } as unknown) as IPythonSettings); + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test('When not in experiment, do not show notification for the same', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test('Do not show notification if notification is disabled', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(false); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test('Do not show notification when a new terminal is opened for which there is prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(true); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test("Disable notification if `Don't show again` is clicked", async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(message, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Do not disable notification if prompt is closed', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(message, ...prompts)).thenReturn(Promise.resolve(undefined)); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).never(); + }); +}); diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 73c9e82274de..86823f16a8c8 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -35,6 +35,8 @@ import { TerminalEnvVarCollectionService } from '../../../client/interpreter/act import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Terminal Environment Variable Collection Service', () => { let platform: IPlatformService; @@ -262,6 +264,161 @@ suite('Terminal Environment Variable Collection Service', () => { verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); }); + test('Correct track that prompt was set for non-Windows bash where PS1 is set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'bash'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for non-Windows zsh where PS1 is set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was not set for non-Windows where PS1 is not set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was set correctly for global interpreters', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: undefined, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(undefined); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was set for Windows when not using powershell', async () => { + when(platform.osType).thenReturn(OSType.Windows); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'cmd'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for Windows when using powershell', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'powershell'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { when( environmentActivationService.getActivatedEnvironmentVariables( diff --git a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts index 9671e393dc43..2ad67831c455 100644 --- a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts @@ -223,7 +223,7 @@ suite('Virtual Environment Prompt', () => { notificationPromptEnabled.verifyAll(); }); - test("If user selects 'Do not show again', prompt is disabled", async () => { + test('If user selects "Don\'t show again", prompt is disabled', async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; From 0248fa8b32e8b8e15d682550243e36443a5d8193 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 14 Aug 2023 22:20:52 -0700 Subject: [PATCH 225/336] fixing failing tests on CI (#21814) fixing https://github.com/microsoft/vscode-python/issues/21813 --- src/test/testing/common/testingAdapter.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 5c92c7cf3941..1334085e4cea 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -276,7 +276,6 @@ suite('End to End Tests: test adapters', () => { .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { traceError(`resolveExecution ${data}`); - console.log(`resolveExecution ${data}`); traceLog(`resolveExecution ${data}`); // do the following asserts for each time resolveExecution is called, should be called once per test. // 1. Check the status, can be subtest success or failure @@ -315,7 +314,7 @@ suite('End to End Tests: test adapters', () => { // verification after discovery is complete resultResolver.verify( (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.exactly(200), + typeMoq.Times.atLeastOnce(), ); }); }); From 5140a8d3e5181785b11448620a66969afbeb089a Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 14 Aug 2023 22:30:09 -0700 Subject: [PATCH 226/336] Apply API recommendations for Create Env API (#21804) Closes https://github.com/microsoft/vscode-python/issues/21090 --- .../creation/createEnvironment.ts | 17 ++-- .../creation/proposed.createEnvApis.ts | 99 +++++++++++++------ .../provider/condaCreationProvider.ts | 23 +++-- .../creation/provider/venvCreationProvider.ts | 21 ++-- .../condaCreationProvider.unit.test.ts | 2 - .../venvCreationProvider.unit.test.ts | 2 - 6 files changed, 104 insertions(+), 60 deletions(-) diff --git a/src/client/pythonEnvironments/creation/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts index 4593ff1abf92..f026a1a82ccd 100644 --- a/src/client/pythonEnvironments/creation/createEnvironment.ts +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -33,14 +33,12 @@ function fireStartedEvent(options?: CreateEnvironmentOptions): void { } function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: Error): void { - onCreateEnvironmentExitedEvent.fire({ - options, - workspaceFolder: result?.workspaceFolder, - path: result?.path, - action: result?.action, - error: error || result?.error, - }); startedEventCount -= 1; + if (result) { + onCreateEnvironmentExitedEvent.fire({ options, ...result }); + } else if (error) { + onCreateEnvironmentExitedEvent.fire({ options, error }); + } } export function getCreationEvents(): { @@ -195,5 +193,8 @@ export async function handleCreateEnvironmentCommand( } } - return result; + if (result) { + return Object.freeze(result); + } + return undefined; } diff --git a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts index 0120b2a6e8d7..ea520fdd27e2 100644 --- a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts +++ b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts @@ -40,40 +40,83 @@ export interface EnvironmentWillCreateEvent { /** * Options used to create a Python environment. */ - options: CreateEnvironmentOptions | undefined; + readonly options: CreateEnvironmentOptions | undefined; } +export type CreateEnvironmentResult = + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action?: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error?: Error; + } + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path?: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error?: Error; + } + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path?: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action?: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error: Error; + }; + /** * Params passed on `onDidCreateEnvironment` event handler. */ -export interface EnvironmentDidCreateEvent extends CreateEnvironmentResult { +export type EnvironmentDidCreateEvent = CreateEnvironmentResult & { /** * Options used to create the Python environment. */ - options: CreateEnvironmentOptions | undefined; -} - -export interface CreateEnvironmentResult { - /** - * Workspace folder associated with the environment. - */ - workspaceFolder: WorkspaceFolder | undefined; - - /** - * Path to the executable python in the environment - */ - path: string | undefined; - - /** - * User action that resulted in exit from the create environment flow. - */ - action: CreateEnvironmentUserActions | undefined; - - /** - * Error if any occurred during environment creation. - */ - error: Error | undefined; -} + readonly options: CreateEnvironmentOptions | undefined; +}; /** * Extensions that want to contribute their own environment creation can do that by registering an object @@ -120,14 +163,14 @@ export interface ProposedCreateEnvironmentAPI { * provider (including internal providers). This will also receive any options passed in * or defaults used to create environment. */ - onWillCreateEnvironment: Event; + readonly onWillCreateEnvironment: Event; /** * This API can be used to detect when the environment provider exits for any registered * provider (including internal providers). This will also receive created environment path, * any errors, or user actions taken from the provider. */ - onDidCreateEnvironment: Event; + readonly onDidCreateEnvironment: Event; /** * This API will show a QuickPick to select an environment provider from available list of diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 39cd40afd41a..74a42808e609 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -4,7 +4,7 @@ import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; import * as path from 'path'; import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; -import { traceError, traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog } from '../../../logging'; import { CreateEnvironmentProgress } from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { execObservable } from '../../../common/process/rawProcessApis'; @@ -77,7 +77,7 @@ async function createCondaEnv( args: string[], progress: CreateEnvironmentProgress, token?: CancellationToken, -): Promise { +): Promise { progress.report({ message: CreateEnv.Conda.creating, }); @@ -174,6 +174,7 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise => { - let hasError = false; - progress.report({ message: CreateEnv.statusStarting, }); @@ -237,17 +237,20 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise Python for more info.'); + } else { + throw new Error('A workspace is needed to create conda environment'); } } catch (ex) { traceError(ex); - hasError = true; + showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); throw ex; - } finally { - if (hasError) { - showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); - } } - return { path: envPath, workspaceFolder: workspace, action: undefined, error: undefined }; }, ); } diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index b00682c3cb5d..6c310fd68331 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -8,7 +8,7 @@ import { createVenvScript } from '../../../common/process/internal/scripts'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; import { Common, CreateEnv } from '../../../common/utils/localize'; -import { traceError, traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { CreateEnvironmentProgress } from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { IInterpreterQuickPick } from '../../../interpreter/configuration/types'; @@ -144,6 +144,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { traceError('Workspace was not selected or found for creating virtual environment.'); return MultiStepAction.Cancel; } + traceInfo(`Selected workspace ${workspace.uri.fsPath} for creating virtual environment.`); return MultiStepAction.Continue; }, undefined, @@ -183,6 +184,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { traceError('Virtual env creation requires an interpreter.'); return MultiStepAction.Cancel; } + traceInfo(`Selected interpreter ${interpreter} for creating virtual environment.`); return MultiStepAction.Continue; }, undefined, @@ -237,8 +239,6 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { progress: CreateEnvironmentProgress, token: CancellationToken, ): Promise => { - let hasError = false; - progress.report({ message: CreateEnv.statusStarting, }); @@ -247,18 +247,19 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { try { if (interpreter && workspace) { envPath = await createVenv(workspace, interpreter, args, progress, token); + if (envPath) { + return { path: envPath, workspaceFolder: workspace }; + } + throw new Error('Failed to create virtual environment. See Output > Python for more info.'); } + throw new Error( + 'Failed to create virtual environment. Either interpreter or workspace is undefined.', + ); } catch (ex) { traceError(ex); - hasError = true; + showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); throw ex; - } finally { - if (hasError) { - showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); - } } - - return { path: envPath, workspaceFolder: workspace, action: undefined, error: undefined }; }, ); } diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index cb4df95c8c1f..3fcadc42ba09 100644 --- a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -134,8 +134,6 @@ suite('Conda Creation provider tests', () => { assert.deepStrictEqual(await promise, { path: 'new_environment', workspaceFolder: workspace1, - action: undefined, - error: undefined, }); assert.isTrue(showErrorMessageWithLogsStub.notCalled); }); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index 1c22264f2ada..5bd325c51a0f 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -158,8 +158,6 @@ suite('venv Creation provider tests', () => { assert.deepStrictEqual(actual, { path: 'new_environment', workspaceFolder: workspace1, - action: undefined, - error: undefined, }); interpreterQuickPick.verifyAll(); progressMock.verifyAll(); From 96ba7353c15f530e1848735d2be2b09982c70d9f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 16 Aug 2023 09:35:25 -0700 Subject: [PATCH 227/336] fix data to string from buffer for output channel (#21821) fix https://github.com/microsoft/vscode-python/issues/21820 --- src/client/testing/testController/common/server.ts | 4 ++-- .../testing/testController/pytest/pytestDiscoveryAdapter.ts | 4 ++-- .../testing/testController/pytest/pytestExecutionAdapter.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 661290bf4988..8797a861fb4a 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -213,10 +213,10 @@ export class PythonTestServer implements ITestServer, Disposable { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { - spawnOptions?.outputChannel?.append(data); + spawnOptions?.outputChannel?.append(data.toString()); }); result?.proc?.stderr?.on('data', (data) => { - spawnOptions?.outputChannel?.append(data); + spawnOptions?.outputChannel?.append(data.toString()); }); result?.proc?.on('exit', () => { traceLog('Exec server closed.', uuid); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 44ab3746dde4..450e2ef1edf2 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -89,10 +89,10 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { - spawnOptions.outputChannel?.append(data); + spawnOptions.outputChannel?.append(data.toString()); }); result?.proc?.stderr?.on('data', (data) => { - spawnOptions.outputChannel?.append(data); + spawnOptions.outputChannel?.append(data.toString()); }); result?.proc?.on('exit', () => { deferredExec.resolve({ stdout: '', stderr: '' }); diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 4a9a57b16fed..96d53db22c1c 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -169,10 +169,10 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { - this.outputChannel?.append(data); + this.outputChannel?.append(data.toString()); }); result?.proc?.stderr?.on('data', (data) => { - this.outputChannel?.append(data); + this.outputChannel?.append(data.toString()); }); result?.proc?.on('exit', () => { From c97945571e24c14bae9d885b359deec1385dff0e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 17 Aug 2023 15:47:03 -0700 Subject: [PATCH 228/336] Set workspaceFolder in debug config before substituting command variables (#21835) For https://github.com/microsoft/vscode-python/issues/18482 --- .../debugger/extension/configuration/resolvers/launch.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index f48b2c19aaff..e4828844a7ad 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -55,6 +55,10 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver Date: Fri, 18 Aug 2023 12:03:59 -0700 Subject: [PATCH 229/336] Wrap env collection workspace proposed APIs in `try...catch` block (#21846) Closes https://github.com/microsoft/vscode-python/issues/21831 --- package.json | 2 +- .../terminalEnvVarCollectionService.ts | 37 ++++++++++--------- ...rminalEnvVarCollectionService.unit.test.ts | 1 + 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index fe247f8b25c1..3f98b3d08b6c 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.82.0-20230809" + "vscode": "^1.81.0-20230809" }, "enableTelemetry": false, "keywords": [ diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index a45306132bf4..5dc716f82afa 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -20,7 +20,7 @@ import { } from '../../common/types'; import { Deferred, createDeferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; -import { traceDecoratorVerbose, traceVerbose } from '../../logging'; +import { traceDecoratorVerbose, traceVerbose, traceWarn } from '../../logging'; import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; @@ -62,8 +62,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async activate(resource: Resource): Promise { if (!inTerminalEnvVarExperiment(this.experimentService)) { - const workspaceFolder = this.getWorkspaceFolder(resource); - this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + this.context.environmentVariableCollection.clear(); await this.handleMicroVenv(resource); if (!this.registeredOnce) { this.interpreterService.onDidChangeInterpreter( @@ -227,22 +226,26 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } private async handleMicroVenv(resource: Resource) { - const workspaceFolder = this.getWorkspaceFolder(resource); - const interpreter = await this.interpreterService.getActiveInterpreter(resource); - if (interpreter?.envType === EnvironmentType.Venv) { - const activatePath = path.join(path.dirname(interpreter.path), 'activate'); - if (!(await pathExists(activatePath))) { - const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); - const pathVarName = getSearchPathEnvVarNames()[0]; - envVarCollection.replace( - 'PATH', - `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, - { applyAtShellIntegration: true, applyAtProcessCreation: true }, - ); - return; + try { + const workspaceFolder = this.getWorkspaceFolder(resource); + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.envType === EnvironmentType.Venv) { + const activatePath = path.join(path.dirname(interpreter.path), 'activate'); + if (!(await pathExists(activatePath))) { + const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); + const pathVarName = getSearchPathEnvVarNames()[0]; + envVarCollection.replace( + 'PATH', + `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, + { applyAtShellIntegration: true, applyAtProcessCreation: true }, + ); + return; + } + this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); } + } catch (ex) { + traceWarn(`Microvenv failed as it is using proposed API which is constantly changing`, ex); } - this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); } private getWorkspaceFolder(resource: Resource): WorkspaceFolder | undefined { diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 86823f16a8c8..b63750e5bf73 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -124,6 +124,7 @@ suite('Terminal Environment Variable Collection Service', () => { test('When not in experiment, do not apply activated variables to the collection and clear it instead', async () => { reset(experimentService); + when(context.environmentVariableCollection).thenReturn(instance(collection)); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); const applyCollectionStub = sinon.stub(terminalEnvVarCollectionService, '_applyCollection'); applyCollectionStub.resolves(); From 021b3620d132ff4bb3646dc136adf56afec909dc Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 18 Aug 2023 13:50:09 -0700 Subject: [PATCH 230/336] Update VS Code engine (#21847) For https://github.com/microsoft/vscode-python/issues/21831 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3f98b3d08b6c..fe247f8b25c1 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.81.0-20230809" + "vscode": "^1.82.0-20230809" }, "enableTelemetry": false, "keywords": [ From 0749b203a3bf49daf7c7509586c41d5e17ff015b Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 21 Aug 2023 10:40:23 -0700 Subject: [PATCH 231/336] Show `Python: Clear Workspace interpreter` command regardless of whether a Python file is opened (#21858) Closes https://github.com/microsoft/vscode-python/issues/21850 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fe247f8b25c1..9e5c3e596a43 100644 --- a/package.json +++ b/package.json @@ -1787,7 +1787,7 @@ "category": "Python", "command": "python.clearWorkspaceInterpreter", "title": "%python.command.python.clearWorkspaceInterpreter.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" + "when": "!virtualWorkspace && shellExecutionSupported" }, { "category": "Python", From cfbf1f3dabf05a375a50cc46b4d15a7368a2f3e7 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 21 Aug 2023 10:52:32 -0700 Subject: [PATCH 232/336] remove usage of pytest CollectReport in rewrite (#21859) as per https://docs.pytest.org/en/7.1.x/reference/reference.html#collectreport, `CollectReport` is experimental and therefore it should not be in our extension. Fixes https://github.com/microsoft/vscode-python/issues/21784 --- pythonFiles/vscode_pytest/__init__.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 49d429662e3a..adf72c134119 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -46,6 +46,15 @@ def __init__(self, message): ERRORS = [] +IS_DISCOVERY = False +map_id_to_path = dict() +collected_tests_so_far = list() + + +def pytest_load_initial_conftests(early_config, parser, args): + if "--collect-only" in args: + global IS_DISCOVERY + IS_DISCOVERY = True def pytest_internalerror(excrepr, excinfo): @@ -70,7 +79,7 @@ def pytest_exception_interact(node, call, report): # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. # If it is during discovery, then add the error to error logs. - if type(report) == pytest.CollectReport: + if IS_DISCOVERY: if call.excinfo and call.excinfo.typename != "AssertionError": if report.outcome == "skipped" and "SkipTest" in str(call): return @@ -168,19 +177,6 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): tests: Dict[str, TestOutcome] -IS_DISCOVERY = False -map_id_to_path = dict() - - -def pytest_load_initial_conftests(early_config, parser, args): - if "--collect-only" in args: - global IS_DISCOVERY - IS_DISCOVERY = True - - -collected_tests_so_far = list() - - def pytest_report_teststatus(report, config): """ A pytest hook that is called when a test is called. It is called 3 times per test, From 15bb9748a533c148df3e2b5a202fb3e9e57731b6 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 21 Aug 2023 22:39:59 -0700 Subject: [PATCH 233/336] Do not filter using scheme when filtering environments (#21862) For https://github.com/microsoft/vscode-python/issues/21825 On codespaces, it was leading to workspace environments not being displayed, which could mess up auto-selection. --- src/client/common/utils/misc.ts | 7 +++---- src/client/pythonEnvironments/base/info/env.ts | 8 +++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/client/common/utils/misc.ts b/src/client/common/utils/misc.ts index c95a3cc75575..455392d28eb1 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -60,7 +60,7 @@ function isUri(resource?: Uri | any): resource is Uri { /** * Create a filter func that determine if the given URI and candidate match. * - * The scheme must match, as well as path. + * Only compares path. * * @param checkParent - if `true`, match if the candidate is rooted under `uri` * or if the candidate matches `uri` exactly. @@ -80,9 +80,8 @@ export function getURIFilter( } const uriRoot = `${uriPath}/`; function filter(candidate: Uri): boolean { - if (candidate.scheme !== uri.scheme) { - return false; - } + // Do not compare schemes as it is sometimes not available, in + // which case file is assumed as scheme. let candidatePath = candidate.path; while (candidatePath.endsWith('/')) { candidatePath = candidatePath.slice(0, -1); diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index 2527f18202cd..12b3e519b944 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -87,7 +87,13 @@ export function areEnvsDeepEqual(env1: PythonEnvInfo, env2: PythonEnvInfo): bool env2Clone.source = env2Clone.source.sort(); const searchLocation1 = env1.searchLocation?.fsPath ?? ''; const searchLocation2 = env2.searchLocation?.fsPath ?? ''; - return isEqual(env1Clone, env2Clone) && arePathsSame(searchLocation1, searchLocation2); + const searchLocation1Scheme = env1.searchLocation?.scheme ?? ''; + const searchLocation2Scheme = env2.searchLocation?.scheme ?? ''; + return ( + isEqual(env1Clone, env2Clone) && + arePathsSame(searchLocation1, searchLocation2) && + searchLocation1Scheme === searchLocation2Scheme + ); } /** From 30e26c2dc47981f03dbcfdfb66440ebe9b211de8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 23 Aug 2023 11:25:17 -0700 Subject: [PATCH 234/336] Update proposed API for env collection (#21819) For https://github.com/microsoft/vscode-python/issues/20822 Blocked on https://github.com/microsoft/vscode/issues/171173#issuecomment-1679208632 --- package-lock.json | 2 +- package.json | 2 +- .../terminalEnvVarCollectionService.ts | 93 ++++++++++++------- ...rminalEnvVarCollectionService.unit.test.ts | 50 ++++++++-- ...scode.proposed.envCollectionWorkspace.d.ts | 50 +++++----- 5 files changed, 127 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84357451dc3c..e001c51ee220 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.82.0-20230809" + "vscode": "^1.82.0-20230823" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index 9e5c3e596a43..bc7378aa521b 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.82.0-20230809" + "vscode": "^1.82.0-20230823" }, "enableTelemetry": false, "keywords": [ diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 5dc716f82afa..33d51e7e2ed0 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -3,7 +3,14 @@ import * as path from 'path'; import { inject, injectable } from 'inversify'; -import { ProgressOptions, ProgressLocation, MarkdownString, WorkspaceFolder } from 'vscode'; +import { + ProgressOptions, + ProgressLocation, + MarkdownString, + WorkspaceFolder, + GlobalEnvironmentVariableCollection, + EnvironmentVariableScope, +} from 'vscode'; import { pathExists } from 'fs-extra'; import { IExtensionActivationService } from '../../activation/types'; import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; @@ -20,7 +27,7 @@ import { } from '../../common/types'; import { Deferred, createDeferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; -import { traceDecoratorVerbose, traceVerbose, traceWarn } from '../../logging'; +import { traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging'; import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; @@ -61,52 +68,56 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ ) {} public async activate(resource: Resource): Promise { - if (!inTerminalEnvVarExperiment(this.experimentService)) { - this.context.environmentVariableCollection.clear(); - await this.handleMicroVenv(resource); + try { + if (!inTerminalEnvVarExperiment(this.experimentService)) { + this.context.environmentVariableCollection.clear(); + await this.handleMicroVenv(resource); + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + await this.handleMicroVenv(r); + }, + this, + this.disposables, + ); + this.registeredOnce = true; + } + return; + } if (!this.registeredOnce) { this.interpreterService.onDidChangeInterpreter( async (r) => { - await this.handleMicroVenv(r); + this.showProgress(); + await this._applyCollection(r).ignoreErrors(); + this.hideProgress(); + }, + this, + this.disposables, + ); + this.applicationEnvironment.onDidChangeShell( + async (shell: string) => { + this.showProgress(); + this.processEnvVars = undefined; + // Pass in the shell where known instead of relying on the application environment, because of bug + // on VSCode: https://github.com/microsoft/vscode/issues/160694 + await this._applyCollection(undefined, shell).ignoreErrors(); + this.hideProgress(); }, this, this.disposables, ); this.registeredOnce = true; } - return; - } - if (!this.registeredOnce) { - this.interpreterService.onDidChangeInterpreter( - async (r) => { - this.showProgress(); - await this._applyCollection(r).ignoreErrors(); - this.hideProgress(); - }, - this, - this.disposables, - ); - this.applicationEnvironment.onDidChangeShell( - async (shell: string) => { - this.showProgress(); - this.processEnvVars = undefined; - // Pass in the shell where known instead of relying on the application environment, because of bug - // on VSCode: https://github.com/microsoft/vscode/issues/160694 - await this._applyCollection(undefined, shell).ignoreErrors(); - this.hideProgress(); - }, - this, - this.disposables, - ); - this.registeredOnce = true; + this._applyCollection(resource).ignoreErrors(); + } catch (ex) { + traceError(`Activating terminal env collection failed`, ex); } - this._applyCollection(resource).ignoreErrors(); } public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); - const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); // Clear any previously set env vars from collection envVarCollection.clear(); if (!settings.terminal.activateEnvironment) { @@ -221,6 +232,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // PS1 should be set but no PS1 was set. return; } + const config = this.workspaceService + .getConfiguration('terminal') + .get('integrated.shellIntegration.enabled'); + if (!config) { + traceVerbose('PS1 is not set when shell integration is disabled.'); + return; + } } this.terminalPromptIsCorrect(resource); } @@ -232,7 +250,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (interpreter?.envType === EnvironmentType.Venv) { const activatePath = path.join(path.dirname(interpreter.path), 'activate'); if (!(await pathExists(activatePath))) { - const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); const pathVarName = getSearchPathEnvVarNames()[0]; envVarCollection.replace( 'PATH', @@ -241,13 +259,18 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ ); return; } - this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + this.getEnvironmentVariableCollection({ workspaceFolder }).clear(); } } catch (ex) { traceWarn(`Microvenv failed as it is using proposed API which is constantly changing`, ex); } } + private getEnvironmentVariableCollection(scope: EnvironmentVariableScope = {}) { + const envVarCollection = this.context.environmentVariableCollection as GlobalEnvironmentVariableCollection; + return envVarCollection.getScoped(scope); + } + private getWorkspaceFolder(resource: Resource): WorkspaceFolder | undefined { let workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); if ( diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index b63750e5bf73..7393f4ad07ad 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -9,9 +9,10 @@ import { mock, instance, when, anything, verify, reset } from 'ts-mockito'; import { EnvironmentVariableCollection, EnvironmentVariableMutatorOptions, - EnvironmentVariableScope, + GlobalEnvironmentVariableCollection, ProgressLocation, Uri, + WorkspaceConfiguration, WorkspaceFolder, } from 'vscode'; import { @@ -44,12 +45,12 @@ suite('Terminal Environment Variable Collection Service', () => { let context: IExtensionContext; let shell: IApplicationShell; let experimentService: IExperimentService; - let collection: EnvironmentVariableCollection & { - getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; - }; + let collection: EnvironmentVariableCollection; + let globalCollection: GlobalEnvironmentVariableCollection; let applicationEnvironment: IApplicationEnvironment; let environmentActivationService: IEnvironmentActivationService; let workspaceService: IWorkspaceService; + let workspaceConfig: WorkspaceConfiguration; let terminalEnvVarCollectionService: TerminalEnvVarCollectionService; const progressOptions = { location: ProgressLocation.Window, @@ -62,19 +63,20 @@ suite('Terminal Environment Variable Collection Service', () => { setup(() => { workspaceService = mock(); + workspaceConfig = mock(); when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); when(workspaceService.workspaceFolders).thenReturn(undefined); + when(workspaceService.getConfiguration('terminal')).thenReturn(instance(workspaceConfig)); + when(workspaceConfig.get('integrated.shellIntegration.enabled')).thenReturn(true); platform = mock(); when(platform.osType).thenReturn(getOSType()); interpreterService = mock(); context = mock(); shell = mock(); - collection = mock< - EnvironmentVariableCollection & { - getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; - } - >(); - when(context.getEnvironmentVariableCollection(anything())).thenReturn(instance(collection)); + globalCollection = mock(); + collection = mock(); + when(context.environmentVariableCollection).thenReturn(instance(globalCollection)); + when(globalCollection.getScoped(anything())).thenReturn(instance(collection)); experimentService = mock(); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); applicationEnvironment = mock(); @@ -291,6 +293,34 @@ suite('Terminal Environment Variable Collection Service', () => { expect(result).to.equal(true); }); + test('Correct track that prompt was set for PS1 if shell integration is disabled', async () => { + reset(workspaceConfig); + when(workspaceConfig.get('integrated.shellIntegration.enabled')).thenReturn(false); + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'bash'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + test('Correct track that prompt was not set for non-Windows zsh where PS1 is set', async () => { when(platform.osType).thenReturn(OSType.Linux); const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; diff --git a/types/vscode.proposed.envCollectionWorkspace.d.ts b/types/vscode.proposed.envCollectionWorkspace.d.ts index 406e5b98cd47..494929ba15eb 100644 --- a/types/vscode.proposed.envCollectionWorkspace.d.ts +++ b/types/vscode.proposed.envCollectionWorkspace.d.ts @@ -4,30 +4,34 @@ *--------------------------------------------------------------------------------------------*/ declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/171173 - // https://github.com/microsoft/vscode/issues/182069 + // export interface ExtensionContext { + // /** + // * Gets the extension's global environment variable collection for this workspace, enabling changes to be + // * applied to terminal environment variables. + // */ + // readonly environmentVariableCollection: GlobalEnvironmentVariableCollection; + // } - export interface ExtensionContext { - /** - * Gets the extension's environment variable collection for this workspace, enabling changes - * to be applied to terminal environment variables. - * - * @deprecated Use {@link getEnvironmentVariableCollection} instead. - */ - readonly environmentVariableCollection: EnvironmentVariableCollection; - /** - * Gets the extension's environment variable collection for this workspace, enabling changes - * to be applied to terminal environment variables. - * - * @param scope The scope to which the environment variable collection applies to. - */ - getEnvironmentVariableCollection(scope?: EnvironmentVariableScope): EnvironmentVariableCollection; - } + export interface GlobalEnvironmentVariableCollection extends EnvironmentVariableCollection { + /** + * Gets scope-specific environment variable collection for the extension. This enables alterations to + * terminal environment variables solely within the designated scope, and is applied in addition to (and + * after) the global collection. + * + * Each object obtained through this method is isolated and does not impact objects for other scopes, + * including the global collection. + * + * @param scope The scope to which the environment variable collection applies to. + */ + getScoped(scope: EnvironmentVariableScope): EnvironmentVariableCollection; + } - export type EnvironmentVariableScope = { - /** - * Any specific workspace folder to get collection for. If unspecified, collection applicable to all workspace folders is returned. - */ - workspaceFolder?: WorkspaceFolder; - }; + export type EnvironmentVariableScope = { + /** + * Any specific workspace folder to get collection for. If unspecified, collection applicable to all workspace folders is returned. + */ + workspaceFolder?: WorkspaceFolder; + }; } From 3fa5d4bda885ed0a430229ef5f3fe5585c9e6249 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 25 Aug 2023 09:44:12 -0700 Subject: [PATCH 235/336] Support for Create Env command to re-create env for venv (#21829) Closes https://github.com/microsoft/vscode-python/issues/21827 --- src/client/common/utils/localize.ts | 7 + .../creation/common/commonUtils.ts | 19 +++ .../creation/provider/venvCreationProvider.ts | 141 ++++++++++++----- .../creation/provider/venvDeleteUtils.ts | 99 ++++++++++++ .../creation/provider/venvSwitchPython.ts | 28 ++++ .../creation/provider/venvUtils.ts | 78 +++++++++- src/client/telemetry/constants.ts | 2 + src/client/telemetry/index.ts | 24 +++ .../venvCreationProvider.unit.test.ts | 117 +++++++++++++++ .../provider/venvDeleteUtils.unit.test.ts | 142 ++++++++++++++++++ .../creation/provider/venvUtils.unit.test.ts | 68 ++++++++- 11 files changed, 685 insertions(+), 40 deletions(-) create mode 100644 src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts create mode 100644 src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts create mode 100644 src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 3ec1829201f8..4401cab56176 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -464,6 +464,13 @@ export namespace CreateEnv { export const error = l10n.t('Creating virtual environment failed with error.'); export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml'); export const requirementsQuickPickTitle = l10n.t('Select dependencies to install'); + export const recreate = l10n.t('Recreate'); + export const recreateDescription = l10n.t('Delete existing ".venv" environment and create a new one'); + export const useExisting = l10n.t('Use Existing'); + export const useExistingDescription = l10n.t('Use existing ".venv" environment with no changes to it'); + export const existingVenvQuickPickPlaceholder = l10n.t('Use or Recreate existing environment?'); + export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...'); + export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.'); } export namespace Conda { diff --git a/src/client/pythonEnvironments/creation/common/commonUtils.ts b/src/client/pythonEnvironments/creation/common/commonUtils.ts index 0e303e70138e..b4d4a37eae9b 100644 --- a/src/client/pythonEnvironments/creation/common/commonUtils.ts +++ b/src/client/pythonEnvironments/creation/common/commonUtils.ts @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; import { Commands } from '../../../common/constants'; import { Common } from '../../../common/utils/localize'; import { executeCommand } from '../../../common/vscodeApis/commandApis'; import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { isWindows } from '../../../common/platform/platformService'; export async function showErrorMessageWithLogs(message: string): Promise { const result = await showErrorMessage(message, Common.openOutputPanel, Common.selectPythonInterpreter); @@ -13,3 +17,18 @@ export async function showErrorMessageWithLogs(message: string): Promise { await executeCommand(Commands.Set_Interpreter); } } + +export function getVenvPath(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, '.venv'); +} + +export async function hasVenv(workspaceFolder: WorkspaceFolder): Promise { + return fs.pathExists(getVenvPath(workspaceFolder)); +} + +export function getVenvExecutable(workspaceFolder: WorkspaceFolder): string { + if (isWindows()) { + return path.join(getVenvPath(workspaceFolder), 'Scripts', 'python.exe'); + } + return path.join(getVenvPath(workspaceFolder), 'bin', 'python'); +} diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index 6c310fd68331..ba47edcf6f30 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -17,8 +17,14 @@ import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vs import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { VenvProgressAndTelemetry, VENV_CREATED_MARKER, VENV_EXISTING_MARKER } from './venvProgressAndTelemetry'; -import { showErrorMessageWithLogs } from '../common/commonUtils'; -import { IPackageInstallSelection, pickPackagesToInstall } from './venvUtils'; +import { getVenvExecutable, showErrorMessageWithLogs } from '../common/commonUtils'; +import { + ExistingVenvAction, + IPackageInstallSelection, + deleteEnvironment, + pickExistingVenvAction, + pickPackagesToInstall, +} from './venvUtils'; import { InputFlowAction } from '../../../common/utils/multiStepInput'; import { CreateEnvironmentProvider, @@ -150,33 +156,66 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { undefined, ); - let interpreter: string | undefined; - const interpreterStep = new MultiStepNode( + let existingVenvAction: ExistingVenvAction | undefined; + const existingEnvStep = new MultiStepNode( workspaceStep, - async () => { - if (workspace) { + async (context?: MultiStepAction) => { + if (workspace && context === MultiStepAction.Continue) { try { - interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick( - workspace.uri, - (i: PythonEnvironment) => - [ - EnvironmentType.System, - EnvironmentType.MicrosoftStore, - EnvironmentType.Global, - EnvironmentType.Pyenv, - ].includes(i.envType) && i.type === undefined, // only global intepreters - { - skipRecommended: true, - showBackButton: true, - placeholder: CreateEnv.Venv.selectPythonPlaceHolder, - title: null, - }, - ); + existingVenvAction = await pickExistingVenvAction(workspace); + return MultiStepAction.Continue; } catch (ex) { - if (ex === InputFlowAction.back) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + return MultiStepAction.Continue; + }, + undefined, + ); + workspaceStep.next = existingEnvStep; + + let interpreter: string | undefined; + const interpreterStep = new MultiStepNode( + existingEnvStep, + async (context?: MultiStepAction) => { + if (workspace) { + if ( + existingVenvAction === ExistingVenvAction.Recreate || + existingVenvAction === ExistingVenvAction.Create + ) { + try { + interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick( + workspace.uri, + (i: PythonEnvironment) => + [ + EnvironmentType.System, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.Pyenv, + ].includes(i.envType) && i.type === undefined, // only global intepreters + { + skipRecommended: true, + showBackButton: true, + placeholder: CreateEnv.Venv.selectPythonPlaceHolder, + title: null, + }, + ); + } catch (ex) { + if (ex === InputFlowAction.back) { + return MultiStepAction.Back; + } + interpreter = undefined; + } + } else if (existingVenvAction === ExistingVenvAction.UseExisting) { + if (context === MultiStepAction.Back) { return MultiStepAction.Back; } - interpreter = undefined; + interpreter = getVenvExecutable(workspace); } } @@ -189,7 +228,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { }, undefined, ); - workspaceStep.next = interpreterStep; + existingEnvStep.next = interpreterStep; let addGitIgnore = true; let installPackages = true; @@ -200,19 +239,23 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { let installInfo: IPackageInstallSelection[] | undefined; const packagesStep = new MultiStepNode( interpreterStep, - async () => { + async (context?: MultiStepAction) => { if (workspace && installPackages) { - try { - installInfo = await pickPackagesToInstall(workspace); - } catch (ex) { - if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { - return ex; + if (existingVenvAction !== ExistingVenvAction.UseExisting) { + try { + installInfo = await pickPackagesToInstall(workspace); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; } - throw ex; - } - if (!installInfo) { - traceVerbose('Virtual env creation exited during dependencies selection.'); - return MultiStepAction.Cancel; + if (!installInfo) { + traceVerbose('Virtual env creation exited during dependencies selection.'); + return MultiStepAction.Cancel; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; } } @@ -227,6 +270,32 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { throw action; } + if (workspace) { + if (existingVenvAction === ExistingVenvAction.Recreate) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'triggered', + }); + if (await deleteEnvironment(workspace, interpreter)) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'deleted', + }); + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'failed', + }); + throw MultiStepAction.Cancel; + } + } else if (existingVenvAction === ExistingVenvAction.UseExisting) { + sendTelemetryEvent(EventName.ENVIRONMENT_REUSE, undefined, { + environmentType: 'venv', + }); + return { path: getVenvExecutable(workspace), workspaceFolder: workspace }; + } + } + const args = generateCommandArgs(installInfo, addGitIgnore); return withProgress( diff --git a/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts b/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts new file mode 100644 index 000000000000..46a0adf0f228 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; +import { traceError, traceInfo } from '../../../logging'; +import { getVenvPath, showErrorMessageWithLogs } from '../common/commonUtils'; +import { CreateEnv } from '../../../common/utils/localize'; +import { sleep } from '../../../common/utils/async'; +import { switchSelectedPython } from './venvSwitchPython'; + +async function tryDeleteFile(file: string): Promise { + try { + if (!(await fs.pathExists(file))) { + return true; + } + await fs.unlink(file); + return true; + } catch (err) { + traceError(`Failed to delete file [${file}]:`, err); + return false; + } +} + +async function tryDeleteDir(dir: string): Promise { + try { + if (!(await fs.pathExists(dir))) { + return true; + } + await fs.rmdir(dir, { + recursive: true, + maxRetries: 10, + retryDelay: 200, + }); + return true; + } catch (err) { + traceError(`Failed to delete directory [${dir}]:`, err); + return false; + } +} + +export async function deleteEnvironmentNonWindows(workspaceFolder: WorkspaceFolder): Promise { + const venvPath = getVenvPath(workspaceFolder); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted venv dir: ${venvPath}`); + return true; + } + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; +} + +export async function deleteEnvironmentWindows( + workspaceFolder: WorkspaceFolder, + interpreter: string | undefined, +): Promise { + const venvPath = getVenvPath(workspaceFolder); + const venvPythonPath = path.join(venvPath, 'Scripts', 'python.exe'); + + if (await tryDeleteFile(venvPythonPath)) { + traceInfo(`Deleted python executable: ${venvPythonPath}`); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted ".venv" dir: ${venvPath}`); + return true; + } + + traceError(`Failed to delete ".venv" dir: ${venvPath}`); + traceError( + 'This happens if the virtual environment is still in use, or some binary in the venv is still running.', + ); + traceError(`Please delete the ".venv" manually: [${venvPath}]`); + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; + } + traceError(`Failed to delete python executable: ${venvPythonPath}`); + traceError('This happens if the virtual environment is still in use.'); + + if (interpreter) { + traceError('We will attempt to switch python temporarily to delete the ".venv"'); + + await switchSelectedPython(interpreter, workspaceFolder.uri, 'temporarily to delete the ".venv"'); + + traceInfo(`Attempting to delete ".venv" again: ${venvPath}`); + const ms = 500; + for (let i = 0; i < 5; i = i + 1) { + traceInfo(`Waiting for ${ms}ms to let processes exit, before a delete attempt.`); + await sleep(ms); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted ".venv" dir: ${venvPath}`); + return true; + } + traceError(`Failed to delete ".venv" dir [${venvPath}] (attempt ${i + 1}/5).`); + } + } else { + traceError(`Please delete the ".venv" dir manually: [${venvPath}]`); + } + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts b/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts new file mode 100644 index 000000000000..e2567dfd114b --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Disposable, Uri } from 'vscode'; +import { createDeferred } from '../../../common/utils/async'; +import { getExtension } from '../../../common/vscodeApis/extensionsApi'; +import { PVSC_EXTENSION_ID, PythonExtension } from '../../../api/types'; +import { traceInfo } from '../../../logging'; + +export async function switchSelectedPython(interpreter: string, uri: Uri, purpose: string): Promise { + let dispose: Disposable | undefined; + try { + const deferred = createDeferred(); + const api: PythonExtension = getExtension(PVSC_EXTENSION_ID)?.exports as PythonExtension; + dispose = api.environments.onDidChangeActiveEnvironmentPath(async (e) => { + if (path.normalize(e.path) === path.normalize(interpreter)) { + traceInfo(`Switched to interpreter ${purpose}: ${interpreter}`); + deferred.resolve(); + } + }); + api.environments.updateActiveEnvironmentPath(interpreter, uri); + traceInfo(`Switching interpreter ${purpose}: ${interpreter}`); + await deferred.promise; + } finally { + dispose?.dispose(); + } +} diff --git a/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/src/client/pythonEnvironments/creation/provider/venvUtils.ts index 7c6505082fbc..d7a0be170f99 100644 --- a/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -5,11 +5,20 @@ import * as tomljs from '@iarna/toml'; import * as fs from 'fs-extra'; import { flatten, isArray } from 'lodash'; import * as path from 'path'; -import { CancellationToken, QuickPickItem, RelativePattern, WorkspaceFolder } from 'vscode'; -import { CreateEnv } from '../../../common/utils/localize'; -import { MultiStepAction, MultiStepNode, showQuickPickWithBack } from '../../../common/vscodeApis/windowApis'; +import { CancellationToken, ProgressLocation, QuickPickItem, RelativePattern, WorkspaceFolder } from 'vscode'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { + MultiStepAction, + MultiStepNode, + showQuickPickWithBack, + withProgress, +} from '../../../common/vscodeApis/windowApis'; import { findFiles } from '../../../common/vscodeApis/workspaceApis'; import { traceError, traceVerbose } from '../../../logging'; +import { Commands } from '../../../common/constants'; +import { isWindows } from '../../../common/platform/platformService'; +import { getVenvPath, hasVenv } from '../common/commonUtils'; +import { deleteEnvironmentNonWindows, deleteEnvironmentWindows } from './venvDeleteUtils'; const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; async function getPipRequirementsFiles( @@ -226,3 +235,66 @@ export async function pickPackagesToInstall( return packages; } + +export async function deleteEnvironment( + workspaceFolder: WorkspaceFolder, + interpreter: string | undefined, +): Promise { + const venvPath = getVenvPath(workspaceFolder); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.Venv.deletingEnvironmentProgress} ([${Common.showLogs}](command:${Commands.ViewOutput})): ${venvPath}`, + cancellable: false, + }, + async () => { + if (isWindows()) { + return deleteEnvironmentWindows(workspaceFolder, interpreter); + } + return deleteEnvironmentNonWindows(workspaceFolder); + }, + ); +} + +export enum ExistingVenvAction { + Recreate, + UseExisting, + Create, +} + +export async function pickExistingVenvAction( + workspaceFolder: WorkspaceFolder | undefined, +): Promise { + if (workspaceFolder) { + if (await hasVenv(workspaceFolder)) { + const items: QuickPickItem[] = [ + { label: CreateEnv.Venv.recreate, description: CreateEnv.Venv.recreateDescription }, + { + label: CreateEnv.Venv.useExisting, + description: CreateEnv.Venv.useExistingDescription, + }, + ]; + + const selection = (await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Venv.existingVenvQuickPickPlaceholder, + ignoreFocusOut: true, + }, + undefined, + )) as QuickPickItem | undefined; + + if (selection?.label === CreateEnv.Venv.recreate) { + return ExistingVenvAction.Recreate; + } + + if (selection?.label === CreateEnv.Venv.useExisting) { + return ExistingVenvAction.UseExisting; + } + } else { + return ExistingVenvAction.Create; + } + } + + throw MultiStepAction.Cancel; +} diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 159f5690e5c5..a729b3d491e8 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -112,6 +112,8 @@ export enum EventName { ENVIRONMENT_INSTALLED_PACKAGES = 'ENVIRONMENT.INSTALLED_PACKAGES', ENVIRONMENT_INSTALLING_PACKAGES_FAILED = 'ENVIRONMENT.INSTALLING_PACKAGES_FAILED', ENVIRONMENT_BUTTON = 'ENVIRONMENT.BUTTON', + ENVIRONMENT_DELETE = 'ENVIRONMENT.DELETE', + ENVIRONMENT_REUSE = 'ENVIRONMENT.REUSE', TOOLS_EXTENSIONS_ALREADY_INSTALLED = 'TOOLS_EXTENSIONS.ALREADY_INSTALLED', TOOLS_EXTENSIONS_PROMPT_SHOWN = 'TOOLS_EXTENSIONS.PROMPT_SHOWN', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index a068f21d2327..f4947cd73f05 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2120,6 +2120,30 @@ export interface IEventNamePropertyMapping { "environment.button" : {"owner": "karthiknadig" } */ [EventName.ENVIRONMENT_BUTTON]: never | undefined; + /** + * Telemetry event if user selected to delete the existing environment. + */ + /* __GDPR__ + "environment.delete" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "status" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_DELETE]: { + environmentType: 'venv' | 'conda'; + status: 'triggered' | 'deleted' | 'failed'; + }; + /** + * Telemetry event if user selected to re-use the existing environment. + */ + /* __GDPR__ + "environment.reuse" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_REUSE]: { + environmentType: 'venv' | 'conda'; + }; /** * Telemetry event sent when a linter or formatter extension is already installed. */ diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index 5bd325c51a0f..280d05bf5935 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -35,6 +35,8 @@ suite('venv Creation provider tests', () => { let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; let pickPackagesToInstallStub: sinon.SinonStub; + let pickExistingVenvActionStub: sinon.SinonStub; + let deleteEnvironmentStub: sinon.SinonStub; const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), @@ -43,6 +45,8 @@ suite('venv Creation provider tests', () => { }; setup(() => { + pickExistingVenvActionStub = sinon.stub(venvUtils, 'pickExistingVenvAction'); + deleteEnvironmentStub = sinon.stub(venvUtils, 'deleteEnvironment'); pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); interpreterQuickPick = typemoq.Mock.ofType(); @@ -54,6 +58,9 @@ suite('venv Creation provider tests', () => { progressMock = typemoq.Mock.ofType(); venvProvider = new VenvCreationProvider(interpreterQuickPick.object); + + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Create); + deleteEnvironmentStub.resolves(true); }); teardown(() => { @@ -70,6 +77,8 @@ suite('venv Creation provider tests', () => { assert.isTrue(pickWorkspaceFolderStub.calledOnce); interpreterQuickPick.verifyAll(); assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(pickExistingVenvActionStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('No Python selected', async () => { @@ -85,6 +94,7 @@ suite('venv Creation provider tests', () => { assert.isTrue(pickWorkspaceFolderStub.calledOnce); interpreterQuickPick.verifyAll(); assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('User pressed Esc while selecting dependencies', async () => { @@ -99,6 +109,7 @@ suite('venv Creation provider tests', () => { await assert.isRejected(venvProvider.createEnvironment()); assert.isTrue(pickPackagesToInstallStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('Create venv with python selected by user no packages selected', async () => { @@ -162,6 +173,7 @@ suite('venv Creation provider tests', () => { interpreterQuickPick.verifyAll(); progressMock.verifyAll(); assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('Create venv failed', async () => { @@ -216,6 +228,7 @@ suite('venv Creation provider tests', () => { _complete!(); await assert.isRejected(promise); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('Create venv failed (non-zero exit code)', async () => { @@ -274,5 +287,109 @@ suite('venv Creation provider tests', () => { interpreterQuickPick.verifyAll(); progressMock.verifyAll(); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv with pre-existing .venv, user selects re-create', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Recreate); + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.calledOnce); + }); + + test('Create venv with pre-existing .venv, user selects re-create, delete env failed', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Recreate); + pickWorkspaceFolderStub.resolves(workspace1); + deleteEnvironmentStub.resolves(false); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + await assert.isRejected(venvProvider.createEnvironment()); + + interpreterQuickPick.verifyAll(); + assert.isTrue(withProgressStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.calledOnce); + }); + + test('Create venv with pre-existing .venv, user selects use existing', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.UseExisting); + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.never()); + + pickPackagesToInstallStub.resolves([]); + + interpreterQuickPick.verifyAll(); + assert.isTrue(withProgressStub.notCalled); + assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); }); }); diff --git a/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..d075979b70b1 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as fs from 'fs-extra'; +import * as sinon from 'sinon'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { assert } from 'chai'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { + deleteEnvironmentNonWindows, + deleteEnvironmentWindows, +} from '../../../../client/pythonEnvironments/creation/provider/venvDeleteUtils'; +import * as switchPython from '../../../../client/pythonEnvironments/creation/provider/venvSwitchPython'; +import * as asyncApi from '../../../../client/common/utils/async'; + +suite('Test Delete environments (windows)', () => { + let pathExistsStub: sinon.SinonStub; + let rmdirStub: sinon.SinonStub; + let unlinkStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + let switchPythonStub: sinon.SinonStub; + let sleepStub: sinon.SinonStub; + + const workspace1: WorkspaceFolder = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + pathExistsStub.resolves(true); + + rmdirStub = sinon.stub(fs, 'rmdir'); + unlinkStub = sinon.stub(fs, 'unlink'); + + sleepStub = sinon.stub(asyncApi, 'sleep'); + sleepStub.resolves(); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + switchPythonStub = sinon.stub(switchPython, 'switchSelectedPython'); + switchPythonStub.resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete venv folder succeeded', async () => { + rmdirStub.resolves(); + unlinkStub.resolves(); + assert.ok(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(unlinkStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete python.exe succeeded but venv dir failed', async () => { + rmdirStub.rejects(); + unlinkStub.resolves(); + assert.notOk(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(unlinkStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete python.exe failed first attempt', async () => { + unlinkStub.rejects(); + rmdirStub.resolves(); + assert.ok(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(switchPythonStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete python.exe failed all attempts', async () => { + unlinkStub.rejects(); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentWindows(workspace1, 'python.exe')); + assert.ok(switchPythonStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete python.exe failed no interpreter', async () => { + unlinkStub.rejects(); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentWindows(workspace1, undefined)); + assert.ok(switchPythonStub.notCalled); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); +}); + +suite('Test Delete environments (linux/mac)', () => { + let pathExistsStub: sinon.SinonStub; + let rmdirStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + + const workspace1: WorkspaceFolder = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + rmdirStub = sinon.stub(fs, 'rmdir'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete venv folder succeeded', async () => { + pathExistsStub.resolves(true); + rmdirStub.resolves(); + + assert.ok(await deleteEnvironmentNonWindows(workspace1)); + + assert.ok(pathExistsStub.calledOnce); + assert.ok(rmdirStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete venv folder failed', async () => { + pathExistsStub.resolves(true); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentNonWindows(workspace1)); + + assert.ok(pathExistsStub.calledOnce); + assert.ok(rmdirStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts index 360bb43fad4b..ae4f43a0296c 100644 --- a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts @@ -8,7 +8,11 @@ import { Uri } from 'vscode'; import * as path from 'path'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; -import { pickPackagesToInstall } from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; +import { + ExistingVenvAction, + pickExistingVenvAction, + pickPackagesToInstall, +} from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import { CreateEnv } from '../../../../client/common/utils/localize'; @@ -346,3 +350,65 @@ suite('Venv Utils test', () => { assert.isTrue(readFileStub.notCalled); }); }); + +suite('Test pick existing venv action', () => { + let withProgressStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + teardown(() => { + sinon.restore(); + }); + + test('User selects existing venv', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Venv.useExisting, + description: CreateEnv.Venv.useExistingDescription, + }); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.UseExisting); + }); + + test('User presses escape', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves(undefined); + await assert.isRejected(pickExistingVenvAction(workspace1)); + }); + + test('User selects delete venv', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }); + withProgressStub.resolves(true); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.Recreate); + }); + + test('User clicks on back', async () => { + pathExistsStub.resolves(true); + // We use reject with "Back" to simulate the user clicking on back. + showQuickPickWithBackStub.rejects(windowApis.MultiStepAction.Back); + withProgressStub.resolves(false); + await assert.isRejected(pickExistingVenvAction(workspace1)); + }); + + test('No venv found', async () => { + pathExistsStub.resolves(false); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.Create); + }); +}); From 98428cd8449ec73c095eda89065bdb384e4481e4 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 28 Aug 2023 16:31:46 -0700 Subject: [PATCH 236/336] Apply custom env variables to terminal when in `pythonTerminalEnvVarActivation` experiment (#21879) For https://github.com/microsoft/vscode-python/issues/944 #20822 We only apply those env vars to terminal which are not in process env variables, hence remove custom env vars from process variables. --- src/client/common/process/processFactory.ts | 6 ++++-- src/client/common/process/types.ts | 2 +- src/client/interpreter/activation/service.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/common/process/processFactory.ts b/src/client/common/process/processFactory.ts index 8681d5073d8e..40204a640dae 100644 --- a/src/client/common/process/processFactory.ts +++ b/src/client/common/process/processFactory.ts @@ -17,8 +17,10 @@ export class ProcessServiceFactory implements IProcessServiceFactory { @inject(IProcessLogger) private readonly processLogger: IProcessLogger, @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, ) {} - public async create(resource?: Uri): Promise { - const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); + public async create(resource?: Uri, options?: { doNotUseCustomEnvs: boolean }): Promise { + const customEnvVars = options?.doNotUseCustomEnvs + ? undefined + : await this.envVarsService.getEnvironmentVariables(resource); const proc: IProcessService = new ProcessService(customEnvVars); this.disposableRegistry.push(proc); return proc.on('exec', this.processLogger.logProcess.bind(this.processLogger)); diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 62e787b694b5..d4b742718e36 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -55,7 +55,7 @@ export interface IProcessService extends IDisposable { export const IProcessServiceFactory = Symbol('IProcessServiceFactory'); export interface IProcessServiceFactory { - create(resource?: Uri): Promise; + create(resource?: Uri, options?: { doNotUseCustomEnvs: boolean }): Promise; } export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory'); diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index 4364cc825f78..02d621c0ccda 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -192,7 +192,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi args[i] = arg.toCommandArgumentForPythonExt(); }); const command = `${interpreterPath} ${args.join(' ')}`; - const processService = await this.processServiceFactory.create(resource); + const processService = await this.processServiceFactory.create(resource, { doNotUseCustomEnvs: true }); const result = await processService.shellExec(command, { shell, timeout: ENVIRONMENT_TIMEOUT, From 12040116426b24ffb21bcbbb953f7f8487f3019a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 29 Aug 2023 11:08:49 -0700 Subject: [PATCH 237/336] Activate environment when not using integrated terminal for debugging (#21880) For https://github.com/microsoft/vscode-python/issues/4300 --- .../debugger/extension/configuration/resolvers/launch.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index e4828844a7ad..c4ae6a204d71 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -19,7 +19,7 @@ import { getProgram, IDebugEnvironmentVariablesService } from './helper'; @injectable() export class LaunchConfigurationResolver extends BaseConfigurationResolver { - private isPythonSet = false; + private isCustomPythonSet = false; constructor( @inject(IDiagnosticsService) @@ -38,7 +38,7 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { - this.isPythonSet = debugConfiguration.python !== undefined; + this.isCustomPythonSet = debugConfiguration.python !== undefined; if ( debugConfiguration.name === undefined && debugConfiguration.type === undefined && @@ -110,7 +110,9 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver Date: Tue, 29 Aug 2023 16:49:49 -0700 Subject: [PATCH 238/336] Also show interpreter in status bar when a Python related output channel is opened (#21894) Closes https://github.com/microsoft/vscode-python/issues/21890 --- src/client/interpreter/interpreterService.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 3cfb651977bb..b595fc2365a8 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -31,7 +31,7 @@ import { PythonEnvironmentsChangedEvent, } from './contracts'; import { traceError, traceLog } from '../logging'; -import { Commands, PYTHON_LANGUAGE } from '../common/constants'; +import { Commands, PVSC_EXTENSION_ID, PYTHON_LANGUAGE } from '../common/constants'; import { reportActiveInterpreterChanged } from '../environmentApi'; import { IPythonExecutionFactory } from '../common/process/types'; import { Interpreters } from '../common/utils/localize'; @@ -138,7 +138,12 @@ export class InterpreterService implements Disposable, IInterpreterService { return false; } const document = this.docManager.activeTextEditor?.document; - if (document?.fileName.endsWith('settings.json')) { + // Output channel for MS Python related extensions. These contain "ms-python" in their ID. + const pythonOutputChannelPattern = PVSC_EXTENSION_ID.split('.')[0]; + if ( + document?.fileName.endsWith('settings.json') || + document?.fileName.includes(pythonOutputChannelPattern) + ) { return false; } return document?.languageId !== PYTHON_LANGUAGE; From f255e0261ddd39172d6f8bf0bd94d08adbada1e7 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 29 Aug 2023 16:51:29 -0700 Subject: [PATCH 239/336] Call out that env name may not show in terminal activation notification (#21897) Closes https://github.com/microsoft/vscode-python/issues/21887 --- src/client/common/utils/localize.ts | 2 +- .../terminalEnvVarCollectionPrompt.ts | 11 ++++-- ...erminalEnvVarCollectionPrompt.unit.test.ts | 37 ++++++++++++------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 4401cab56176..eb8e94a85a52 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -199,7 +199,7 @@ export namespace Interpreters { export const activatingTerminals = l10n.t('Reactivating terminals...'); export const activateTerminalDescription = l10n.t('Activated environment for'); export const terminalEnvVarCollectionPrompt = l10n.t( - 'The Python extension automatically activates all terminals using the selected environment. You can hover over the terminal tab to see more information about the activation. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', + 'The Python extension automatically activates all terminals using the selected environment, even when the name of the environment{0} is not present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', ); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts index 7833f34ce2fb..8b7850514874 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts @@ -9,11 +9,13 @@ import { IDisposableRegistry, IExperimentService, IPersistentStateFactory, + Resource, } from '../../common/types'; import { Common, Interpreters } from '../../common/utils/localize'; import { IExtensionSingleActivationService } from '../../activation/types'; import { ITerminalEnvVarCollectionService } from './types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IInterpreterService } from '../contracts'; export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; @@ -30,6 +32,7 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio @inject(ITerminalEnvVarCollectionService) private readonly terminalEnvVarCollectionService: ITerminalEnvVarCollectionService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IExperimentService) private readonly experimentService: IExperimentService, ) {} @@ -52,12 +55,12 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio // No need to show notification if terminal prompt already indicates when env is activated. return; } - await this.notifyUsers(); + await this.notifyUsers(resource); }), ); } - private async notifyUsers(): Promise { + private async notifyUsers(resource: Resource): Promise { const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( terminalEnvCollectionPromptKey, true, @@ -66,8 +69,10 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio return; } const prompts = [Common.doNotShowAgain]; + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const terminalPromptName = interpreter?.envName ? ` (${interpreter.envName})` : ''; const selection = await this.appShell.showInformationMessage( - Interpreters.terminalEnvVarCollectionPrompt, + Interpreters.terminalEnvVarCollectionPrompt.format(terminalPromptName), ...prompts, ); if (!selection) { diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts index 8edaa00dcf73..e1bc2d171226 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -18,6 +18,8 @@ import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/ac import { Common, Interpreters } from '../../../client/common/utils/localize'; import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; import { sleep } from '../../core'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Terminal Environment Variable Collection Prompt', () => { let shell: IApplicationShell; @@ -30,12 +32,18 @@ suite('Terminal Environment Variable Collection Prompt', () => { let terminalEventEmitter: EventEmitter; let notificationEnabled: IPersistentState; let configurationService: IConfigurationService; + let interpreterService: IInterpreterService; const prompts = [Common.doNotShowAgain]; - const message = Interpreters.terminalEnvVarCollectionPrompt; + const envName = 'env'; + const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format(` (${envName})`); setup(async () => { shell = mock(); terminalManager = mock(); + interpreterService = mock(); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName, + } as unknown) as PythonEnvironment); experimentService = mock(); activeResourceService = mock(); persistentStateFactory = mock(); @@ -61,6 +69,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { instance(activeResourceService), instance(terminalEnvVarCollectionService), instance(configurationService), + instance(interpreterService), instance(experimentService), ); }); @@ -74,13 +83,13 @@ suite('Terminal Environment Variable Collection Prompt', () => { } as unknown) as Terminal; when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(true); - when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); await sleep(1); - verify(shell.showInformationMessage(message, ...prompts)).once(); + verify(shell.showInformationMessage(expectedMessage, ...prompts)).once(); }); test('Do not show notification if automatic terminal activation is turned off', async () => { @@ -98,13 +107,13 @@ suite('Terminal Environment Variable Collection Prompt', () => { } as unknown) as Terminal; when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(true); - when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); await sleep(1); - verify(shell.showInformationMessage(message, ...prompts)).never(); + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); }); test('When not in experiment, do not show notification for the same', async () => { @@ -116,7 +125,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { } as unknown) as Terminal; when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(true); - when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); reset(experimentService); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); @@ -124,7 +133,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { terminalEventEmitter.fire(terminal); await sleep(1); - verify(shell.showInformationMessage(message, ...prompts)).never(); + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); }); test('Do not show notification if notification is disabled', async () => { @@ -136,13 +145,13 @@ suite('Terminal Environment Variable Collection Prompt', () => { } as unknown) as Terminal; when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(false); - when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); await sleep(1); - verify(shell.showInformationMessage(message, ...prompts)).never(); + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); }); test('Do not show notification when a new terminal is opened for which there is prompt set', async () => { @@ -154,13 +163,13 @@ suite('Terminal Environment Variable Collection Prompt', () => { } as unknown) as Terminal; when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(true); when(notificationEnabled.value).thenReturn(true); - when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); await sleep(1); - verify(shell.showInformationMessage(message, ...prompts)).never(); + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); }); test("Disable notification if `Don't show again` is clicked", async () => { @@ -173,7 +182,9 @@ suite('Terminal Environment Variable Collection Prompt', () => { when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(true); when(notificationEnabled.updateValue(false)).thenResolve(); - when(shell.showInformationMessage(message, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenReturn( + Promise.resolve(Common.doNotShowAgain), + ); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); @@ -192,7 +203,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(true); when(notificationEnabled.updateValue(false)).thenResolve(); - when(shell.showInformationMessage(message, ...prompts)).thenReturn(Promise.resolve(undefined)); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(undefined)); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); From 941fcfa938113b00d14cd944399185fcf9f35085 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 29 Aug 2023 18:55:27 -0700 Subject: [PATCH 240/336] Fixes from TPIs (#21896) Closes https://github.com/microsoft/vscode-python/issues/21884 Closes https://github.com/microsoft/vscode-python/issues/21889 --- src/client/common/utils/localize.ts | 4 +++- .../creation/provider/condaCreationProvider.ts | 6 ++++-- .../creation/provider/venvCreationProvider.ts | 7 +++++-- .../creation/provider/condaCreationProvider.unit.test.ts | 6 ++++-- .../creation/provider/venvCreationProvider.unit.test.ts | 6 ++++-- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index eb8e94a85a52..4cda15e15ec0 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -468,7 +468,9 @@ export namespace CreateEnv { export const recreateDescription = l10n.t('Delete existing ".venv" environment and create a new one'); export const useExisting = l10n.t('Use Existing'); export const useExistingDescription = l10n.t('Use existing ".venv" environment with no changes to it'); - export const existingVenvQuickPickPlaceholder = l10n.t('Use or Recreate existing environment?'); + export const existingVenvQuickPickPlaceholder = l10n.t( + 'Choose an option to handle the existing ".venv" environment', + ); export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...'); export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.'); } diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 74a42808e609..7ca44c1b7eff 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -128,7 +128,9 @@ async function createCondaEnv( dispose(); if (proc?.exitCode !== 0) { traceError('Error while running venv creation script: ', progressAndTelemetry.getLastError()); - deferred.reject(progressAndTelemetry.getLastError()); + deferred.reject( + progressAndTelemetry.getLastError() || `Conda env creation failed with exitCode: ${proc?.exitCode}`, + ); } else { deferred.resolve(condaEnvPath); } @@ -249,7 +251,7 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { assert.isDefined(_error); _error!('bad arguments'); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); }); @@ -241,7 +242,8 @@ suite('Conda Creation provider tests', () => { _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); }); }); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index 280d05bf5935..de65887b7edc 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -226,7 +226,8 @@ suite('venv Creation provider tests', () => { assert.isDefined(_error); _error!('bad arguments'); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); assert.isTrue(deleteEnvironmentStub.notCalled); }); @@ -283,7 +284,8 @@ suite('venv Creation provider tests', () => { _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); interpreterQuickPick.verifyAll(); progressMock.verifyAll(); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); From 31aa24625b726b5a0af0d7c21666f61b59010c17 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 29 Aug 2023 23:11:38 -0700 Subject: [PATCH 241/336] Also show env name for prefixed conda envs in terminal prompt (#21899) --- .../terminalEnvVarCollectionPrompt.ts | 17 ++++++++++++++++- .../terminalEnvVarCollectionPrompt.unit.test.ts | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts index 8b7850514874..b5d42f0e7dbe 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts @@ -3,6 +3,7 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; +import * as path from 'path'; import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types'; import { IConfigurationService, @@ -16,6 +17,7 @@ import { IExtensionSingleActivationService } from '../../activation/types'; import { ITerminalEnvVarCollectionService } from './types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; import { IInterpreterService } from '../contracts'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; @@ -70,7 +72,7 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio } const prompts = [Common.doNotShowAgain]; const interpreter = await this.interpreterService.getActiveInterpreter(resource); - const terminalPromptName = interpreter?.envName ? ` (${interpreter.envName})` : ''; + const terminalPromptName = getPromptName(interpreter); const selection = await this.appShell.showInformationMessage( Interpreters.terminalEnvVarCollectionPrompt.format(terminalPromptName), ...prompts, @@ -83,3 +85,16 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio } } } + +function getPromptName(interpreter?: PythonEnvironment) { + if (!interpreter) { + return ''; + } + if (interpreter.envName) { + return ` "(${interpreter.envName})"`; + } + if (interpreter.envPath) { + return ` "(${path.basename(interpreter.envPath)})"`; + } + return ''; +} diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts index e1bc2d171226..7bddbdcbbfe0 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -35,7 +35,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { let interpreterService: IInterpreterService; const prompts = [Common.doNotShowAgain]; const envName = 'env'; - const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format(` (${envName})`); + const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format(` "(${envName})"`); setup(async () => { shell = mock(); From 7d25ceb8c134c098279f980b1fded2aa2b1c45f6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 30 Aug 2023 09:48:08 -0700 Subject: [PATCH 242/336] Remove finalized api proposals from package.json (#21900) Part of microsoft/vscode#191605 --- package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index bc7378aa521b..e1ceb89d587d 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,7 @@ "envShellEvent", "testObserver", "quickPickItemTooltip", - "envCollectionWorkspace", - "saveEditor", - "envCollectionOptions" + "saveEditor" ], "author": { "name": "Microsoft Corporation" @@ -46,7 +44,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.82.0-20230823" + "vscode": "^1.82.0-20230830" }, "enableTelemetry": false, "keywords": [ From 44f5bf7a158da998776fea1dfd52f3137644bccf Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 30 Aug 2023 13:58:39 -0700 Subject: [PATCH 243/336] Set PS1 for conda environments in non-Windows when in `pythonTerminalEnvVarActivation` experiment (#21902) For #20822 ![image](https://github.com/microsoft/vscode-python/assets/13199757/8c9d4c87-54f2-4661-b6c6-c3b49ee3ff7a) --- .../terminalEnvVarCollectionService.ts | 57 ++++++++++++++++--- ...rminalEnvVarCollectionService.unit.test.ts | 16 ++++-- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 33d51e7e2ed0..3849160b7e4b 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -31,7 +31,7 @@ import { traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../. import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; -import { EnvironmentType } from '../../pythonEnvironments/info'; +import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; import { TerminalShellType } from '../../common/terminal/types'; @@ -44,6 +44,15 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ virtualWorkspace: false, }; + /** + * Prompts for these shells cannot be set reliably using variables + */ + private noPromptVariableShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.fish, + ]; + private deferred: Deferred | undefined; private registeredOnce = false; @@ -150,6 +159,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ ); } const processEnv = this.processEnvVars; + + // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. + env.PS1 = await this.getPS1(shell, resource, env); + Object.keys(env).forEach((key) => { if (shouldSkip(key)) { return; @@ -213,15 +226,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ this.terminalPromptIsCorrect(resource); return; } - // Prompts for these shells cannot be set reliably using variables - const exceptionShells = [ - TerminalShellType.powershell, - TerminalShellType.powershellCore, - TerminalShellType.fish, - TerminalShellType.zsh, // TODO: Remove this once https://github.com/microsoft/vscode/issues/188875 is fixed - ]; const customShellType = identifyShellFromShellPath(shell); - if (exceptionShells.includes(customShellType)) { + if (this.noPromptVariableShells.includes(customShellType)) { return; } if (this.platform.osType !== OSType.Windows) { @@ -243,6 +249,26 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ this.terminalPromptIsCorrect(resource); } + private async getPS1(shell: string, resource: Resource, env: EnvironmentVariables) { + if (env.PS1) { + return env.PS1; + } + const customShellType = identifyShellFromShellPath(shell); + if (this.noPromptVariableShells.includes(customShellType)) { + return undefined; + } + if (this.platform.osType !== OSType.Windows) { + // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const shouldPS1BeSet = interpreter?.type !== undefined; + if (shouldPS1BeSet && !env.PS1) { + // PS1 should be set but no PS1 was set. + return getPromptForEnv(interpreter); + } + } + return undefined; + } + private async handleMicroVenv(resource: Resource) { try { const workspaceFolder = this.getWorkspaceFolder(resource); @@ -313,3 +339,16 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ function shouldSkip(env: string) { return ['_', 'SHLVL'].includes(env); } + +function getPromptForEnv(interpreter: PythonEnvironment | undefined) { + if (!interpreter) { + return undefined; + } + if (interpreter.envName) { + return `(${interpreter.envName}) `; + } + if (interpreter.envPath) { + return `(${path.basename(interpreter.envPath)}) `; + } + return undefined; +} diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 7393f4ad07ad..2e2327bd181c 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -321,9 +321,9 @@ suite('Terminal Environment Variable Collection Service', () => { expect(result).to.equal(false); }); - test('Correct track that prompt was not set for non-Windows zsh where PS1 is set', async () => { + test('Correct track that prompt was set for non-Windows where PS1 is not set but should be set', async () => { when(platform.osType).thenReturn(OSType.Linux); - const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; const ps1Shell = 'zsh'; const resource = Uri.file('a'); const workspaceFolder: WorkspaceFolder = { @@ -332,7 +332,9 @@ suite('Terminal Environment Variable Collection Service', () => { index: 0, }; when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ - type: PythonEnvType.Virtual, + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', } as unknown) as PythonEnvironment); when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); when( @@ -344,13 +346,13 @@ suite('Terminal Environment Variable Collection Service', () => { const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); - expect(result).to.equal(false); + expect(result).to.equal(true); }); - test('Correct track that prompt was not set for non-Windows where PS1 is not set', async () => { + test('Correct track that prompt was not set for non-Windows fish where PS1 is not set', async () => { when(platform.osType).thenReturn(OSType.Linux); const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; - const ps1Shell = 'zsh'; + const ps1Shell = 'fish'; const resource = Uri.file('a'); const workspaceFolder: WorkspaceFolder = { uri: Uri.file('workspacePath'), @@ -359,6 +361,8 @@ suite('Terminal Environment Variable Collection Service', () => { }; when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', } as unknown) as PythonEnvironment); when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); when( From 7a9294cb2e8ccecf3f98130d6361b99556fbd122 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 30 Aug 2023 16:41:16 -0700 Subject: [PATCH 244/336] Apply feedback for terminal activation prompt (#21905) For https://github.com/microsoft/vscode-python/issues/21793 ![image](https://github.com/microsoft/vscode-python/assets/13199757/b3ab6002-0a07-4b3b-8101-a84865ea12e4) --- .../terminalEnvVarCollectionPrompt.ts | 6 ++-- .../terminalEnvVarCollectionService.ts | 4 +++ ...erminalEnvVarCollectionPrompt.unit.test.ts | 4 +-- ...rminalEnvVarCollectionService.unit.test.ts | 28 +++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts index b5d42f0e7dbe..c8aea205a32a 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; +import { Uri, l10n } from 'vscode'; import * as path from 'path'; import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types'; import { @@ -91,10 +91,10 @@ function getPromptName(interpreter?: PythonEnvironment) { return ''; } if (interpreter.envName) { - return ` "(${interpreter.envName})"`; + return `, ${l10n.t('i.e')} "(${interpreter.envName})"`; } if (interpreter.envPath) { - return ` "(${path.basename(interpreter.envPath)})"`; + return `, ${l10n.t('i.e')} "(${path.basename(interpreter.envPath)})"`; } return ''; } diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 3849160b7e4b..fa949ff69fad 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -345,6 +345,10 @@ function getPromptForEnv(interpreter: PythonEnvironment | undefined) { return undefined; } if (interpreter.envName) { + if (interpreter.envName === 'base') { + // If conda base environment is selected, it can lead to "(base)" appearing twice if we return the env name. + return undefined; + } return `(${interpreter.envName}) `; } if (interpreter.envPath) { diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts index 7bddbdcbbfe0..baa83c8b11c5 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -4,7 +4,7 @@ 'use strict'; import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; -import { EventEmitter, Terminal, Uri } from 'vscode'; +import { EventEmitter, Terminal, Uri, l10n } from 'vscode'; import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../../client/common/application/types'; import { IConfigurationService, @@ -35,7 +35,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { let interpreterService: IInterpreterService; const prompts = [Common.doNotShowAgain]; const envName = 'env'; - const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format(` "(${envName})"`); + const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format(`, ${l10n.t('i.e')} "(${envName})"`); setup(async () => { shell = mock(); diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 2e2327bd181c..b3a017031765 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -349,6 +349,34 @@ suite('Terminal Environment Variable Collection Service', () => { expect(result).to.equal(true); }); + test('Correct track that prompt was not set for non-Windows where PS1 is not set but env name is base', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'base', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + test('Correct track that prompt was not set for non-Windows fish where PS1 is not set', async () => { when(platform.osType).thenReturn(OSType.Linux); const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; From d9b9c88f3e555eab540aef5af0f47619c749322e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 31 Aug 2023 10:23:21 -0700 Subject: [PATCH 245/336] Always prepend to PATH instead of replacing it (#21906) For #20822 #11039 Replacing as-is has its problems, for eg. pyenv asks their users to manipulate `PATH` in their init script: https://github.com/pyenv/pyenv#set-up-your-shell-environment-for-pyenv, which we could end up replacing. ![image](https://github.com/microsoft/vscode-python/assets/13199757/cc904f76-8d42-47e1-a6c8-6cfff6543db8) Particularly for pyenv, it mean users not being able to find pyenv: ![image](https://github.com/microsoft/vscode-python/assets/13199757/26100328-c227-435b-a4f2-ec168099f4c1) Prepending solves it for cases where initial PATH value is suffix of the final value: ![image](https://github.com/microsoft/vscode-python/assets/13199757/a95e4ffd-68dc-4e73-905e-504b3051324f) But, in other cases, this means that we end up with the whole `PATH` thrice. This is because it prepends it twice: - Once in shell integration script - Once when creating a process So the final value could be: ``` PATH= ``` where `` refers to value of `PATH` environment variable post activation. eg. ![image](https://github.com/microsoft/vscode-python/assets/13199757/7e771f62-eb53-49be-b261-d259096008f3) --- .../terminalEnvVarCollectionService.ts | 22 ++++++- ...rminalEnvVarCollectionService.unit.test.ts | 64 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index fa949ff69fad..9015dd7b9388 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -167,7 +167,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (shouldSkip(key)) { return; } - const value = env[key]; + let value = env[key]; const prevValue = processEnv[key]; if (prevValue !== value) { if (value !== undefined) { @@ -180,6 +180,26 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ }); return; } + if (key === 'PATH') { + if (processEnv.PATH && env.PATH?.endsWith(processEnv.PATH)) { + // Prefer prepending to PATH instead of replacing it, as we do not want to replace any + // changes to PATH users might have made it in their init scripts (~/.bashrc etc.) + const prependedPart = env.PATH.slice(0, -processEnv.PATH.length); + value = prependedPart; + traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); + envVarCollection.prepend(key, value, { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }); + } else { + traceVerbose(`Prepending environment variable ${key} in collection to ${value}`); + envVarCollection.prepend(key, value, { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }); + } + return; + } traceVerbose(`Setting environment variable ${key} in collection to ${value}`); envVarCollection.replace(key, value, { applyAtShellIntegration: true, diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index b3a017031765..1513be676ee4 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -216,6 +216,70 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); + test('Prepend only "prepend portion of PATH" where applicable', async () => { + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const prependedPart = 'path/to/activate/dir:'; + const envVars: NodeJS.ProcessEnv = { PATH: `${prependedPart}${processEnv.PATH}` }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', prependedPart, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Prepend full PATH otherwise', async () => { + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const finalPath = 'hello/3/2/1'; + const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', finalPath, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + test('Verify envs are not applied if env activation is disabled', async () => { const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; when( From 8c612511b99cb5ebc78153684de8fe595bd154dc Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 5 Sep 2023 11:44:29 -0700 Subject: [PATCH 246/336] Update version for release candidate (#21919) --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e001c51ee220..cfccb197a736 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.15.0-dev", + "version": "2023.16.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.15.0-dev", + "version": "2023.16.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.82.0-20230823" + "vscode": "^1.82.0-20230830" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index e1ceb89d587d..f915aad147a4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.15.0-dev", + "version": "2023.16.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From e8d0ee5bb668b92c75cea40a1395c190d16391cf Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 5 Sep 2023 12:00:28 -0700 Subject: [PATCH 247/336] Update main to next pre-release version (#21921) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfccb197a736..41754245460e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.16.0-rc", + "version": "2023.17.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.16.0-rc", + "version": "2023.17.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index f915aad147a4..384502899d85 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.16.0-rc", + "version": "2023.17.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From c45036bc4e0717af62f2e3b293cc8133dc396e42 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 5 Sep 2023 13:33:27 -0700 Subject: [PATCH 248/336] Update paths for dependabot (#21920) For https://github.com/microsoft/vscode-python/issues/21139 --- .github/dependabot.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d54cf6b74a53..de5ebfe9158b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,27 @@ updates: labels: - 'no-changelog' + - package-ecosystem: 'github-actions' + directory: .github/actions/build-vsix + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/lint + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/smoke-test + schedule: + interval: daily + labels: + - 'no-changelog' + # Not skipping the news for some Python dependencies in case it's actually useful to communicate to users. - package-ecosystem: 'pip' directory: / From 6e9e656eca2c6b7557ec3337d84a132ec9d25b3b Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 5 Sep 2023 13:33:44 -0700 Subject: [PATCH 249/336] Remove activation triggers that are handled automatically (#21918) Closes https://github.com/microsoft/vscode-python/issues/21901 --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index 384502899d85..898f6e05ede1 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,6 @@ "onLanguage:python", "onDebugDynamicConfigurations:python", "onDebugResolve:python", - "onWalkthrough:pythonWelcome", - "onWalkthrough:pythonWelcome2", - "onWalkthrough:pythonDataScienceWelcome", "workspaceContains:mspythonconfig.json", "workspaceContains:pyproject.toml", "workspaceContains:Pipfile", From 2cbafbbb052b16229db5d9d9e236ed9e8615212e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:37:08 -0700 Subject: [PATCH 250/336] Bump actions/setup-node from 2 to 3 in /.github/actions/lint (#21923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2 to 3.
Release notes

Sourced from actions/setup-node's releases.

Add support for asdf format and update actions/cache version to 3.0.0

In scope of this release we updated actions/cache package as the new version contains fixes for caching error handling. Moreover, we added support for asdf format as Node.js version file actions/setup-node#373. Besides, we introduced new output node-version and added npm-shrinkwrap.json to dependency file patterns: actions/setup-node#439

Update actions/cache version to 2.0.2

In scope of this release we updated actions/cache package as the new version contains fixes related to GHES 3.5 (actions/setup-node#460)

v3.0.0

In scope of this release we changed version of the runtime Node.js for the setup-node action and updated package-lock.json file to v2.

Breaking Changes

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/lint/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml index 1efa6aab79a5..e80c78960907 100644 --- a/.github/actions/lint/action.yml +++ b/.github/actions/lint/action.yml @@ -10,7 +10,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ inputs.node_version }} cache: 'npm' From b5d4249c2b810b38d7b179e49470fab3ef155475 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:37:32 -0700 Subject: [PATCH 251/336] Bump actions/upload-artifact from 2 to 3 in /.github/actions/build-vsix (#21924) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
Release notes

Sourced from actions/upload-artifact's releases.

v3.0.0

What's Changed

  • Update default runtime to node16 (#293)
  • Update package-lock.json file version to 2 (#302)

Breaking Changes

With the update to Node 16, all scripts will now be run with Node 16 rather than Node 12.

v2.3.1

Fix for empty fails on Windows failing on upload #281

v2.3.0 Upload Artifact

  • Optimizations for faster uploads of larger files that are already compressed
  • Significantly improved logging when there are chunked uploads
  • Clarifications in logs around the upload size and prohibited characters that aren't allowed in the artifact name or any uploaded files
  • Various other small bugfixes & optimizations

v2.2.4

  • Retry on HTTP 500 responses from the service

v2.2.3

  • Fixes for proxy related issues

v2.2.2

  • Improved retryability and error handling

v2.2.1

  • Update used actions/core package to the latest version

v2.2.0

  • Support for artifact retention

v2.1.4

  • Add Third Party License Information

v2.1.3

  • Use updated version of the @action/artifact NPM package

v2.1.2

  • Increase upload chunk size from 4MB to 8MB
  • Detect case insensitive file uploads

v2.1.1

  • Fix for certain symlinks not correctly being identified as directories before starting uploads

v2.1.0

  • Support for uploading artifacts with multiple paths
  • Support for using exclude paths
  • Updates to dependencies

... (truncated)

Commits
  • 0b7f8ab ci(github): update action/download-artifact from v1 to v3 (#312)
  • 013d2b8 Create devcontainer for codespaces + update all dev dependencies (#375)
  • 055b8b3 Bump Actions NPM dependencies (#374)
  • 7a5d483 ci(github): update action/checkout from v2 to v3 (#315)
  • e0057a5 README: Bump actions/checkout to v3 (#352)
  • 7fe6c13 Update to latest actions/publish-action (#363)
  • 83fd05a Bump actions-core to v1.10.0 (#356)
  • 3cea537 Merge pull request #327 from actions/robherley/artifact-1.1.0
  • 849aa77 nvm use 12 & npm run release
  • 4d39869 recompile with correct ncc version
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 6c4621c7eb9b..ed410c64455c 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -84,7 +84,7 @@ runs: shell: bash - name: Upload VSIX - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ inputs.artifact_name }} path: ${{ inputs.vsix_name }} From 835bc16859c76e04f57decbe359d0aed03438626 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:37:52 -0700 Subject: [PATCH 252/336] Bump actions/setup-python from 2 to 4 in /.github/actions/lint (#21925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4.
Release notes

Sourced from actions/setup-python's releases.

v4.0.0

What's Changed

  • Support for python-version-file input: #336

Example of usage:

- uses: actions/setup-python@v4
  with:
python-version-file: '.python-version' # Read python version from a file
- run: python my_script.py

There is no default python version for this setup-python major version, the action requires to specify either python-version input or python-version-file input. If the python-version input is not specified the action will try to read required version from file from python-version-file input.

  • Use pypyX.Y for PyPy python-version input: #349

Example of usage:

- uses: actions/setup-python@v4
  with:
    python-version: 'pypy3.9' # pypy-X.Y kept for backward compatibility
- run: python my_script.py
  • RUNNER_TOOL_CACHE environment variable is equal AGENT_TOOLSDIRECTORY: #338

  • Bugfix: create missing pypyX.Y symlinks: #347

  • PKG_CONFIG_PATH environment variable: #400

  • Added python-path output: #405 python-path output contains Python executable path.

  • Updated zeit/ncc to vercel/ncc package: #393

  • Bugfix: fixed output for prerelease version of poetry: #409

  • Made pythonLocation environment variable consistent for Python and PyPy: #418

  • Bugfix for 3.x-dev syntax: #417

  • Other improvements: #318 #396 #384 #387 #388

v3.1.4

What's Changed

In the scope of this patch release, the warning for deprecating Python 2.x was added in actions/setup-python#674 by @​dmitry-shibanov

For more information, check out actions/setup-python#672

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=2&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/lint/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml index e80c78960907..1d302b055bee 100644 --- a/.github/actions/lint/action.yml +++ b/.github/actions/lint/action.yml @@ -36,7 +36,7 @@ runs: shell: bash - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.x' cache: 'pip' From d5c077c696dc3f08707317b898ee7e30b1e70bf3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:38:17 -0700 Subject: [PATCH 253/336] Bump actions/setup-node from 2 to 3 in /.github/actions/build-vsix (#21927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2 to 3.
Release notes

Sourced from actions/setup-node's releases.

Add support for asdf format and update actions/cache version to 3.0.0

In scope of this release we updated actions/cache package as the new version contains fixes for caching error handling. Moreover, we added support for asdf format as Node.js version file actions/setup-node#373. Besides, we introduced new output node-version and added npm-shrinkwrap.json to dependency file patterns: actions/setup-node#439

Update actions/cache version to 2.0.2

In scope of this release we updated actions/cache package as the new version contains fixes related to GHES 3.5 (actions/setup-node#460)

v3.0.0

In scope of this release we changed version of the runtime Node.js for the setup-node action and updated package-lock.json file to v2.

Breaking Changes

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index ed410c64455c..962fd85fc355 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -16,7 +16,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ inputs.node_version }} cache: 'npm' From 0cf25478c70ee25c8fbb1720ab2aa754b6e02a87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:38:40 -0700 Subject: [PATCH 254/336] Bump actions/setup-python from 2 to 4 in /.github/actions/build-vsix (#21926) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4.
Release notes

Sourced from actions/setup-python's releases.

v4.0.0

What's Changed

  • Support for python-version-file input: #336

Example of usage:

- uses: actions/setup-python@v4
  with:
python-version-file: '.python-version' # Read python version from a file
- run: python my_script.py

There is no default python version for this setup-python major version, the action requires to specify either python-version input or python-version-file input. If the python-version input is not specified the action will try to read required version from file from python-version-file input.

  • Use pypyX.Y for PyPy python-version input: #349

Example of usage:

- uses: actions/setup-python@v4
  with:
    python-version: 'pypy3.9' # pypy-X.Y kept for backward compatibility
- run: python my_script.py
  • RUNNER_TOOL_CACHE environment variable is equal AGENT_TOOLSDIRECTORY: #338

  • Bugfix: create missing pypyX.Y symlinks: #347

  • PKG_CONFIG_PATH environment variable: #400

  • Added python-path output: #405 python-path output contains Python executable path.

  • Updated zeit/ncc to vercel/ncc package: #393

  • Bugfix: fixed output for prerelease version of poetry: #409

  • Made pythonLocation environment variable consistent for Python and PyPy: #418

  • Bugfix for 3.x-dev syntax: #417

  • Other improvements: #318 #396 #384 #387 #388

v3.1.4

What's Changed

In the scope of this patch release, the warning for deprecating Python 2.x was added in actions/setup-python#674 by @​dmitry-shibanov

For more information, check out actions/setup-python#672

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=2&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 962fd85fc355..4c783fed1c42 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -23,7 +23,7 @@ runs: # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. - name: Use Python 3.7 for JediLSP - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 cache: 'pip' From a72ebb2ca4c8f6ad48c7652769e9ae9df9f92bf6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 17:48:04 +0000 Subject: [PATCH 255/336] Bump actions/checkout from 3 to 4 (#21912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
Release notes

Sourced from actions/checkout's releases.

v4.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v4.0.0

v3.6.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3.5.3...v3.6.0

v3.5.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v3.5.3

v3.5.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v3.5.1...v3.5.2

v3.5.1

What's Changed

New Contributors

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

v4.0.0

v3.6.0

v3.5.3

v3.5.2

v3.5.1

v3.5.0

v3.4.0

v3.3.0

v3.2.0

v3.1.0

v3.0.2

v3.0.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 10 +++++----- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/info-needed-closer.yml | 2 +- .github/workflows/issue-labels.yml | 2 +- .github/workflows/pr-check.yml | 12 ++++++------ .github/workflows/test-plan-item-validator.yml | 2 +- .github/workflows/triage-info-needed.yml | 4 ++-- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24d91b94da10..873b81fa66f8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build VSIX uses: ./.github/actions/build-vsix @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Lint uses: ./.github/actions/lint @@ -82,7 +82,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install core Python requirements uses: brettcannon/pip-secure-install@v1 @@ -125,7 +125,7 @@ jobs: test-suite: [ts-unit, python-unit, venv, single-workspace, multi-workspace, debugger, functional] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} @@ -333,7 +333,7 @@ jobs: os: [ubuntu-latest, windows-latest] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Smoke tests uses: ./.github/actions/smoke-tests diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 278c2cf22e4a..5b037d5a1d0b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml index c0b130be803b..442799cd7a16 100644 --- a/.github/workflows/info-needed-closer.yml +++ b/.github/workflows/info-needed-closer.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index d54015d94e46..7817ed62bd9b 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index aa9cae2aa474..c7335702e991 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build VSIX uses: ./.github/actions/build-vsix @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Lint uses: ./.github/actions/lint @@ -56,7 +56,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install base Python requirements uses: brettcannon/pip-secure-install@v1 @@ -100,7 +100,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} @@ -302,7 +302,7 @@ jobs: steps: # Need the source to have the tests available. - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Smoke tests uses: ./.github/actions/smoke-tests @@ -323,7 +323,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Node uses: actions/setup-node@v3 diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 9d0805a9db9b..17f1740345f2 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -12,7 +12,7 @@ jobs: if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index c717d7ec94b3..24ad2ed2c480 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -13,7 +13,7 @@ jobs: if: contains(github.event.issue.labels.*.name, 'triage-needed') && !contains(github.event.issue.labels.*.name, 'info-needed') steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable From b4c545d52a6e0a91d995c0adc171799705d258c3 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:34:03 -0700 Subject: [PATCH 256/336] Update telemetry package (#21914) This PR updates the [telemetry package](https://github.com/microsoft/vscode-extension-telemetry) to the latest version and fixes the formatting of a line --- package-lock.json | 1055 +++++++++++++++++++++++++-------------------- package.json | 6 +- 2 files changed, 602 insertions(+), 459 deletions(-) diff --git a/package-lock.json b/package-lock.json index 41754245460e..70205a90f00e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", - "@vscode/extension-telemetry": "^0.7.7", + "@vscode/extension-telemetry": "^0.8.4", "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", "diff-match-patch": "^1.0.0", @@ -165,41 +165,43 @@ } }, "node_modules/@azure/abort-controller/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/core-auth": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.4.0.tgz", - "integrity": "sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", + "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", "dependencies": { "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.1.0", "tslib": "^2.2.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/@azure/core-auth/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.3.tgz", - "integrity": "sha512-AMQb0ttiGJ0MIV/r+4TVra6U4+90mPeOveehFnrqKlo7dknPJYdJ61wOzYJXJjDxF8LcCtSogfRelkq+fCGFTw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.1.tgz", + "integrity": "sha512-Kji9k6TOFRDB5ZMTw8qUf2IJ+CeJtsuMdAHox9eqpTf1cefiNMpzrfnF6sINEBZJsaVaWgQ0o48B6kcUH68niA==", "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.4.0", "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.3.0", + "@azure/core-util": "^1.0.0", "@azure/logger": "^1.0.0", "form-data": "^4.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", - "tslib": "^2.2.0" + "tslib": "^2.2.0", + "uuid": "^8.3.0" }, "engines": { "node": ">=14.0.0" @@ -243,9 +245,9 @@ } }, "node_modules/@azure/core-rest-pipeline/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/core-tracing": { "version": "1.0.1", @@ -259,14 +261,14 @@ } }, "node_modules/@azure/core-tracing/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/core-util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.0.tgz", - "integrity": "sha512-ANP0Er7R2KHHHjwmKzPF9wbd0gXvOX7yRRHeYL1eNd/OaNrMLyfZH/FQasHRVAf6rMXX+EAUpvYwLMFDHDI5Gw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz", + "integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==", "dependencies": { "@azure/abort-controller": "^1.0.0", "tslib": "^2.2.0" @@ -276,9 +278,9 @@ } }, "node_modules/@azure/core-util/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/logger": { "version": "1.0.4", @@ -292,9 +294,30 @@ } }, "node_modules/@azure/logger/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { + "version": "1.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz", + "integrity": "sha512-fsUarKQDvjhmBO4nIfaZkfNSApm1hZBzcvpNbSrXdcUBxu7lRvKsV5DnwszX7cnhLyVOW9yl1uigtRQ1yDANjA==", + "dependencies": { + "@azure/core-tracing": "^1.0.0", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/instrumentation": "^0.41.2", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@babel/code-frame": { "version": "7.12.11", @@ -957,83 +980,122 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.9.tgz", - "integrity": "sha512-3pCfM2TzHn3gU9pxHztduKcVRdb/nzruvPFfHPZD0IM0mb0h6TGo2isELF3CTMahTx50RAC51ojNIw2/7VRkOg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", + "integrity": "sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg==", "dependencies": { - "@microsoft/applicationinsights-core-js": "2.8.10", + "@microsoft/applicationinsights-core-js": "2.8.15", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.9.tgz", - "integrity": "sha512-D/RtqkQ2Nr4cuoGqmhi5QTmi3cBlxehIThJ1u3BaH9H/YkLNTKEcHZRWTXy14bXheCefNHciLuadg37G2Kekcg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz", + "integrity": "sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA==", "dependencies": { - "@microsoft/1ds-core-js": "3.2.9", + "@microsoft/1ds-core-js": "3.2.13", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.11.tgz", - "integrity": "sha512-DGDNzT4DMlSvUzWjA4y3tDg47+QYOPV+W07vlfdPwGgLwrl4n6Q4crrW8Y/IOpthHAKDU8rolSAUvP3NqxPi4Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.2.tgz", + "integrity": "sha512-jDBNKbCHsJgmpv0CKNhJ/uN9ZphvfGdb93Svk+R4LjO8L3apNNMbDDPxBvXXi0uigRmA1TBcmyBG4IRKjabGhw==", "dependencies": { - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "dependencies": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, "node_modules/@microsoft/applicationinsights-common": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.11.tgz", - "integrity": "sha512-Cxu4gRajkYv9buEtrcLGHK97AqGK62feN9jH9/JSjUSiSFhbnWtYvEg1EMqMI/P4pneu53yLJloITB+TKwmK7A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.2.tgz", + "integrity": "sha512-y+WXWop+OVim954Cu1uyYMnNx6PWO8okHpZIQi/1YSqtqaYdtJVPv4P0AVzwJdohxzVfgzKvqj9nec/VWqE2Zg==", "dependencies": { - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "dependencies": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, + "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.10.tgz", - "integrity": "sha512-jQrufDW0+sV8fBhRvzIPNGiCC6dELH+Ug0DM5CfN9757TBqZJz8CSWyDjex39as8+jD0F/8HRU9QdmrVgq5vFg==", + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz", + "integrity": "sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ==", "dependencies": { "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/dynamicproto-js": "^1.1.9" }, "peerDependencies": { "tslib": "*" @@ -1045,32 +1107,52 @@ "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.11.tgz", - "integrity": "sha512-11T7bbP4ifIBg95E9mYZv1g/vcWvw/KaWKRcGMREP3+vBTLBwMB8r2e9Zd583bOVx+9/gRvfIg+Z/lInQqAfbA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.2.tgz", + "integrity": "sha512-6Lq0DE/pZp9RvSV+weGbcxN1NDmfczj6gNPhvZKV2YSQ3RK0LZE3+wjTWLXfuStq8a+nCBdsRpWk8tOKgsoxcg==", "dependencies": { - "@microsoft/applicationinsights-channel-js": "2.8.11", - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-channel-js": "3.0.2", + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "dependencies": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, + "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, "node_modules/@microsoft/applicationinsights-web-snippet": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.0.1.tgz", @@ -1081,6 +1163,19 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "node_modules/@nevware21/ts-async": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", + "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + } + }, + "node_modules/@nevware21/ts-utils": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", + "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + }, "node_modules/@nicolo-ribaudo/semver-v6": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", @@ -1134,11 +1229,11 @@ } }, "node_modules/@opentelemetry/core": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.11.0.tgz", - "integrity": "sha512-aP1wHSb+YfU0pM63UAkizYPuS4lZxzavHHw5KJfFNN2oWQ79HSm6JR3CzwFKHwKhSzHN8RE9fgP1IdVJ8zmo1w==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.15.2.tgz", + "integrity": "sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw==", "dependencies": { - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/semantic-conventions": "1.15.2" }, "engines": { "node": ">=14" @@ -1147,13 +1242,31 @@ "@opentelemetry/api": ">=1.0.0 <1.5.0" } }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", + "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", + "dependencies": { + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "1.4.2", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.1", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/resources": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.11.0.tgz", - "integrity": "sha512-y0z2YJTqk0ag+hGT4EXbxH/qPhDe8PfwltYb4tXIEsozgEFfut/bqW7H7pDvylmCjBRMG4NjtLp57V1Ev++brA==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.15.2.tgz", + "integrity": "sha512-xmMRLenT9CXmm5HMbzpZ1hWhaUowQf8UB4jMjFlAxx1QzQcsD3KFNAVX/CAWzFPtllTyTplrA4JrQ7sCH3qmYw==", "dependencies": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" }, "engines": { "node": ">=14" @@ -1163,13 +1276,13 @@ } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.11.0.tgz", - "integrity": "sha512-DV8e5/Qo42V8FMBlQ0Y0Liv6Hl/Pp5bAZ73s7r1euX8w4bpRes1B7ACiA4yujADbWMJxBgSo4fGbi4yjmTMG2A==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.15.2.tgz", + "integrity": "sha512-BEaxGZbWtvnSPchV98qqqqa96AOcb41pjgvhfzDij10tkBhIu9m0Jd6tZ1tJB5ZHfHbTffqYVYE0AOGobec/EQ==", "dependencies": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/resources": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/resources": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" }, "engines": { "node": ">=14" @@ -1179,9 +1292,9 @@ } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.11.0.tgz", - "integrity": "sha512-fG4D0AktoHyHwGhFGv+PzKrZjxbKJfckJauTJdq2A+ej5cTazmNYjJVAODXXkYyrsI10muMl+B1iO2q1R6Lp+w==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz", + "integrity": "sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw==", "engines": { "node": ">=14" } @@ -1451,6 +1564,11 @@ "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", "dev": true }, + "node_modules/@types/shimmer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.2.tgz", + "integrity": "sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg==" + }, "node_modules/@types/shortid": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", @@ -1564,21 +1682,6 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/experimental-utils": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", @@ -1688,21 +1791,6 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", @@ -1726,14 +1814,14 @@ "dev": true }, "node_modules/@vscode/extension-telemetry": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.7.7.tgz", - "integrity": "sha512-uW508BPjkWDBOKvvvSym3ZmGb7kHIiWaAfB/1PHzLz2x9TrC33CfjmFEI+CywIL/jBv4bqZxxjN4tfefB61F+g==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz", + "integrity": "sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA==", "dependencies": { - "@microsoft/1ds-core-js": "^3.2.9", - "@microsoft/1ds-post-js": "^3.2.9", - "@microsoft/applicationinsights-web-basic": "^2.8.11", - "applicationinsights": "2.5.0" + "@microsoft/1ds-core-js": "^3.2.13", + "@microsoft/1ds-post-js": "^3.2.13", + "@microsoft/applicationinsights-web-basic": "^3.0.2", + "applicationinsights": "^2.7.1" }, "engines": { "vscode": "^1.75.0" @@ -2069,7 +2157,6 @@ "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2078,10 +2165,9 @@ } }, "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "peerDependencies": { "acorn": "^8" } @@ -2279,21 +2365,23 @@ } }, "node_modules/applicationinsights": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.5.0.tgz", - "integrity": "sha512-6kIFmpANRok+6FhCOmO7ZZ/mh7fdNKn17BaT13cg/RV5roLPJlA6q8srWexayHd3MPcwMb9072e8Zp0P47s/pw==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.7.3.tgz", + "integrity": "sha512-JY8+kTEkjbA+kAVNWDtpfW2lqsrDALfDXuxOs74KLPu2y13fy/9WB52V4LfYVTVcW1/jYOXjTxNS2gPZIDh1iw==", "dependencies": { - "@azure/core-auth": "^1.4.0", - "@azure/core-rest-pipeline": "^1.10.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-rest-pipeline": "1.10.1", + "@azure/core-util": "1.2.0", + "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.5", "@microsoft/applicationinsights-web-snippet": "^1.0.1", - "@opentelemetry/api": "^1.0.4", - "@opentelemetry/core": "^1.0.1", - "@opentelemetry/sdk-trace-base": "^1.0.1", - "@opentelemetry/semantic-conventions": "^1.0.1", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/sdk-trace-base": "^1.15.2", + "@opentelemetry/semantic-conventions": "^1.15.2", "cls-hooked": "^4.2.2", "continuation-local-storage": "^3.2.1", - "diagnostic-channel": "1.1.0", - "diagnostic-channel-publishers": "1.0.5" + "diagnostic-channel": "1.1.1", + "diagnostic-channel-publishers": "1.0.7" }, "engines": { "node": ">=8.0.0" @@ -3813,6 +3901,11 @@ "deprecated": "CircularJSON is in maintenance only, flatted is its successor.", "dev": true }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" + }, "node_modules/class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -4771,29 +4864,21 @@ } }, "node_modules/diagnostic-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz", - "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", + "integrity": "sha512-r2HV5qFkUICyoaKlBEpLKHjxMXATUf/l+h8UZPGBHGLy4DDiY2sOLcIctax4eRnTw5wH2jTMExLntGPJ8eOJxw==", "dependencies": { - "semver": "^5.3.0" + "semver": "^7.5.3" } }, "node_modules/diagnostic-channel-publishers": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz", - "integrity": "sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.7.tgz", + "integrity": "sha512-SEECbY5AiVt6DfLkhkaHNeshg1CogdLLANA8xlG/TKvS+XUgvIKl7VspJGYiEdL5OUyzMVnr7o0AwB7f+/Mjtg==", "peerDependencies": { "diagnostic-channel": "*" } }, - "node_modules/diagnostic-channel/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -5827,21 +5912,6 @@ "node": ">=0.4.0" } }, - "node_modules/eslint/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint/node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6908,8 +6978,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -7506,7 +7575,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -7852,6 +7920,17 @@ "node": ">=4" } }, + "node_modules/import-in-the-middle": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", + "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-assertions": "^1.9.0", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -8057,10 +8136,9 @@ } }, "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dependencies": { "has": "^1.0.3" }, @@ -9982,6 +10060,11 @@ "node": ">=10" } }, + "node_modules/module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + }, "node_modules/mrmime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz", @@ -10141,22 +10224,6 @@ "node": ">=10" } }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "optional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", @@ -11218,8 +11285,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-root": { "version": "0.1.1", @@ -11963,6 +12029,35 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", + "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==", + "dependencies": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/require-in-the-middle/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -11970,12 +12065,11 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "dependencies": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -12215,9 +12309,9 @@ } }, "node_modules/semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -13131,7 +13225,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -13715,21 +13808,6 @@ "node": ">=8.6" } }, - "node_modules/ts-loader/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ts-loader/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -14400,7 +14478,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, "bin": { "uuid": "dist/bin/uuid" } @@ -14576,20 +14653,6 @@ "vscode": "^1.67.0" } }, - "node_modules/vscode-languageclient/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/vscode-languageserver": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", @@ -15415,42 +15478,44 @@ }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, "@azure/core-auth": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.4.0.tgz", - "integrity": "sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", + "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", "requires": { "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.1.0", "tslib": "^2.2.0" }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, "@azure/core-rest-pipeline": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.3.tgz", - "integrity": "sha512-AMQb0ttiGJ0MIV/r+4TVra6U4+90mPeOveehFnrqKlo7dknPJYdJ61wOzYJXJjDxF8LcCtSogfRelkq+fCGFTw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.1.tgz", + "integrity": "sha512-Kji9k6TOFRDB5ZMTw8qUf2IJ+CeJtsuMdAHox9eqpTf1cefiNMpzrfnF6sINEBZJsaVaWgQ0o48B6kcUH68niA==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.4.0", "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.3.0", + "@azure/core-util": "^1.0.0", "@azure/logger": "^1.0.0", "form-data": "^4.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", - "tslib": "^2.2.0" + "tslib": "^2.2.0", + "uuid": "^8.3.0" }, "dependencies": { "@tootallnate/once": { @@ -15477,9 +15542,9 @@ } }, "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, @@ -15492,25 +15557,25 @@ }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, "@azure/core-util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.0.tgz", - "integrity": "sha512-ANP0Er7R2KHHHjwmKzPF9wbd0gXvOX7yRRHeYL1eNd/OaNrMLyfZH/FQasHRVAf6rMXX+EAUpvYwLMFDHDI5Gw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz", + "integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==", "requires": { "@azure/abort-controller": "^1.0.0", "tslib": "^2.2.0" }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, @@ -15523,9 +15588,29 @@ }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@azure/opentelemetry-instrumentation-azure-sdk": { + "version": "1.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz", + "integrity": "sha512-fsUarKQDvjhmBO4nIfaZkfNSApm1hZBzcvpNbSrXdcUBxu7lRvKsV5DnwszX7cnhLyVOW9yl1uigtRQ1yDANjA==", + "requires": { + "@azure/core-tracing": "^1.0.0", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/instrumentation": "^0.41.2", + "tslib": "^2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, @@ -16031,75 +16116,114 @@ } }, "@microsoft/1ds-core-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.9.tgz", - "integrity": "sha512-3pCfM2TzHn3gU9pxHztduKcVRdb/nzruvPFfHPZD0IM0mb0h6TGo2isELF3CTMahTx50RAC51ojNIw2/7VRkOg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", + "integrity": "sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg==", "requires": { - "@microsoft/applicationinsights-core-js": "2.8.10", + "@microsoft/applicationinsights-core-js": "2.8.15", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "@microsoft/1ds-post-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.9.tgz", - "integrity": "sha512-D/RtqkQ2Nr4cuoGqmhi5QTmi3cBlxehIThJ1u3BaH9H/YkLNTKEcHZRWTXy14bXheCefNHciLuadg37G2Kekcg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz", + "integrity": "sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA==", "requires": { - "@microsoft/1ds-core-js": "3.2.9", + "@microsoft/1ds-core-js": "3.2.13", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "@microsoft/applicationinsights-channel-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.11.tgz", - "integrity": "sha512-DGDNzT4DMlSvUzWjA4y3tDg47+QYOPV+W07vlfdPwGgLwrl4n6Q4crrW8Y/IOpthHAKDU8rolSAUvP3NqxPi4Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.2.tgz", + "integrity": "sha512-jDBNKbCHsJgmpv0CKNhJ/uN9ZphvfGdb93Svk+R4LjO8L3apNNMbDDPxBvXXi0uigRmA1TBcmyBG4IRKjabGhw==", "requires": { - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "dependencies": { "@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", + "requires": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + } + }, + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", "requires": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } }, "@microsoft/applicationinsights-common": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.11.tgz", - "integrity": "sha512-Cxu4gRajkYv9buEtrcLGHK97AqGK62feN9jH9/JSjUSiSFhbnWtYvEg1EMqMI/P4pneu53yLJloITB+TKwmK7A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.2.tgz", + "integrity": "sha512-y+WXWop+OVim954Cu1uyYMnNx6PWO8okHpZIQi/1YSqtqaYdtJVPv4P0AVzwJdohxzVfgzKvqj9nec/VWqE2Zg==", "requires": { - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "dependencies": { "@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", + "requires": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + } + }, + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", "requires": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } }, "@microsoft/applicationinsights-core-js": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.10.tgz", - "integrity": "sha512-jQrufDW0+sV8fBhRvzIPNGiCC6dELH+Ug0DM5CfN9757TBqZJz8CSWyDjex39as8+jD0F/8HRU9QdmrVgq5vFg==", + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz", + "integrity": "sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ==", "requires": { "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/dynamicproto-js": "^1.1.9" } }, "@microsoft/applicationinsights-shims": { @@ -16108,24 +16232,44 @@ "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" }, "@microsoft/applicationinsights-web-basic": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.11.tgz", - "integrity": "sha512-11T7bbP4ifIBg95E9mYZv1g/vcWvw/KaWKRcGMREP3+vBTLBwMB8r2e9Zd583bOVx+9/gRvfIg+Z/lInQqAfbA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.2.tgz", + "integrity": "sha512-6Lq0DE/pZp9RvSV+weGbcxN1NDmfczj6gNPhvZKV2YSQ3RK0LZE3+wjTWLXfuStq8a+nCBdsRpWk8tOKgsoxcg==", "requires": { - "@microsoft/applicationinsights-channel-js": "2.8.11", - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-channel-js": "3.0.2", + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "dependencies": { "@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "requires": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + } + }, + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } @@ -16140,6 +16284,19 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "@nevware21/ts-async": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", + "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "requires": { + "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + } + }, + "@nevware21/ts-utils": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", + "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + }, "@nicolo-ribaudo/semver-v6": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", @@ -16178,36 +16335,48 @@ "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==" }, "@opentelemetry/core": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.11.0.tgz", - "integrity": "sha512-aP1wHSb+YfU0pM63UAkizYPuS4lZxzavHHw5KJfFNN2oWQ79HSm6JR3CzwFKHwKhSzHN8RE9fgP1IdVJ8zmo1w==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.15.2.tgz", + "integrity": "sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw==", "requires": { - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/semantic-conventions": "1.15.2" + } + }, + "@opentelemetry/instrumentation": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", + "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", + "requires": { + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "1.4.2", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.1", + "shimmer": "^1.2.1" } }, "@opentelemetry/resources": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.11.0.tgz", - "integrity": "sha512-y0z2YJTqk0ag+hGT4EXbxH/qPhDe8PfwltYb4tXIEsozgEFfut/bqW7H7pDvylmCjBRMG4NjtLp57V1Ev++brA==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.15.2.tgz", + "integrity": "sha512-xmMRLenT9CXmm5HMbzpZ1hWhaUowQf8UB4jMjFlAxx1QzQcsD3KFNAVX/CAWzFPtllTyTplrA4JrQ7sCH3qmYw==", "requires": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" } }, "@opentelemetry/sdk-trace-base": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.11.0.tgz", - "integrity": "sha512-DV8e5/Qo42V8FMBlQ0Y0Liv6Hl/Pp5bAZ73s7r1euX8w4bpRes1B7ACiA4yujADbWMJxBgSo4fGbi4yjmTMG2A==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.15.2.tgz", + "integrity": "sha512-BEaxGZbWtvnSPchV98qqqqa96AOcb41pjgvhfzDij10tkBhIu9m0Jd6tZ1tJB5ZHfHbTffqYVYE0AOGobec/EQ==", "requires": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/resources": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/resources": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" } }, "@opentelemetry/semantic-conventions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.11.0.tgz", - "integrity": "sha512-fG4D0AktoHyHwGhFGv+PzKrZjxbKJfckJauTJdq2A+ej5cTazmNYjJVAODXXkYyrsI10muMl+B1iO2q1R6Lp+w==" + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz", + "integrity": "sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw==" }, "@polka/url": { "version": "1.0.0-next.21", @@ -16468,6 +16637,11 @@ "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", "dev": true }, + "@types/shimmer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.2.tgz", + "integrity": "sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg==" + }, "@types/shortid": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", @@ -16556,15 +16730,6 @@ "requires": { "ms": "2.1.2" } - }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } } } }, @@ -16624,15 +16789,6 @@ "requires": { "ms": "2.1.2" } - }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } } } }, @@ -16652,14 +16808,14 @@ "dev": true }, "@vscode/extension-telemetry": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.7.7.tgz", - "integrity": "sha512-uW508BPjkWDBOKvvvSym3ZmGb7kHIiWaAfB/1PHzLz2x9TrC33CfjmFEI+CywIL/jBv4bqZxxjN4tfefB61F+g==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz", + "integrity": "sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA==", "requires": { - "@microsoft/1ds-core-js": "^3.2.9", - "@microsoft/1ds-post-js": "^3.2.9", - "@microsoft/applicationinsights-web-basic": "^2.8.11", - "applicationinsights": "2.5.0" + "@microsoft/1ds-core-js": "^3.2.13", + "@microsoft/1ds-post-js": "^3.2.13", + "@microsoft/applicationinsights-web-basic": "^3.0.2", + "applicationinsights": "^2.7.1" } }, "@vscode/jupyter-lsp-middleware": { @@ -16951,14 +17107,12 @@ "acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==" }, "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "requires": {} }, "acorn-jsx": { @@ -17108,21 +17262,23 @@ } }, "applicationinsights": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.5.0.tgz", - "integrity": "sha512-6kIFmpANRok+6FhCOmO7ZZ/mh7fdNKn17BaT13cg/RV5roLPJlA6q8srWexayHd3MPcwMb9072e8Zp0P47s/pw==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.7.3.tgz", + "integrity": "sha512-JY8+kTEkjbA+kAVNWDtpfW2lqsrDALfDXuxOs74KLPu2y13fy/9WB52V4LfYVTVcW1/jYOXjTxNS2gPZIDh1iw==", "requires": { - "@azure/core-auth": "^1.4.0", - "@azure/core-rest-pipeline": "^1.10.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-rest-pipeline": "1.10.1", + "@azure/core-util": "1.2.0", + "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.5", "@microsoft/applicationinsights-web-snippet": "^1.0.1", - "@opentelemetry/api": "^1.0.4", - "@opentelemetry/core": "^1.0.1", - "@opentelemetry/sdk-trace-base": "^1.0.1", - "@opentelemetry/semantic-conventions": "^1.0.1", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/sdk-trace-base": "^1.15.2", + "@opentelemetry/semantic-conventions": "^1.15.2", "cls-hooked": "^4.2.2", "continuation-local-storage": "^3.2.1", - "diagnostic-channel": "1.1.0", - "diagnostic-channel-publishers": "1.0.5" + "diagnostic-channel": "1.1.1", + "diagnostic-channel-publishers": "1.0.7" } }, "arch": { @@ -18288,6 +18444,11 @@ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", "dev": true }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -19086,24 +19247,17 @@ "optional": true }, "diagnostic-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz", - "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", + "integrity": "sha512-r2HV5qFkUICyoaKlBEpLKHjxMXATUf/l+h8UZPGBHGLy4DDiY2sOLcIctax4eRnTw5wH2jTMExLntGPJ8eOJxw==", "requires": { - "semver": "^5.3.0" - }, - "dependencies": { - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" - } + "semver": "^7.5.3" } }, "diagnostic-channel-publishers": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz", - "integrity": "sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.7.tgz", + "integrity": "sha512-SEECbY5AiVt6DfLkhkaHNeshg1CogdLLANA8xlG/TKvS+XUgvIKl7VspJGYiEdL5OUyzMVnr7o0AwB7f+/Mjtg==", "requires": {} }, "diff": { @@ -19653,15 +19807,6 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -20775,8 +20920,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -21253,7 +21397,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -21504,6 +21647,17 @@ } } }, + "import-in-the-middle": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", + "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", + "requires": { + "acorn": "^8.8.2", + "acorn-import-assertions": "^1.9.0", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -21657,10 +21811,9 @@ "dev": true }, "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "requires": { "has": "^1.0.3" } @@ -23134,6 +23287,11 @@ } } }, + "module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + }, "mrmime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz", @@ -23274,18 +23432,6 @@ "optional": true, "requires": { "semver": "^7.3.5" - }, - "dependencies": { - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "optional": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "node-addon-api": { @@ -24124,8 +24270,7 @@ "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-root": { "version": "0.1.1", @@ -24703,6 +24848,26 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, + "require-in-the-middle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", + "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==", + "requires": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -24710,12 +24875,11 @@ "dev": true }, "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "requires": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -24894,9 +25058,9 @@ } }, "semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -25604,8 +25768,7 @@ "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, "sver-compat": { "version": "1.5.0", @@ -26054,15 +26217,6 @@ "picomatch": "^2.3.1" } }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -26589,8 +26743,7 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache": { "version": "2.3.0", @@ -26736,16 +26889,6 @@ "minimatch": "^5.1.0", "semver": "^7.3.7", "vscode-languageserver-protocol": "3.17.3" - }, - "dependencies": { - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "vscode-languageserver": { diff --git a/package.json b/package.json index 898f6e05ede1..7e044a95f7af 100644 --- a/package.json +++ b/package.json @@ -1142,7 +1142,7 @@ "scope": "machine", "type": "string" }, - "python.missingPackage.severity":{ + "python.missingPackage.severity": { "default": "Hint", "description": "%python.missingPackage.severity.description%", "enum": [ @@ -2104,7 +2104,7 @@ }, "dependencies": { "@iarna/toml": "^2.2.5", - "@vscode/extension-telemetry": "^0.7.7", + "@vscode/extension-telemetry": "^0.8.4", "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", "diff-match-patch": "^1.0.0", @@ -2221,4 +2221,4 @@ "webpack-require-from": "^1.8.6", "yargs": "^15.3.1" } -} +} \ No newline at end of file From 69e8e7d13b96c55ba38f9c3c6599fde9be0bdf83 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 7 Sep 2023 15:13:30 -0700 Subject: [PATCH 257/336] Catch errors when looking up python binaries in a PATH (#21948) Closes https://github.com/microsoft/vscode-python/issues/21944 --- src/client/pythonEnvironments/common/posixUtils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/client/pythonEnvironments/common/posixUtils.ts b/src/client/pythonEnvironments/common/posixUtils.ts index eb60fc029949..0e79ec9d590e 100644 --- a/src/client/pythonEnvironments/common/posixUtils.ts +++ b/src/client/pythonEnvironments/common/posixUtils.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import { uniq } from 'lodash'; import { getSearchPathEntries } from '../../common/utils/exec'; import { resolveSymbolicLink } from './externalDependencies'; -import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { traceError, traceInfo, traceVerbose, traceWarn } from '../../logging'; /** * Determine if the given filename looks like the simplest Python executable. @@ -117,7 +117,10 @@ function pickShortestPath(pythonPaths: string[]) { export async function getPythonBinFromPosixPaths(searchDirs: string[]): Promise { const binToLinkMap = new Map(); for (const searchDir of searchDirs) { - const paths = await findPythonBinariesInDir(searchDir); + const paths = await findPythonBinariesInDir(searchDir).catch((ex) => { + traceWarn('Looking for python binaries within', searchDir, 'failed with', ex); + return []; + }); for (const filepath of paths) { // Ensure that we have a collection of unique global binaries by From 7196a36b98d92726fc9bc5901cd06e9cd05be05f Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 8 Sep 2023 11:47:05 -0700 Subject: [PATCH 258/336] Update Python extension API version (#21953) --- pythonExtensionApi/package-lock.json | 4 ++-- pythonExtensionApi/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index 9b4847457b20..1f4098e1b0de 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vscode/python-extension", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.4", + "version": "1.0.5", "license": "MIT", "devDependencies": { "@types/vscode": "^1.78.0", diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index 86ac58f42f20..baabf85d6549 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.4", + "version": "1.0.5", "author": { "name": "Microsoft Corporation" }, From 30c83a3d32b6ab0225ec06a06904285c93efd48c Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Sat, 9 Sep 2023 01:59:07 -0700 Subject: [PATCH 259/336] Added git settings for branch name suggestion, protection, pull, and mergeEditor (#21954) VS Code repository, specifically in the .vscode/settings.json, has some nice git features such as: Issue: #21955 "git.branchRandomName.enable" (for suggesting random branch name when creating a new branch, comes in very handy when person wants to make and try quick changes in Codespaces), "git.branchProtection" (for branch protection), "git.pullBeforeCheckout": (for pulling before checking out a branch), "git.mergeEditor": (for making easier when in times of resolving merge conflicts) which I found could be useful to the Python extension repository as well. Credits to @karrtikr for suggesting random name, and branch protection. --- .vscode/settings.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 174a850c901e..06011b3d13cd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -65,5 +65,14 @@ "--max-line-length=88" ], "typescript.preferences.importModuleSpecifier": "relative", - "debug.javascript.usePreview": false + "debug.javascript.usePreview": false, + // Branch name suggestion. + "git.branchRandomName.enable": true, + "git.branchProtection": [ + "main", + "release/*" + ], + "git.pullBeforeCheckout": true, + // Open merge editor for resolving conflicts. + "git.mergeEditor": true, } From 8543dd356312fb3ea6b4b66561d896408de7faab Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 11 Sep 2023 11:21:44 -0400 Subject: [PATCH 260/336] Fix unittest subtest names that have spaces (#21947) fixes https://github.com/microsoft/vscode-python/issues/21733#issuecomment-1707804763 --- .../testController/common/resultResolver.ts | 8 +++++--- .../testController/resultResolver.unit.test.ts | 15 ++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 6e875473c836..aa9f2a541f51 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -216,9 +216,11 @@ export class PythonResultResolver implements ITestResultResolver { throw new Error('Parent test item not found'); } } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const subtestId = keyTemp.split(' ')[1]; + // split only on first " [" since the subtest ID has the parent test ID in the first part of the ID. + const index = keyTemp.indexOf(' ['); + const parentTestCaseId = keyTemp.substring(0, index); + // add one to index to remove the space from the start of the subtest ID + const subtestId = keyTemp.substring(index + 1, keyTemp.length); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); // find the subtest's parent test item diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 09a68128167d..694fdacb8049 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -270,15 +270,16 @@ suite('Result Resolver tests', () => { testProvider, workspaceUri, ); - const mockSubtestItem = createMockTestItem('parentTest subTest'); + const subtestName = 'parentTest [subTest with spaces and [brackets]]'; + const mockSubtestItem = createMockTestItem(subtestName); // add a mock test item to the map of known VSCode ids to run ids resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); // creates a mock test item with a space which will be used to split the runId - resultResolver.runIdToVSid.set('parentTest subTest', 'parentTest subTest'); + resultResolver.runIdToVSid.set(subtestName, subtestName); // add this mock test to the map of known test items resultResolver.runIdToTestItem.set('parentTest', mockTestItem2); - resultResolver.runIdToTestItem.set('parentTest subTest', mockSubtestItem); + resultResolver.runIdToTestItem.set(subtestName, mockSubtestItem); let generatedId: string | undefined; testControllerMock @@ -294,12 +295,12 @@ suite('Result Resolver tests', () => { cwd: workspaceUri.fsPath, status: 'success', result: { - 'parentTest subTest': { - test: 'test', + 'parentTest [subTest with spaces and [brackets]]': { + test: 'parentTest', outcome: 'subtest-success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess message: 'message', traceback: 'traceback', - subtest: 'subtest', + subtest: subtestName, }, }, error: '', @@ -310,7 +311,7 @@ suite('Result Resolver tests', () => { // verify that the passed function was called for the single test item assert.ok(generatedId); - assert.strictEqual(generatedId, 'subTest'); + assert.strictEqual(generatedId, '[subTest with spaces and [brackets]]'); }); test('resolveExecution handles failed tests correctly', async () => { // test specific constants used expected values From e32657f83eb5d9c65f5190b91573405e91f12e6c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 11 Sep 2023 09:37:21 -0700 Subject: [PATCH 261/336] incorrect print included for absolute path calculations (#21932) an additional print statement was left in the pytest plugin which unnecessarily printed all absolute paths calculated. --- pythonFiles/vscode_pytest/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index adf72c134119..3d5bde44204f 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -126,7 +126,6 @@ def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: """ split_id = test_id.split("::")[1:] absolute_test_id = "::".join([str(testPath), *split_id]) - print("absolute path", absolute_test_id) return absolute_test_id From d9a23181279e39905fb7969fcd930fb46085c900 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Sep 2023 10:34:28 -0700 Subject: [PATCH 262/336] Do not assume casing of activated environment variables Python returns (#21970) For #20950 --- .../activation/terminalEnvVarCollectionService.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 9015dd7b9388..39bee239806b 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -133,12 +133,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); return; } - const env = await this.environmentActivationService.getActivatedEnvironmentVariables( + const activatedEnv = await this.environmentActivationService.getActivatedEnvironmentVariables( resource, undefined, undefined, shell, ); + const env = activatedEnv ? normCaseKeys(activatedEnv) : undefined; if (!env) { const shellType = identifyShellFromShellPath(shell); const defaultShell = defaultShells[this.platform.osType]; @@ -158,7 +159,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ shell, ); } - const processEnv = this.processEnvVars; + const processEnv = normCaseKeys(this.processEnvVars); // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. env.PS1 = await this.getPS1(shell, resource, env); @@ -376,3 +377,11 @@ function getPromptForEnv(interpreter: PythonEnvironment | undefined) { } return undefined; } + +function normCaseKeys(env: EnvironmentVariables): EnvironmentVariables { + const result: EnvironmentVariables = {}; + Object.keys(env).forEach((key) => { + result[key.toUpperCase()] = env[key]; + }); + return result; +} From 91b2c113f168069f76a2f26c37a2d2f8f759a682 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:52:46 -0700 Subject: [PATCH 263/336] Drop Python 3.7 support (#21962) Drop Python 3.7 support, and replace with Python 3.8 Resolves: #21532 /vscode-python/requirements.txt generated same hash even when running: ```pip-compile --generate-hashes requirements.in``` from the Python3.8 virtual environment. - Same result with pythonFiles/jedilsp_requirements/requirements.txt when running: ```pip-compile --generate-hashes pythonFiles/jedilsp_requirements/requirements.in``` --- .github/actions/build-vsix/action.yml | 4 ++-- build/azure-pipeline.pre-release.yml | 2 +- build/azure-pipeline.stable.yml | 2 +- build/ci/conda_env_1.yml | 2 +- pythonFiles/jedilsp_requirements/requirements.in | 2 +- pythonFiles/jedilsp_requirements/requirements.txt | 2 +- pythonFiles/tests/test_data/missing-deps.data | 2 +- pythonFiles/tests/test_data/no-missing-deps.data | 2 +- pythonFiles/tests/test_data/pyproject-missing-deps.data | 2 +- .../tests/test_data/pyproject-no-missing-deps.data | 2 +- requirements.txt | 2 +- scripts/onCreateCommand.sh | 8 ++++---- .../pythonEnvironments/creation/provider/condaUtils.ts | 2 +- .../common/envlayouts/pipenv/project1/CustomPipfileName | 2 +- .../common/envlayouts/pipenv/project2/Pipfile | 2 +- .../common/envlayouts/pipenv/project3/Pipfile | 2 +- .../common/envlayouts/workspace/folder1/Pipfile | 2 +- .../creation/installedPackagesDiagnostics.unit.test.ts | 4 ++-- 18 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 4c783fed1c42..ae7b8fddba69 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -22,10 +22,10 @@ runs: cache: 'npm' # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. - - name: Use Python 3.7 for JediLSP + - name: Use Python 3.8 for JediLSP uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.8 cache: 'pip' cache-dependency-path: | requirements.txt diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index d4fe0ac376fb..eed32b70c35d 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -38,7 +38,7 @@ extends: - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '3.8' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 05f83aa81824..c147f8b55164 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -33,7 +33,7 @@ extends: - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '3.8' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/build/ci/conda_env_1.yml b/build/ci/conda_env_1.yml index df5c917dcf4f..e9d08d0820a4 100644 --- a/build/ci/conda_env_1.yml +++ b/build/ci/conda_env_1.yml @@ -1,4 +1,4 @@ name: conda_env_1 dependencies: - - python=3.7 + - python=3.8 - pip diff --git a/pythonFiles/jedilsp_requirements/requirements.in b/pythonFiles/jedilsp_requirements/requirements.in index 7ad7ca14fa90..0167b757cf7b 100644 --- a/pythonFiles/jedilsp_requirements/requirements.in +++ b/pythonFiles/jedilsp_requirements/requirements.in @@ -1,6 +1,6 @@ # This file is used to generate requirements.txt. # To update requirements.txt, run the following commands. -# Use Python 3.7 when creating the environment or using pip-tools +# Use Python 3.8 when creating the environment or using pip-tools # 1) pip install pip-tools # 2) pip-compile --generate-hashes --upgrade pythonFiles\jedilsp_requirements\requirements.in diff --git a/pythonFiles/jedilsp_requirements/requirements.txt b/pythonFiles/jedilsp_requirements/requirements.txt index 90d10b467640..64b1bb958f67 100644 --- a/pythonFiles/jedilsp_requirements/requirements.txt +++ b/pythonFiles/jedilsp_requirements/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes 'pythonFiles\jedilsp_requirements\requirements.in' diff --git a/pythonFiles/tests/test_data/missing-deps.data b/pythonFiles/tests/test_data/missing-deps.data index c42d23c7dd67..c8c911f218a8 100644 --- a/pythonFiles/tests/test_data/missing-deps.data +++ b/pythonFiles/tests/test_data/missing-deps.data @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes --resolver=backtracking requirements-test.in diff --git a/pythonFiles/tests/test_data/no-missing-deps.data b/pythonFiles/tests/test_data/no-missing-deps.data index 5c2f1178bbdf..d5d04476dec0 100644 --- a/pythonFiles/tests/test_data/no-missing-deps.data +++ b/pythonFiles/tests/test_data/no-missing-deps.data @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes --resolver=backtracking requirements-test.in diff --git a/pythonFiles/tests/test_data/pyproject-missing-deps.data b/pythonFiles/tests/test_data/pyproject-missing-deps.data index f217a0bdade6..e4d6f9eb10d3 100644 --- a/pythonFiles/tests/test_data/pyproject-missing-deps.data +++ b/pythonFiles/tests/test_data/pyproject-missing-deps.data @@ -5,5 +5,5 @@ build-backend = "flit_core.buildapi" [project] name = "something" version = "2023.0.0" -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = ["pytest==7.3.1", "flake8-csv"] diff --git a/pythonFiles/tests/test_data/pyproject-no-missing-deps.data b/pythonFiles/tests/test_data/pyproject-no-missing-deps.data index 729bc9169e6f..64dadf6fdf2e 100644 --- a/pythonFiles/tests/test_data/pyproject-no-missing-deps.data +++ b/pythonFiles/tests/test_data/pyproject-no-missing-deps.data @@ -5,5 +5,5 @@ build-backend = "flit_core.buildapi" [project] name = "something" version = "2023.0.0" -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = [jedi-language-server"] diff --git a/requirements.txt b/requirements.txt index f2af0ca4204b..8ea311d28cb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes requirements.in diff --git a/scripts/onCreateCommand.sh b/scripts/onCreateCommand.sh index a90a5366417d..6303d21ef486 100644 --- a/scripts/onCreateCommand.sh +++ b/scripts/onCreateCommand.sh @@ -12,15 +12,15 @@ command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" source ~/.bashrc # Install Python via pyenv . -pyenv install 3.7:latest 3.8:latest 3.9:latest 3.10:latest 3.11:latest +pyenv install 3.8:latest 3.9:latest 3.10:latest 3.11:latest -# Set default Python version to 3.7 . -pyenv global 3.7.17 +# Set default Python version to 3.8 . +pyenv global 3.8.18 npm ci # Create Virutal environment. -pyenv exec python3.7 -m venv .venv +pyenv exec python3.8 -m venv .venv # Activate Virtual environment. source /workspaces/vscode-python/.venv/bin/activate diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts index e00a1c8dca09..8f14ffafcdaa 100644 --- a/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -39,7 +39,7 @@ export async function getCondaBaseEnv(): Promise { } export async function pickPythonVersion(token?: CancellationToken): Promise { - const items: QuickPickItem[] = ['3.10', '3.11', '3.9', '3.8', '3.7'].map((v) => ({ + const items: QuickPickItem[] = ['3.10', '3.11', '3.9', '3.8'].map((v) => ({ label: v === RECOMMENDED_CONDA_PYTHON ? `${Octicons.Star} Python` : 'Python', description: v, })); diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile b/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts index 10fe06bba442..4addb5687085 100644 --- a/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts +++ b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts @@ -62,7 +62,7 @@ function getPyProjectTomlFile(): typemoq.IMock { .setup((p) => p.getText(typemoq.It.isAny())) .returns( () => - '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[project]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[project]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', ); return someFile; } @@ -76,7 +76,7 @@ function getSomeTomlFile(): typemoq.IMock { .setup((p) => p.getText(typemoq.It.isAny())) .returns( () => - '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[something]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[something]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', ); return someFile; } From 7aa6660d58f66afa929169c7436a992fdaefe93f Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Sep 2023 11:30:16 -0700 Subject: [PATCH 264/336] Clear environment collection only after all async operations are done (#21975) For #20950 --- .../activation/terminalEnvVarCollectionService.ts | 6 ++++-- .../activation/terminalEnvVarCollectionService.unit.test.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 39bee239806b..75ef8168484b 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -127,9 +127,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); - // Clear any previously set env vars from collection - envVarCollection.clear(); if (!settings.terminal.activateEnvironment) { + envVarCollection.clear(); traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); return; } @@ -150,6 +149,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ return; } await this.trackTerminalPrompt(shell, resource, env); + envVarCollection.clear(); this.processEnvVars = undefined; return; } @@ -164,6 +164,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. env.PS1 = await this.getPS1(shell, resource, env); + // Clear any previously set env vars from collection + envVarCollection.clear(); Object.keys(env).forEach((key) => { if (shouldSkip(key)) { return; diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 1513be676ee4..cedc2701112f 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -570,7 +570,7 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.clear()).twice(); + verify(collection.clear()).once(); }); test('If no activated variables are returned for default shell, clear collection', async () => { From df0b493b0a342ae03bfa377025578f450575fab2 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 12 Sep 2023 13:26:10 -0700 Subject: [PATCH 265/336] handle subprocess segfaults for testAdapters (#21963) closes: https://github.com/microsoft/vscode-python/issues/21662 Not only does this make sure segfaults are correct for unittest but also for pytest. --- .../testing/testController/common/server.ts | 18 ++- .../testing/testController/common/types.ts | 2 + .../testing/testController/common/utils.ts | 26 +++- .../pytest/pytestExecutionAdapter.ts | 13 +- .../unittest/testDiscoveryAdapter.ts | 2 +- .../unittest/testExecutionAdapter.ts | 2 +- .../testing/common/testingAdapter.test.ts | 130 ++++++++++++++++-- .../errorWorkspace/test_seg_fault.py | 18 +++ .../test_parameterized_subtest.py | 2 +- 9 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 8797a861fb4a..d0a225ae5712 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -15,7 +15,7 @@ import { traceError, traceInfo, traceLog } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; -import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; +import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER, createExecutionErrorPayload } from './utils'; import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { @@ -133,6 +133,10 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDiscoveryDataReceived.event; } + public triggerRunDataReceivedEvent(payload: DataReceivedEvent): void { + this._onRunDataReceived.fire(payload); + } + public dispose(): void { this.server.close(); this._onDataReceived.dispose(); @@ -146,6 +150,7 @@ export class PythonTestServer implements ITestServer, Disposable { options: TestCommandOptions, runTestIdPort?: string, runInstance?: TestRun, + testIds?: string[], callback?: () => void, ): Promise { const { uuid } = options; @@ -218,8 +223,15 @@ export class PythonTestServer implements ITestServer, Disposable { result?.proc?.stderr?.on('data', (data) => { spawnOptions?.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', () => { - traceLog('Exec server closed.', uuid); + result?.proc?.on('exit', (code, signal) => { + // if the child has testIds then this is a run request + if (code !== 0 && testIds) { + // if the child process exited with a non-zero exit code, then we need to send the error payload. + this._onRunDataReceived.fire({ + uuid, + data: JSON.stringify(createExecutionErrorPayload(code, signal, testIds, options.cwd)), + }); + } deferred.resolve({ stdout: '', stderr: '' }); callback?.(); }); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 16c0bd0e3cee..610c3d76d17b 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -178,12 +178,14 @@ export interface ITestServer { options: TestCommandOptions, runTestIdsPort?: string, runInstance?: TestRun, + testIds?: string[], callback?: () => void, ): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; deleteUUID(uuid: string): void; + triggerRunDataReceivedEvent(data: DataReceivedEvent): void; } export interface ITestResultResolver { runIdToVSid: Map; diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index f98550d3e72b..dd1b51551a45 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -9,7 +9,7 @@ import { EnableTestAdapterRewrite } from '../../../common/experiments/groups'; import { IExperimentService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities'; -import { DiscoveredTestItem, DiscoveredTestNode, ITestResultResolver } from './types'; +import { DiscoveredTestItem, DiscoveredTestNode, ExecutionTestPayload, ITestResultResolver } from './types'; export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); @@ -188,3 +188,27 @@ export function populateTestTree( function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem { return test.type_ === 'test'; } + +export function createExecutionErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + testIds: string[], + cwd: string, +): ExecutionTestPayload { + const etp: ExecutionTestPayload = { + cwd, + status: 'error', + error: 'Test run failed, the python test process was terminated before it could exit on its own.', + result: {}, + }; + // add error result for each attempted test. + for (let i = 0; i < testIds.length; i = i + 1) { + const test = testIds[i]; + etp.result![test] = { + test, + outcome: 'error', + message: ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal}`, + }; + } + return etp; +} diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 96d53db22c1c..9705374d74af 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -5,7 +5,7 @@ import { TestRun, Uri } from 'vscode'; import * as path from 'path'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred } from '../../../common/utils/async'; -import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, @@ -130,7 +130,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } - traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); if (spawnOptions.extraVariables) @@ -175,7 +174,15 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { this.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', () => { + result?.proc?.on('exit', (code, signal) => { + // if the child has testIds then this is a run request + if (code !== 0 && testIds) { + // if the child process exited with a non-zero exit code, then we need to send the error payload. + this.testServer.triggerRunDataReceivedEvent({ + uuid, + data: JSON.stringify(utils.createExecutionErrorPayload(code, signal, testIds, cwd)), + }); + } deferredExec.resolve({ stdout: '', stderr: '' }); deferred.resolve(); disposeDataReceiver?.(this.testServer); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 1cbad7ef65ef..9820aa89626c 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -64,7 +64,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { - await this.testServer.sendCommand(options, undefined, undefined, callback); + await this.testServer.sendCommand(options, undefined, undefined, [], callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 9af9e593c246..bc5e41d19d9d 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -83,7 +83,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, () => { + await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, testIds, () => { deferred.resolve(); disposeDataReceiver?.(this.testServer); }); diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 1334085e4cea..5af7e59f6a46 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -13,7 +13,7 @@ import { ITestDebugLauncher } from '../../../client/testing/common/types'; import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; -import { traceError, traceLog } from '../../../client/logging'; +import { traceLog } from '../../../client/logging'; import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; @@ -39,6 +39,12 @@ suite('End to End Tests: test adapters', () => { 'testTestingRootWkspc', 'largeWorkspace', ); + const rootPathErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'errorWorkspace', + ); suiteSetup(async () => { serviceContainer = (await initialize()).serviceContainer; }); @@ -86,10 +92,21 @@ suite('End to End Tests: test adapters', () => { await discoveryAdapter.discoverTests(workspaceUri).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); + // resultResolver.verify( + // (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), + // typeMoq.Times.once(), + // ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + }); + + await discoveryAdapter.discoverTests(Uri.parse(rootPathErrorWorkspace)).finally(() => { + // verification after discovery is complete // 1. Check the status is "success" assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); @@ -191,7 +208,6 @@ suite('End to End Tests: test adapters', () => { resultResolver .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { - traceLog(`resolveDiscovery ${data}`); actualData = data; return Promise.resolve(); }); @@ -231,7 +247,6 @@ suite('End to End Tests: test adapters', () => { resultResolver .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { - traceLog(`resolveExecution ${data}`); actualData = data; return Promise.resolve(); }); @@ -263,7 +278,6 @@ suite('End to End Tests: test adapters', () => { (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), typeMoq.Times.once(), ); - // 1. Check the status is "success" assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); // 2. Confirm tests are found @@ -275,7 +289,6 @@ suite('End to End Tests: test adapters', () => { resultResolver .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { - traceError(`resolveExecution ${data}`); traceLog(`resolveExecution ${data}`); // do the following asserts for each time resolveExecution is called, should be called once per test. // 1. Check the status, can be subtest success or failure @@ -328,7 +341,6 @@ suite('End to End Tests: test adapters', () => { resultResolver .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { - traceLog(`resolveExecution ${data}`); actualData = data; return Promise.resolve(); }); @@ -366,7 +378,6 @@ suite('End to End Tests: test adapters', () => { (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), typeMoq.Times.once(), ); - // 1. Check the status is "success" assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); // 2. Confirm no errors @@ -379,7 +390,6 @@ suite('End to End Tests: test adapters', () => { resultResolver .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { - traceLog(`resolveExecution ${data}`); // do the following asserts for each time resolveExecution is called, should be called once per test. // 1. Check the status is "success" assert.strictEqual(data.status, 'success', "Expected status to be 'success'"); @@ -424,4 +434,100 @@ suite('End to End Tests: test adapters', () => { ); }); }); + test('unittest execution adapter seg fault error handling', async () => { + const testId = `test_seg_fault.TestSegmentationFault.test_segfault`; + const testIds: string[] = [testId]; + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + // 1. Check the status is "success" + assert.strictEqual(data.status, 'error', "Expected status to be 'error'"); + // 2. Confirm no errors + assert.ok(data.error, "Expected errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(data.result, 'Expected results to be present'); + // 4. make sure the testID is found in the results + assert.notDeepEqual( + JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'), + -1, + 'Expected testId to be present', + ); + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + + // run pytest execution + const executionAdapter = new UnittestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object).finally(() => { + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.exactly(1), + ); + }); + }); + test('pytest execution adapter seg fault error handling', async () => { + const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; + const testIds: string[] = [testId]; + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + // 1. Check the status is "success" + assert.strictEqual(data.status, 'error', "Expected status to be 'error'"); + // 2. Confirm no errors + assert.ok(data.error, "Expected errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(data.result, 'Expected results to be present'); + // 4. make sure the testID is found in the results + assert.notDeepEqual( + JSON.stringify(data).search('test_seg_fault.py::TestSegmentationFault::test_segfault'), + -1, + 'Expected testId to be present', + ); + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.exactly(1), + ); + }); + }); }); diff --git a/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py new file mode 100644 index 000000000000..bad7ff8fcbbd --- /dev/null +++ b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +import ctypes + + +class TestSegmentationFault(unittest.TestCase): + def cause_segfault(self): + ctypes.string_at(0) # Dereference a NULL pointer + + def test_segfault(self): + assert True + self.cause_segfault() + + +if __name__ == "__main__": + unittest.main() diff --git a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py index 3e84df0a2d9f..8c6a29adf495 100644 --- a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py +++ b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py @@ -6,7 +6,7 @@ @pytest.mark.parametrize("num", range(0, 200)) def test_odd_even(num): - return num % 2 == 0 + assert num % 2 == 0 class NumbersTest(unittest.TestCase): From 2268d5382b31b5500286f02bff2f12590b069369 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 12 Sep 2023 15:57:11 -0700 Subject: [PATCH 266/336] Add support to delete and re-create .conda environments (#21977) Fix https://github.com/microsoft/vscode-python/issues/21828 --- src/client/common/utils/localize.ts | 16 +++- .../creation/common/commonUtils.ts | 8 ++ .../provider/condaCreationProvider.ts | 81 ++++++++++++---- .../creation/provider/condaDeleteUtils.ts | 37 ++++++++ .../creation/provider/condaUtils.ts | 93 ++++++++++++++++++- .../condaCreationProvider.unit.test.ts | 8 ++ .../provider/condaDeleteUtils.unit.test.ts | 71 ++++++++++++++ .../creation/provider/condaUtils.unit.test.ts | 69 +++++++++++++- 8 files changed, 354 insertions(+), 29 deletions(-) create mode 100644 src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts create mode 100644 src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 4cda15e15ec0..7ec517f82277 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -464,8 +464,10 @@ export namespace CreateEnv { export const error = l10n.t('Creating virtual environment failed with error.'); export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml'); export const requirementsQuickPickTitle = l10n.t('Select dependencies to install'); - export const recreate = l10n.t('Recreate'); - export const recreateDescription = l10n.t('Delete existing ".venv" environment and create a new one'); + export const recreate = l10n.t('Delete and Recreate'); + export const recreateDescription = l10n.t( + 'Delete existing ".venv" directory and create a new ".venv" environment', + ); export const useExisting = l10n.t('Use Existing'); export const useExistingDescription = l10n.t('Use existing ".venv" environment with no changes to it'); export const existingVenvQuickPickPlaceholder = l10n.t( @@ -485,6 +487,16 @@ export namespace CreateEnv { ); export const creating = l10n.t('Creating conda environment...'); export const providerDescription = l10n.t('Creates a `.conda` Conda environment in the current workspace'); + + export const recreate = l10n.t('Delete and Recreate'); + export const recreateDescription = l10n.t('Delete existing ".conda" environment and create a new one'); + export const useExisting = l10n.t('Use Existing'); + export const useExistingDescription = l10n.t('Use existing ".conda" environment with no changes to it'); + export const existingCondaQuickPickPlaceholder = l10n.t( + 'Choose an option to handle the existing ".conda" environment', + ); + export const deletingEnvironmentProgress = l10n.t('Deleting existing ".conda" environment...'); + export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".conda" environment.'); } } diff --git a/src/client/pythonEnvironments/creation/common/commonUtils.ts b/src/client/pythonEnvironments/creation/common/commonUtils.ts index b4d4a37eae9b..16d8015e3f26 100644 --- a/src/client/pythonEnvironments/creation/common/commonUtils.ts +++ b/src/client/pythonEnvironments/creation/common/commonUtils.ts @@ -32,3 +32,11 @@ export function getVenvExecutable(workspaceFolder: WorkspaceFolder): string { } return path.join(getVenvPath(workspaceFolder), 'bin', 'python'); } + +export function getPrefixCondaEnvPath(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, '.conda'); +} + +export async function hasPrefixCondaEnv(workspaceFolder: WorkspaceFolder): Promise { + return fs.pathExists(getPrefixCondaEnvPath(workspaceFolder)); +} diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 7ca44c1b7eff..86e0b56801cd 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -9,11 +9,18 @@ import { CreateEnvironmentProgress } from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; -import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; +import { getOSType, OSType } from '../../../common/utils/platform'; import { createCondaScript } from '../../../common/process/internal/scripts'; import { Common, CreateEnv } from '../../../common/utils/localize'; -import { getCondaBaseEnv, pickPythonVersion } from './condaUtils'; -import { showErrorMessageWithLogs } from '../common/commonUtils'; +import { + ExistingCondaAction, + deleteEnvironment, + getCondaBaseEnv, + getPathEnvVariableForConda, + pickExistingCondaAction, + pickPythonVersion, +} from './condaUtils'; +import { getPrefixCondaEnvPath, showErrorMessageWithLogs } from '../common/commonUtils'; import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vscodeApis/windowApis'; import { EventName } from '../../../telemetry/constants'; import { sendTelemetryEvent } from '../../../telemetry'; @@ -83,22 +90,7 @@ async function createCondaEnv( }); const deferred = createDeferred(); - let pathEnv = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path') || ''; - if (getOSType() === OSType.Windows) { - // On windows `conda.bat` is used, which adds the following bin directories to PATH - // then launches `conda.exe` which is a stub to `python.exe -m conda`. Here, we are - // instead using the `python.exe` that ships with conda to run a python script that - // handles conda env creation and package installation. - // See conda issue: https://github.com/conda/conda/issues/11399 - const root = path.dirname(command); - const libPath1 = path.join(root, 'Library', 'bin'); - const libPath2 = path.join(root, 'Library', 'mingw-w64', 'bin'); - const libPath3 = path.join(root, 'Library', 'usr', 'bin'); - const libPath4 = path.join(root, 'bin'); - const libPath5 = path.join(root, 'Scripts'); - const libPath = [libPath1, libPath2, libPath3, libPath4, libPath5].join(path.delimiter); - pathEnv = `${libPath}${path.delimiter}${pathEnv}`; - } + const pathEnv = getPathEnvVariableForConda(command); traceLog('Running Conda Env creation script: ', [command, ...args]); const { proc, out, dispose } = execObservable(command, args, { mergeStdOutErr: true, @@ -182,6 +174,29 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { + if (workspace && context === MultiStepAction.Continue) { + try { + existingCondaAction = await pickExistingCondaAction(workspace); + return MultiStepAction.Continue; + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + return MultiStepAction.Continue; + }, + undefined, + ); + workspaceStep.next = existingEnvStep; + let version: string | undefined; const versionStep = new MultiStepNode( workspaceStep, @@ -204,13 +219,39 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspace); + const command = interpreter; + const args = ['-m', 'conda', 'env', 'remove', '--prefix', condaEnvPath, '--yes']; + try { + traceInfo(`Deleting conda environment: ${condaEnvPath}`); + traceInfo(`Running command: ${command} ${args.join(' ')}`); + const result = await plainExec(command, args, { mergeStdOutErr: true }, { ...process.env, PATH: pathEnvVar }); + traceInfo(result.stdout); + if (await hasPrefixCondaEnv(workspace)) { + // If conda cannot delete files it will name the files as .conda_trash. + // These need to be deleted manually. + traceError(`Conda environment ${condaEnvPath} could not be deleted.`); + traceError(`Please delete the environment manually: ${condaEnvPath}`); + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + return false; + } + } catch (err) { + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + traceError(`Deleting conda environment ${condaEnvPath} Failed with error: `, err); + return false; + } + return true; +} diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts index 8f14ffafcdaa..51c55e414245 100644 --- a/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -1,14 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, QuickPickItem, Uri } from 'vscode'; -import { Common } from '../../../browser/localize'; -import { Octicons } from '../../../common/constants'; -import { CreateEnv } from '../../../common/utils/localize'; +import * as path from 'path'; +import { CancellationToken, ProgressLocation, QuickPickItem, Uri, WorkspaceFolder } from 'vscode'; +import { Commands, Octicons } from '../../../common/constants'; +import { Common, CreateEnv } from '../../../common/utils/localize'; import { executeCommand } from '../../../common/vscodeApis/commandApis'; -import { showErrorMessage, showQuickPickWithBack } from '../../../common/vscodeApis/windowApis'; +import { + MultiStepAction, + showErrorMessage, + showQuickPickWithBack, + withProgress, +} from '../../../common/vscodeApis/windowApis'; import { traceLog } from '../../../logging'; import { Conda } from '../../common/environmentManagers/conda'; +import { getPrefixCondaEnvPath, hasPrefixCondaEnv } from '../common/commonUtils'; +import { OSType, getEnvironmentVariable, getOSType } from '../../../common/utils/platform'; +import { deleteCondaEnvironment } from './condaDeleteUtils'; const RECOMMENDED_CONDA_PYTHON = '3.10'; @@ -59,3 +67,78 @@ export async function pickPythonVersion(token?: CancellationToken): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspaceFolder); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.Conda.deletingEnvironmentProgress} ([${Common.showLogs}](command:${Commands.ViewOutput})): ${condaEnvPath}`, + cancellable: false, + }, + async () => deleteCondaEnvironment(workspaceFolder, interpreter, getPathEnvVariableForConda(interpreter)), + ); +} + +export enum ExistingCondaAction { + Recreate, + UseExisting, + Create, +} + +export async function pickExistingCondaAction( + workspaceFolder: WorkspaceFolder | undefined, +): Promise { + if (workspaceFolder) { + if (await hasPrefixCondaEnv(workspaceFolder)) { + const items: QuickPickItem[] = [ + { label: CreateEnv.Conda.recreate, description: CreateEnv.Conda.recreateDescription }, + { + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }, + ]; + + const selection = (await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Conda.existingCondaQuickPickPlaceholder, + ignoreFocusOut: true, + }, + undefined, + )) as QuickPickItem | undefined; + + if (selection?.label === CreateEnv.Conda.recreate) { + return ExistingCondaAction.Recreate; + } + + if (selection?.label === CreateEnv.Conda.useExisting) { + return ExistingCondaAction.UseExisting; + } + } else { + return ExistingCondaAction.Create; + } + } + + throw MultiStepAction.Cancel; +} diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index e1ac1bafe6ac..3195d1f88ea9 100644 --- a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -35,6 +35,7 @@ suite('Conda Creation provider tests', () => { let execObservableStub: sinon.SinonStub; let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; + let pickExistingCondaActionStub: sinon.SinonStub; setup(() => { pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); @@ -46,6 +47,9 @@ suite('Conda Creation provider tests', () => { showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); showErrorMessageWithLogsStub.resolves(); + pickExistingCondaActionStub = sinon.stub(condaUtils, 'pickExistingCondaAction'); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.Create); + progressMock = typemoq.Mock.ofType(); condaProvider = condaCreationProvider(); }); @@ -77,6 +81,7 @@ suite('Conda Creation provider tests', () => { pickPythonVersionStub.resolves(undefined); await assert.isRejected(condaProvider.createEnvironment()); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment', async () => { @@ -136,6 +141,7 @@ suite('Conda Creation provider tests', () => { workspaceFolder: workspace1, }); assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment failed', async () => { @@ -188,6 +194,7 @@ suite('Conda Creation provider tests', () => { const result = await promise; assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment failed (non-zero exit code)', async () => { @@ -245,5 +252,6 @@ suite('Conda Creation provider tests', () => { const result = await promise; assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); }); diff --git a/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..b1acd0678714 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { deleteCondaEnvironment } from '../../../../client/pythonEnvironments/creation/provider/condaDeleteUtils'; + +suite('Conda Delete test', () => { + let plainExecStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete conda env ', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isTrue(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete conda env with error', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(true); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete conda env with exception', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.rejects(new Error('error')); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts index 3f115f9f58ed..a3f4a1abe905 100644 --- a/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts @@ -3,9 +3,17 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; -import { CancellationTokenSource } from 'vscode'; +import * as path from 'path'; +import { CancellationTokenSource, Uri } from 'vscode'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; -import { pickPythonVersion } from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import { + ExistingCondaAction, + pickExistingCondaAction, + pickPythonVersion, +} from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { CreateEnv } from '../../../../client/common/utils/localize'; suite('Conda Utils test', () => { let showQuickPickWithBackStub: sinon.SinonStub; @@ -43,3 +51,60 @@ suite('Conda Utils test', () => { assert.isUndefined(actual); }); }); + +suite('Existing .conda env test', () => { + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No .conda found', async () => { + hasPrefixCondaEnvStub.resolves(false); + showQuickPickWithBackStub.resolves(undefined); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Create); + assert.isTrue(showQuickPickWithBackStub.notCalled); + }); + + test('User presses escape', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves(undefined); + await assert.isRejected(pickExistingCondaAction(workspace1)); + }); + + test('.conda found and user selected to re-create', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.recreate, + description: CreateEnv.Conda.recreateDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Recreate); + }); + + test('.conda found and user selected to re-use', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.UseExisting); + }); +}); From 9ebc5eb3a26f16cb963935ff01d86b03e1f34e7e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Sep 2023 18:01:29 -0700 Subject: [PATCH 267/336] Fix `${command:python.interpreterPath}` in tasks.json in multiroot workspaces (#21980) Closes https://github.com/microsoft/vscode-python/issues/21915 --- .../launch.json/interpreterPathCommand.ts | 2 +- .../interpreterPathCommand.unit.test.ts | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts b/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts index e4c14de407c9..21c8d0f1147b 100644 --- a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts +++ b/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts @@ -41,7 +41,7 @@ export class InterpreterPathCommand implements IExtensionSingleActivationService let workspaceFolderUri; try { - workspaceFolderUri = workspaceFolder ? Uri.parse(workspaceFolder) : undefined; + workspaceFolderUri = workspaceFolder ? Uri.file(workspaceFolder) : undefined; } catch (ex) { workspaceFolderUri = undefined; } diff --git a/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts index c773e1cbd5bc..77077ad945fb 100644 --- a/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts +++ b/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts @@ -45,7 +45,7 @@ suite('Interpreter Path Command', () => { test('If `workspaceFolder` property exists in `args`, it is used to retrieve setting from config', async () => { const args = { workspaceFolder: 'folderPath' }; when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, Uri.parse('folderPath')); + assert.deepEqual(arg, Uri.file('folderPath')); return Promise.resolve({ path: 'settingValue' }) as unknown; }); @@ -56,7 +56,7 @@ suite('Interpreter Path Command', () => { test('If `args[1]` is defined, it is used to retrieve setting from config', async () => { const args = ['command', 'folderPath']; when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, Uri.parse('folderPath')); + assert.deepEqual(arg, Uri.file('folderPath')); return Promise.resolve({ path: 'settingValue' }) as unknown; }); @@ -73,14 +73,4 @@ suite('Interpreter Path Command', () => { const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); expect(setting).to.equal('settingValue'); }); - - test('If `args[1]` is not a valid uri', async () => { - const args = ['command', '${input:some_input}']; - when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, undefined); - return Promise.resolve({ path: 'settingValue' }) as unknown; - }); - const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); - expect(setting).to.equal('settingValue'); - }); }); From 221b769c084462952a724e42e314f26506f76688 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 13 Sep 2023 12:01:12 -0700 Subject: [PATCH 268/336] Open requirement files (#21917) Closes https://github.com/microsoft/vscode-python/issues/21984 ![image](https://github.com/microsoft/vscode-python/assets/3840081/a5cc4991-7d65-4980-b35e-6453a85f516d) --- src/client/common/utils/localize.ts | 1 + src/client/common/vscodeApis/windowApis.ts | 12 ++++ .../creation/provider/venvUtils.ts | 40 ++++++++--- .../creation/provider/venvUtils.unit.test.ts | 71 +++++++++++++++++-- 4 files changed, 112 insertions(+), 12 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 7ec517f82277..05b525bdf5bf 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -475,6 +475,7 @@ export namespace CreateEnv { ); export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...'); export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.'); + export const openRequirementsFile = l10n.t('Open requirements file'); } export namespace Conda { diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts index 1c242314cb87..c761ff60fa65 100644 --- a/src/client/common/vscodeApis/windowApis.ts +++ b/src/client/common/vscodeApis/windowApis.ts @@ -16,9 +16,15 @@ import { TextEditor, window, Disposable, + QuickPickItemButtonEvent, + Uri, } from 'vscode'; import { createDeferred, Deferred } from '../utils/async'; +export function showTextDocument(uri: Uri): Thenable { + return window.showTextDocument(uri); +} + export function showQuickPick( items: readonly T[] | Thenable, options?: QuickPickOptions, @@ -91,6 +97,7 @@ export async function showQuickPickWithBack( items: readonly T[], options?: QuickPickOptions, token?: CancellationToken, + itemButtonHandler?: (e: QuickPickItemButtonEvent) => void, ): Promise { const quickPick: QuickPick = window.createQuickPick(); const disposables: Disposable[] = [quickPick]; @@ -130,6 +137,11 @@ export async function showQuickPickWithBack( deferred.resolve(undefined); } }), + quickPick.onDidTriggerItemButton((e) => { + if (itemButtonHandler) { + itemButtonHandler(e); + } + }), ); if (token) { disposables.push( diff --git a/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/src/client/pythonEnvironments/creation/provider/venvUtils.ts index d7a0be170f99..723337b2a7fa 100644 --- a/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -5,12 +5,22 @@ import * as tomljs from '@iarna/toml'; import * as fs from 'fs-extra'; import { flatten, isArray } from 'lodash'; import * as path from 'path'; -import { CancellationToken, ProgressLocation, QuickPickItem, RelativePattern, WorkspaceFolder } from 'vscode'; +import { + CancellationToken, + ProgressLocation, + QuickPickItem, + QuickPickItemButtonEvent, + RelativePattern, + ThemeIcon, + Uri, + WorkspaceFolder, +} from 'vscode'; import { Common, CreateEnv } from '../../../common/utils/localize'; import { MultiStepAction, MultiStepNode, showQuickPickWithBack, + showTextDocument, withProgress, } from '../../../common/vscodeApis/windowApis'; import { findFiles } from '../../../common/vscodeApis/workspaceApis'; @@ -20,6 +30,10 @@ import { isWindows } from '../../../common/platform/platformService'; import { getVenvPath, hasVenv } from '../common/commonUtils'; import { deleteEnvironmentNonWindows, deleteEnvironmentWindows } from './venvDeleteUtils'; +export const OPEN_REQUIREMENTS_BUTTON = { + iconPath: new ThemeIcon('go-to-file'), + tooltip: CreateEnv.Venv.openRequirementsFile, +}; const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; async function getPipRequirementsFiles( workspaceFolder: WorkspaceFolder, @@ -78,8 +92,13 @@ async function pickTomlExtras(extras: string[], token?: CancellationToken): Prom return undefined; } -async function pickRequirementsFiles(files: string[], token?: CancellationToken): Promise { +async function pickRequirementsFiles( + files: string[], + root: string, + token?: CancellationToken, +): Promise { const items: QuickPickItem[] = files + .map((p) => path.relative(root, p)) .sort((a, b) => { const al: number = a.split(/[\\\/]/).length; const bl: number = b.split(/[\\\/]/).length; @@ -91,7 +110,10 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken) } return al - bl; }) - .map((e) => ({ label: e })); + .map((e) => ({ + label: e, + buttons: [OPEN_REQUIREMENTS_BUTTON], + })); const selection = await showQuickPickWithBack( items, @@ -101,6 +123,11 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken) canPickMany: true, }, token, + async (e: QuickPickItemButtonEvent) => { + if (e.item.label) { + await showTextDocument(Uri.file(path.join(root, e.item.label))); + } + }, ); if (selection && isArray(selection)) { @@ -195,14 +222,11 @@ export async function pickPackagesToInstall( tomlStep, async (context?: MultiStepAction) => { traceVerbose('Looking for pip requirements.'); - const requirementFiles = (await getPipRequirementsFiles(workspaceFolder, token))?.map((p) => - path.relative(workspaceFolder.uri.fsPath, p), - ); - + const requirementFiles = await getPipRequirementsFiles(workspaceFolder, token); if (requirementFiles && requirementFiles.length > 0) { traceVerbose('Found pip requirements.'); try { - const result = await pickRequirementsFiles(requirementFiles, token); + const result = await pickRequirementsFiles(requirementFiles, workspaceFolder.uri.fsPath, token); const installList = result?.map((p) => path.join(workspaceFolder.uri.fsPath, p)); if (installList) { installList.forEach((i) => { diff --git a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts index ae4f43a0296c..1671026d5dd4 100644 --- a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts @@ -10,11 +10,13 @@ import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; import { ExistingVenvAction, + OPEN_REQUIREMENTS_BUTTON, pickExistingVenvAction, pickPackagesToInstall, } from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import { CreateEnv } from '../../../../client/common/utils/localize'; +import { createDeferred } from '../../../../client/common/utils/async'; chaiUse(chaiAsPromised); @@ -23,6 +25,7 @@ suite('Venv Utils test', () => { let showQuickPickWithBackStub: sinon.SinonStub; let pathExistsStub: sinon.SinonStub; let readFileStub: sinon.SinonStub; + let showTextDocumentStub: sinon.SinonStub; const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), @@ -35,6 +38,7 @@ suite('Venv Utils test', () => { showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); pathExistsStub = sinon.stub(fs, 'pathExists'); readFileStub = sinon.stub(fs, 'readFile'); + showTextDocumentStub = sinon.stub(windowApis, 'showTextDocument'); }); teardown(() => { @@ -224,13 +228,18 @@ suite('Venv Utils test', () => { await assert.isRejected(pickPackagesToInstall(workspace1)); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.isTrue(readFileStub.calledOnce); @@ -257,13 +266,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, []); @@ -290,13 +304,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, [ @@ -328,13 +347,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, [ @@ -349,6 +373,45 @@ suite('Venv Utils test', () => { ]); assert.isTrue(readFileStub.notCalled); }); + + test('User clicks button to open requirements.txt', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + const deferred = createDeferred(); + showQuickPickWithBackStub.callsFake(async (_items, _options, _token, callback) => { + callback({ + button: OPEN_REQUIREMENTS_BUTTON, + item: { label: 'requirements.txt' }, + }); + await deferred.promise; + return [{ label: 'requirements.txt' }]; + }); + + let uri: Uri | undefined; + showTextDocumentStub.callsFake((arg: Uri) => { + uri = arg; + deferred.resolve(); + return Promise.resolve(); + }); + + await pickPackagesToInstall(workspace1); + assert.deepStrictEqual( + uri?.toString(), + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')).toString(), + ); + }); }); suite('Test pick existing venv action', () => { From 1040f3c842798fd0efb1c51aafd31395179ac153 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 13 Sep 2023 23:32:54 -0700 Subject: [PATCH 269/336] Use stdin if workspace has large number of requirements (#21988) Closes https://github.com/microsoft/vscode-python/issues/21480 --- pythonFiles/create_venv.py | 24 ++- pythonFiles/tests/test_create_venv.py | 51 ++++++ .../creation/provider/venvCreationProvider.ts | 37 +++-- .../venvCreationProvider.unit.test.ts | 155 +++++++++++++++++- 4 files changed, 253 insertions(+), 14 deletions(-) diff --git a/pythonFiles/create_venv.py b/pythonFiles/create_venv.py index cac084fd2222..68f21a38b980 100644 --- a/pythonFiles/create_venv.py +++ b/pythonFiles/create_venv.py @@ -3,6 +3,7 @@ import argparse import importlib.util as import_util +import json import os import pathlib import subprocess @@ -56,6 +57,12 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: metavar="NAME", action="store", ) + parser.add_argument( + "--stdin", + action="store_true", + default=False, + help="Read arguments from stdin.", + ) return parser.parse_args(argv) @@ -152,6 +159,16 @@ def install_pip(name: str): ) +def get_requirements_from_args(args: argparse.Namespace) -> List[str]: + requirements = [] + if args.stdin: + data = json.loads(sys.stdin.read()) + requirements = data.get("requirements", []) + if args.requirements: + requirements.extend(args.requirements) + return requirements + + def main(argv: Optional[Sequence[str]] = None) -> None: if argv is None: argv = [] @@ -223,9 +240,10 @@ def main(argv: Optional[Sequence[str]] = None) -> None: print(f"VENV_INSTALLING_PYPROJECT: {args.toml}") install_toml(venv_path, args.extras) - if args.requirements: - print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}") - install_requirements(venv_path, args.requirements) + requirements = get_requirements_from_args(args) + if requirements: + print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}") + install_requirements(venv_path, requirements) if __name__ == "__main__": diff --git a/pythonFiles/tests/test_create_venv.py b/pythonFiles/tests/test_create_venv.py index ae3f18be6f3c..772fc02708f8 100644 --- a/pythonFiles/tests/test_create_venv.py +++ b/pythonFiles/tests/test_create_venv.py @@ -1,7 +1,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import argparse +import contextlib import importlib +import io +import json import os import sys @@ -224,3 +228,50 @@ def run_process(args, error_message): create_venv.run_process = run_process create_venv.main([]) + + +@contextlib.contextmanager +def redirect_io(stream: str, new_stream): + """Redirect stdio streams to a custom stream.""" + old_stream = getattr(sys, stream) + setattr(sys, stream, new_stream) + yield + setattr(sys, stream, old_stream) + + +class CustomIO(io.TextIOWrapper): + """Custom stream object to replace stdio.""" + + name: str = "customio" + + def __init__(self, name: str, encoding="utf-8", newline=None): + self._buffer = io.BytesIO() + self._buffer.name = name + super().__init__(self._buffer, encoding=encoding, newline=newline) + + def close(self): + """Provide this close method which is used by some tools.""" + # This is intentionally empty. + + def get_value(self) -> str: + """Returns value from the buffer as string.""" + self.seek(0) + return self.read() + + +def test_requirements_from_stdin(): + importlib.reload(create_venv) + + cli_requirements = [f"cli-requirement{i}.txt" for i in range(3)] + args = argparse.Namespace() + args.__dict__.update({"stdin": True, "requirements": cli_requirements}) + + stdin_requirements = [f"stdin-requirement{i}.txt" for i in range(20)] + text = json.dumps({"requirements": stdin_requirements}) + str_input = CustomIO("", encoding="utf-8", newline="\n") + with redirect_io("stdin", str_input): + str_input.write(text) + str_input.seek(0) + actual = create_venv.get_requirements_from_args(args) + + assert actual == stdin_requirements + cli_requirements diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index edbdcd7d84a6..61850e404c3d 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -32,8 +32,14 @@ import { CreateEnvironmentResult, } from '../proposed.createEnvApis'; -function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): string[] { +interface IVenvCommandArgs { + argv: string[]; + stdin: string | undefined; +} + +function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): IVenvCommandArgs { const command: string[] = [createVenvScript()]; + let stdin: string | undefined; if (addGitIgnore) { command.push('--git-ignore'); @@ -52,14 +58,21 @@ function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgn }); const requirements = installInfo.filter((i) => i.installType === 'requirements').map((i) => i.installItem); - requirements.forEach((r) => { - if (r) { - command.push('--requirements', r); - } - }); + + if (requirements.length < 10) { + requirements.forEach((r) => { + if (r) { + command.push('--requirements', r); + } + }); + } else { + command.push('--stdin'); + // Too many requirements can cause the command line to be too long error. + stdin = JSON.stringify({ requirements }); + } } - return command; + return { argv: command, stdin }; } function getVenvFromOutput(output: string): string | undefined { @@ -81,7 +94,7 @@ function getVenvFromOutput(output: string): string | undefined { async function createVenv( workspace: WorkspaceFolder, command: string, - args: string[], + args: IVenvCommandArgs, progress: CreateEnvironmentProgress, token?: CancellationToken, ): Promise { @@ -94,11 +107,15 @@ async function createVenv( }); const deferred = createDeferred(); - traceLog('Running Env creation script: ', [command, ...args]); - const { proc, out, dispose } = execObservable(command, args, { + traceLog('Running Env creation script: ', [command, ...args.argv]); + if (args.stdin) { + traceLog('Requirements passed in via stdin: ', args.stdin); + } + const { proc, out, dispose } = execObservable(command, args.argv, { mergeStdOutErr: true, token, cwd: workspace.uri.fsPath, + stdinStr: args.stdin, }); const progressAndTelemetry = new VenvProgressAndTelemetry(progress); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index de65887b7edc..72914b9118e2 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -15,7 +15,7 @@ import * as rawProcessApis from '../../../../client/common/process/rawProcessApi import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import { createDeferred } from '../../../../client/common/utils/async'; -import { Output } from '../../../../client/common/process/types'; +import { Output, SpawnOptions } from '../../../../client/common/process/types'; import { VENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; import { CreateEnv } from '../../../../client/common/utils/localize'; import * as venvUtils from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; @@ -394,4 +394,157 @@ suite('venv Creation provider tests', () => { assert.isTrue(showErrorMessageWithLogsStub.notCalled); assert.isTrue(deleteEnvironmentStub.notCalled); }); + + test('Create venv with 1000 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 1000 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expected = JSON.stringify({ requirements: requirements.map((r) => r.installItem) }); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + execObservableStub.callsFake((_c, argv: string[], options) => { + stdin = options?.stdinStr; + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.strictEqual(stdin, expected); + assert.isTrue(hasStdinArg); + }); + + test('Create venv with 5 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 5 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expectedRequirements = requirements.map((r) => r.installItem).sort(); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + let actualRequirements: string[] = []; + execObservableStub.callsFake((_c, argv: string[], options: SpawnOptions) => { + stdin = options?.stdinStr; + actualRequirements = argv.filter((arg) => arg.startsWith('requirements')).sort(); + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.isUndefined(stdin); + assert.deepStrictEqual(actualRequirements, expectedRequirements); + assert.isFalse(hasStdinArg); + }); }); From 203f58bc169afc5a731557633d25722bed038bd1 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 14 Sep 2023 11:54:59 -0700 Subject: [PATCH 270/336] Fix bug in rawprocess where stdinStr was not passed (#21993) --- src/client/common/process/rawProcessApis.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts index 6f3e40d68736..23c2f3253bb1 100644 --- a/src/client/common/process/rawProcessApis.ts +++ b/src/client/common/process/rawProcessApis.ts @@ -257,6 +257,10 @@ export function execObservable( subscriber.error(ex); internalDisposables.forEach((d) => d.dispose()); }); + if (options.stdinStr !== undefined) { + proc.stdin?.write(options.stdinStr); + proc.stdin?.end(); + } }); return { From f3f48a2e662759b04857aa801d7b8abf7cdbca30 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 15 Sep 2023 04:54:05 -0700 Subject: [PATCH 271/336] Remove envShellEvent proposal usage (#21997) It's been finalized Part of microsoft/vscode#193181 --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7e044a95f7af..98f97c4a1905 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "enabledApiProposals": [ "contribEditorContentMenu", "quickPickSortByLabel", - "envShellEvent", "testObserver", "quickPickItemTooltip", "saveEditor" @@ -44,7 +43,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.82.0-20230830" + "vscode": "^1.83.0-20230915" }, "enableTelemetry": false, "keywords": [ @@ -2221,4 +2220,4 @@ "webpack-require-from": "^1.8.6", "yargs": "^15.3.1" } -} \ No newline at end of file +} From 187ca86a5411fbf886128b585fe4e58439c5bd47 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 15 Sep 2023 13:34:05 -0700 Subject: [PATCH 272/336] Do not upper case custom env variables (#22004) For #20950 closes https://github.com/microsoft/vscode-python/issues/22005 --- .../interpreter/activation/terminalEnvVarCollectionService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 75ef8168484b..9bc95ee6d2e3 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -36,6 +36,7 @@ import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; +import { normCase } from '../../common/platform/fs-paths'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { @@ -383,7 +384,7 @@ function getPromptForEnv(interpreter: PythonEnvironment | undefined) { function normCaseKeys(env: EnvironmentVariables): EnvironmentVariables { const result: EnvironmentVariables = {}; Object.keys(env).forEach((key) => { - result[key.toUpperCase()] = env[key]; + result[normCase(key)] = env[key]; }); return result; } From 5838ea64a365f9abc1fa623cb8e372d4d9628a50 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 15 Sep 2023 15:10:10 -0700 Subject: [PATCH 273/336] pytest complicated parameterize test parsing (#22001) fixes https://github.com/microsoft/vscode-python/issues/22000 --- .../pytestadapter/.data/parametrize_tests.py | 8 +++++--- .../expected_discovery_test_output.py | 20 +++++++++---------- pythonFiles/tests/pytestadapter/helpers.py | 4 ++++ pythonFiles/vscode_pytest/__init__.py | 6 +++--- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py index a39b7c26de9f..c4dbadc32d6e 100644 --- a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py +++ b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py @@ -15,6 +15,8 @@ def test_adding(actual, expected): # Testing pytest with parametrized tests. All three pass. # The tests ids are parametrize_tests.py::test_under_ten[1] and so on. -@pytest.mark.parametrize("num", range(1, 3)) # test_marker--test_under_ten -def test_under_ten(num): - assert num < 10 +@pytest.mark.parametrize( # test_marker--test_string + "string", ["hello", "complicated split [] ()"] +) +def test_string(string): + assert string == "hello" diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 2b2c07ab8ea7..31686d2b3b5d 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -594,46 +594,46 @@ ], }, { - "name": "test_under_ten", + "name": "test_string", "path": os.fspath(parameterize_tests_path), "type_": "function", "children": [ { - "name": "[1]", + "name": "[hello]", "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( - "test_under_ten[1]", + "test_string[hello]", parameterize_tests_path, ), "type_": "test", "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", + "parametrize_tests.py::test_string[hello]", parameterize_tests_path, ), "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", + "parametrize_tests.py::test_string[hello]", parameterize_tests_path, ), }, { - "name": "[2]", + "name": "[complicated split [] ()]", "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( - "test_under_ten[2]", + "test_string[1]", parameterize_tests_path, ), "type_": "test", "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", + "parametrize_tests.py::test_string[complicated split [] ()]", parameterize_tests_path, ), "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", + "parametrize_tests.py::test_string[complicated split [] ()]", parameterize_tests_path, ), }, ], - "id_": "parametrize_tests.py::test_under_ten", + "id_": "parametrize_tests.py::test_string", }, ], }, diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index 7195cfe43ea5..b534e950945a 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -12,6 +12,10 @@ import uuid from typing import Any, Dict, List, Optional, Tuple +script_dir = pathlib.Path(__file__).parent.parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 3d5bde44204f..9e315d7919bb 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -392,9 +392,9 @@ def build_test_tree(session: pytest.Session) -> TestNode: elif hasattr(test_case, "callspec"): # This means it is a parameterized test. function_name: str = "" # parameterized test cases cut the repetitive part of the name off. - name_split = test_node["name"].split("[") - test_node["name"] = "[" + name_split[1] - parent_path = os.fspath(get_node_path(test_case)) + "::" + name_split[0] + parent_part, parameterized_section = test_node["name"].split("[", 1) + test_node["name"] = "[" + parameterized_section + parent_path = os.fspath(get_node_path(test_case)) + "::" + parent_part try: function_name = test_case.originalname # type: ignore function_test_case = function_nodes_dict[parent_path] From 6b3dec49fa5d9fe3046d3c4bdbefaec7e7d3b806 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 15 Sep 2023 15:47:18 -0700 Subject: [PATCH 274/336] Support EOT for testing (#21876) Adds support for the end of transmission (EOT) operator to all pytest and unittest responses. this PR includes: - addition of an EOT that is added to run and discovery returns and processed by the extension to initiate cleanup after run/discovery finishes - updates to all tests to support the use of EOT - redesign of how cleanup works following run/discover to make it more streamlined - full functional tests that check multiple different types of payload splitting from the buffer - tests for the cancellation token during run and debug modes --- pythonFiles/testing_tools/socket_manager.py | 8 +- .../tests/pytestadapter/test_discovery.py | 129 +++-- .../tests/pytestadapter/test_execution.py | 110 ++-- pythonFiles/unittestadapter/discovery.py | 41 +- pythonFiles/unittestadapter/execution.py | 43 +- pythonFiles/vscode_pytest/__init__.py | 147 +++-- .../vscode_pytest/run_pytest_script.py | 2 +- .../testController/common/resultResolver.ts | 50 +- .../testing/testController/common/server.ts | 116 ++-- .../testing/testController/common/types.ts | 20 +- .../testing/testController/common/utils.ts | 105 +++- .../testing/testController/controller.ts | 1 + .../pytest/pytestDiscoveryAdapter.ts | 22 +- .../pytest/pytestExecutionAdapter.ts | 62 +- .../unittest/testDiscoveryAdapter.ts | 9 +- .../unittest/testExecutionAdapter.ts | 28 +- .../testing/common/testingAdapter.test.ts | 533 +++++++++-------- .../testing/common/testingPayloadsEot.test.ts | 210 +++++++ .../testController/payloadTestCases.ts | 149 +++++ .../pytestExecutionAdapter.unit.test.ts | 44 +- .../resultResolver.unit.test.ts | 31 +- .../testController/server.unit.test.ts | 538 ++++++------------ .../testCancellationRunAdapters.unit.test.ts | 350 ++++++++++++ .../testing/testController/utils.unit.test.ts | 22 +- .../test_parameterized_subtest.py | 4 +- 25 files changed, 1882 insertions(+), 892 deletions(-) create mode 100644 src/test/testing/common/testingPayloadsEot.test.ts create mode 100644 src/test/testing/testController/payloadTestCases.ts create mode 100644 src/test/testing/testController/testCancellationRunAdapters.unit.test.ts diff --git a/pythonFiles/testing_tools/socket_manager.py b/pythonFiles/testing_tools/socket_manager.py index 372a50b5e012..b2afbf0e5a17 100644 --- a/pythonFiles/testing_tools/socket_manager.py +++ b/pythonFiles/testing_tools/socket_manager.py @@ -23,6 +23,12 @@ def __init__(self, addr): self.socket = None def __enter__(self): + return self.connect() + + def __exit__(self, *_): + self.close() + + def connect(self): self.socket = socket.socket( socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP ) @@ -35,7 +41,7 @@ def __enter__(self): return self - def __exit__(self, *_): + def close(self): if self.socket: try: self.socket.shutdown(socket.SHUT_RDWR) diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 8d785be27c8b..674d92ac0545 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -29,15 +29,26 @@ def test_import_error(tmp_path): temp_dir.mkdir() p = temp_dir / "error_pytest_import.py" shutil.copyfile(file_path, p) - actual_list: Optional[List[Dict[str, Any]]] = runner( - ["--collect-only", os.fspath(p)] - ) - assert actual_list - for actual in actual_list: - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + actual: Optional[List[Dict[str, Any]]] = runner(["--collect-only", os.fspath(p)]) + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False def test_syntax_error(tmp_path): @@ -60,13 +71,25 @@ def test_syntax_error(tmp_path): p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) actual = runner(["--collect-only", os.fspath(p)]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False def test_parameterized_error_collect(): @@ -76,12 +99,25 @@ def test_parameterized_error_collect(): """ file_path_str = "error_parametrize_discovery.py" actual = runner(["--collect-only", file_path_str]) - if actual: - actual = actual[0] - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False @pytest.mark.parametrize( @@ -146,13 +182,16 @@ def test_pytest_collect(file, expected_const): os.fspath(TEST_DATA_PATH / file), ] ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert actual["tests"] == expected_const + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + assert actual_item.get("tests") == expected_const def test_pytest_root_dir(): @@ -168,14 +207,16 @@ def test_pytest_root_dir(): ], TEST_DATA_PATH / "root", ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH / "root") assert ( - actual["tests"] + actual_item.get("tests") == expected_discovery_test_output.root_with_config_expected_output ) @@ -193,13 +234,15 @@ def test_pytest_config_file(): ], TEST_DATA_PATH / "root", ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH / "root") assert ( - actual["tests"] + actual_item.get("tests") == expected_discovery_test_output.root_with_config_expected_output ) diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 07354b01709b..37a392f66d4b 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import os import shutil +from typing import Any, Dict, List import pytest @@ -23,14 +24,19 @@ def test_config_file(): expected_execution_test_output.config_file_pytest_expected_execution_output ) assert actual - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const def test_rootdir_specified(): @@ -43,14 +49,19 @@ def test_rootdir_specified(): expected_execution_test_output.config_file_pytest_expected_execution_output ) assert actual - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const def test_syntax_error_execution(tmp_path): @@ -73,13 +84,23 @@ def test_syntax_error_execution(tmp_path): p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) actual = runner(["error_syntax_discover.py::test_function"]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 1 + else: + assert False def test_bad_id_error_execution(): @@ -88,13 +109,23 @@ def test_bad_id_error_execution(): The json should still be returned but the errors list should be present. """ actual = runner(["not/a/real::test_id"]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 1 + else: + assert False @pytest.mark.parametrize( @@ -195,7 +226,8 @@ def test_pytest_execution(test_ids, expected_const): 3. uf_single_method_execution_expected_output: test run on a single method in a file. 4. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. 5. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. - 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file at the top level and one test file in a nested folder. + 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file + at the top level and one test file in a nested folder. 7. double_nested_folder_expected_execution_output: test run on a double nested folder. 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. @@ -205,18 +237,22 @@ def test_pytest_execution(test_ids, expected_const): Keyword arguments: test_ids -- an array of test_ids to run. expected_const -- a dictionary of the expected output from running pytest discovery on the files. - """ # noqa: E501 + """ args = test_ids actual = runner(args) assert actual - print(actual) - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(TEST_DATA_PATH) - actual_result_dict.update(a["result"]) + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + actual_result_dict.update(actual_item["result"]) for key in actual_result_dict: if ( actual_result_dict[key]["outcome"] == "failure" diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index cbad40ad1838..6208d24bee9f 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -48,6 +48,13 @@ class PayloadDict(TypedDict): error: NotRequired[List[str]] +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Literal["discovery"] | Literal["execution"] + eot: bool + + def discover_tests( start_dir: str, pattern: str, top_level_dir: Optional[str], uuid: Optional[str] ) -> PayloadDict: @@ -106,17 +113,7 @@ def discover_tests( return payload -if __name__ == "__main__": - # Get unittest discovery arguments. - argv = sys.argv[1:] - index = argv.index("--udiscovery") - - start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) - - # Perform test discovery. - port, uuid = parse_discovery_cli_args(argv[:index]) - payload = discover_tests(start_dir, pattern, top_level_dir, uuid) - +def post_response(payload: PayloadDict | EOTPayloadDict, port: int, uuid: str) -> None: # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) data = json.dumps(payload) @@ -132,3 +129,25 @@ def discover_tests( except Exception as e: print(f"Error sending response: {e}") print(f"Request data: {request}") + + +if __name__ == "__main__": + # Get unittest discovery arguments. + argv = sys.argv[1:] + index = argv.index("--udiscovery") + + start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) + + # Perform test discovery. + port, uuid = parse_discovery_cli_args(argv[:index]) + # Post this discovery payload. + if uuid is not None: + payload = discover_tests(start_dir, pattern, top_level_dir, uuid) + post_response(payload, port, uuid) + # Post EOT token. + eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} + post_response(eot_payload, port, uuid) + else: + print("Error: no uuid provided or parsed.") + eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} + post_response(eot_payload, port, "") diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index f239f81c2d87..7bbc97e78f31 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import argparse +import atexit import enum import json import os @@ -17,7 +18,7 @@ sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from typing_extensions import NotRequired, TypeAlias, TypedDict +from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict from testing_tools import process_json_util, socket_manager from unittestadapter.utils import parse_unittest_args @@ -168,6 +169,13 @@ class PayloadDict(TypedDict): error: NotRequired[str] +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Literal["discovery"] | Literal["execution"] + eot: bool + + # Args: start_path path to a directory or a file, list of ids that may be empty. # Edge cases: # - if tests got deleted since the VS Code side last ran discovery and the current test run, @@ -225,8 +233,11 @@ def run_tests( return payload +__socket = None +atexit.register(lambda: __socket.close() if __socket else None) + + def send_run_data(raw_data, port, uuid): - # Build the request data (it has to be a POST request or the Node side will not process it), and send it. status = raw_data["outcome"] cwd = os.path.abspath(START_DIR) if raw_data["subtest"]: @@ -236,7 +247,20 @@ def send_run_data(raw_data, port, uuid): test_dict = {} test_dict[test_id] = raw_data payload: PayloadDict = {"cwd": cwd, "status": status, "result": test_dict} + post_response(payload, port, uuid) + + +def post_response(payload: PayloadDict | EOTPayloadDict, port: int, uuid: str) -> None: + # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) + global __socket + if __socket is None: + try: + __socket = socket_manager.SocketManager(addr) + __socket.connect() + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + __socket = None data = json.dumps(payload) request = f"""Content-Length: {len(data)} Content-Type: application/json @@ -244,11 +268,10 @@ def send_run_data(raw_data, port, uuid): {data}""" try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Error sending response: {e}") + if __socket is not None and __socket.socket is not None: + __socket.socket.sendall(request.encode("utf-8")) + except Exception as ex: + print(f"Error sending response: {ex}") print(f"Request data: {request}") @@ -312,3 +335,9 @@ def send_run_data(raw_data, port, uuid): "error": "No test ids received from buffer", "result": None, } + eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True} + if UUID is None: + print("Error sending response, uuid unknown to python server.") + post_response(eot_payload, PORT, "unknown") + else: + post_response(eot_payload, PORT, UUID) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 9e315d7919bb..21165a02bf4b 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -1,7 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import atexit import json import os import pathlib import sys +import time import traceback import pytest @@ -301,12 +306,6 @@ def pytest_sessionfinish(session, exitstatus): 4: Pytest encountered an internal error or exception during test execution. 5: Pytest was unable to find any tests to run. """ - print( - "pytest session has finished, exit status: ", - exitstatus, - "in discovery? ", - IS_DISCOVERY, - ) cwd = pathlib.Path.cwd() if IS_DISCOVERY: if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5): @@ -352,6 +351,10 @@ def pytest_sessionfinish(session, exitstatus): exitstatus_bool, None, ) + # send end of transmission token + command_type = "discovery" if IS_DISCOVERY else "execution" + payload: EOTPayloadDict = {"command_type": command_type, "eot": True} + send_post_request(payload) def build_test_tree(session: pytest.Session) -> TestNode: @@ -603,44 +606,60 @@ class ExecutionPayloadDict(Dict): error: Union[str, None] # Currently unused need to check +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Literal["discovery"] | Literal["execution"] + eot: bool + + def get_node_path(node: Any) -> pathlib.Path: + """A function that returns the path of a node given the switch to pathlib.Path.""" return getattr(node, "path", pathlib.Path(node.fspath)) +__socket = None +atexit.register(lambda: __socket.close() if __socket else None) + + def execution_post( - cwd: str, - status: Literal["success", "error"], - tests: Union[testRunResultDict, None], + cwd: str, status: Literal["success", "error"], tests: Union[testRunResultDict, None] ): """ - Sends a post request to the server after the tests have been executed. - Keyword arguments: - cwd -- the current working directory. - session_node -- the status of running the tests - tests -- the tests that were run and their status. + Sends a POST request with execution payload details. + + Args: + cwd (str): Current working directory. + status (Literal["success", "error"]): Execution status indicating success or error. + tests (Union[testRunResultDict, None]): Test run results, if available. """ - testPort = os.getenv("TEST_PORT", 45454) - testuuid = os.getenv("TEST_UUID") + payload: ExecutionPayloadDict = ExecutionPayloadDict( cwd=cwd, status=status, result=tests, not_found=None, error=None ) if ERRORS: payload["error"] = ERRORS + send_post_request(payload) - addr = ("localhost", int(testPort)) - data = json.dumps(payload) - request = f"""Content-Length: {len(data)} -Content-Type: application/json -Request-uuid: {testuuid} -{data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") +def post_response(cwd: str, session_node: TestNode) -> None: + """ + Sends a POST request with test session details in payload. + + Args: + cwd (str): Current working directory. + session_node (TestNode): Node information of the test session. + """ + + payload: DiscoveryPayloadDict = { + "cwd": cwd, + "status": "success" if not ERRORS else "error", + "tests": session_node, + "error": [], + } + if ERRORS is not None: + payload["error"] = ERRORS + send_post_request(payload, cls_encoder=PathEncoder) class PathEncoder(json.JSONEncoder): @@ -652,35 +671,55 @@ def default(self, obj): return super().default(obj) -def post_response(cwd: str, session_node: TestNode) -> None: - """Sends a post request to the server. +def send_post_request( + payload: ExecutionPayloadDict | DiscoveryPayloadDict | EOTPayloadDict, + cls_encoder=None, +): + """ + Sends a post request to the server. Keyword arguments: - cwd -- the current working directory. - session_node -- the session node, which is the top of the testing tree. - errors -- a list of errors that occurred during test collection. + payload -- the payload data to be sent. + cls_encoder -- a custom encoder if needed. """ - payload: DiscoveryPayloadDict = { - "cwd": cwd, - "status": "success" if not ERRORS else "error", - "tests": session_node, - "error": [], - } - if ERRORS is not None: - payload["error"] = ERRORS - test_port: Union[str, int] = os.getenv("TEST_PORT", 45454) - test_uuid: Union[str, None] = os.getenv("TEST_UUID") - addr = "localhost", int(test_port) - data = json.dumps(payload, cls=PathEncoder) + testPort = os.getenv("TEST_PORT", 45454) + testuuid = os.getenv("TEST_UUID") + addr = ("localhost", int(testPort)) + global __socket + + if __socket is None: + try: + __socket = socket_manager.SocketManager(addr) + __socket.connect() + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + __socket = None + + data = json.dumps(payload, cls=cls_encoder) request = f"""Content-Length: {len(data)} Content-Type: application/json -Request-uuid: {test_uuid} +Request-uuid: {testuuid} {data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") + + max_retries = 3 + retries = 0 + while retries < max_retries: + try: + if __socket is not None and __socket.socket is not None: + __socket.socket.sendall(request.encode("utf-8")) + # print("Post request sent successfully!") + # print("data sent", payload, "end of data") + break # Exit the loop if the send was successful + else: + print("Plugin error connection error[vscode-pytest]") + print(f"[vscode-pytest] data: {request}") + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + print(f"[vscode-pytest] data: {request}") + retries += 1 # Increment retry counter + if retries < max_retries: + print(f"Retrying ({retries}/{max_retries}) in 2 seconds...") + time.sleep(2) # Wait for a short duration before retrying + else: + print("Maximum retry attempts reached. Cannot send post request.") diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py index ffb4d0c55b16..0fca8208a406 100644 --- a/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -53,7 +53,7 @@ buffer = b"" # Process the JSON data - print(f"Received JSON data: {test_ids_from_buffer}") + print("Received JSON data in run script") break except json.JSONDecodeError: # JSON decoding error, the complete JSON object is not yet received diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index aa9f2a541f51..5ef6695ca280 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -3,7 +3,7 @@ import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode'; import * as util from 'util'; -import { DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; +import { DiscoveredTestPayload, EOTTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; import { TestProvider } from '../../types'; import { traceError, traceLog } from '../../../logging'; import { Testing } from '../../../common/utils/localize'; @@ -12,6 +12,7 @@ import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { splitLines } from '../../../common/stringUtils'; import { buildErrorNodeOptions, fixLogLines, populateTestTree } from './utils'; +import { Deferred } from '../../../common/utils/async'; export class PythonResultResolver implements ITestResultResolver { testController: TestController; @@ -35,16 +36,30 @@ export class PythonResultResolver implements ITestResultResolver { this.vsIdToRunId = new Map(); } - public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise { - const workspacePath = this.workspaceUri.fsPath; - traceLog('Using result resolver for discovery'); - - const rawTestData = payload; - if (!rawTestData) { + public resolveDiscovery( + payload: DiscoveredTestPayload | EOTTestPayload, + deferredTillEOT: Deferred, + token?: CancellationToken, + ): Promise { + if (!payload) { // No test data is available return Promise.resolve(); } + if ('eot' in payload) { + // the payload is an EOT payload, so resolve the deferred promise. + traceLog('ResultResolver EOT received for discovery.'); + const eotPayload = payload as EOTTestPayload; + if (eotPayload.eot === true) { + deferredTillEOT.resolve(); + return Promise.resolve(); + } + } + return this._resolveDiscovery(payload as DiscoveredTestPayload, token); + } + public _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise { + const workspacePath = this.workspaceUri.fsPath; + const rawTestData = payload as DiscoveredTestPayload; // Check if there were any errors in the discovery process. if (rawTestData.status === 'error') { const testingErrorConst = @@ -87,8 +102,25 @@ export class PythonResultResolver implements ITestResultResolver { return Promise.resolve(); } - public resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise { - const rawTestExecData = payload; + public resolveExecution( + payload: ExecutionTestPayload | EOTTestPayload, + runInstance: TestRun, + deferredTillEOT: Deferred, + ): Promise { + if (payload !== undefined && 'eot' in payload) { + // the payload is an EOT payload, so resolve the deferred promise. + traceLog('ResultResolver EOT received for execution.'); + const eotPayload = payload as EOTTestPayload; + if (eotPayload.eot === true) { + deferredTillEOT.resolve(); + return Promise.resolve(); + } + } + return this._resolveExecution(payload as ExecutionTestPayload, runInstance); + } + + public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise { + const rawTestExecData = payload as ExecutionTestPayload; if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { // Map which holds the subtest information for each test item. diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index d0a225ae5712..f59c486f7a85 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -15,7 +15,7 @@ import { traceError, traceInfo, traceLog } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; -import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER, createExecutionErrorPayload } from './utils'; +import { createEOTPayload, createExecutionErrorPayload, extractJsonPayload } from './utils'; import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { @@ -35,56 +35,22 @@ export class PythonTestServer implements ITestServer, Disposable { this.server = net.createServer((socket: net.Socket) => { let buffer: Buffer = Buffer.alloc(0); // Buffer to accumulate received data socket.on('data', (data: Buffer) => { - try { - let rawData: string = data.toString(); - buffer = Buffer.concat([buffer, data]); - while (buffer.length > 0) { - const rpcHeaders = jsonRPCHeaders(buffer.toString()); - const uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); - const totalContentLength = rpcHeaders.headers.get('Content-Length'); - if (!uuid) { - traceError('On data received: Error occurred because payload UUID is undefined'); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - if (!this.uuids.includes(uuid)) { - traceError('On data received: Error occurred because the payload UUID is not recognized'); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - rawData = rpcHeaders.remainingRawData; - const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); - const extractedData = rpcContent.extractedJSON; - // do not send until we have the full content - if (extractedData.length === Number(totalContentLength)) { - // if the rawData includes tests then this is a discovery request - if (rawData.includes(`"tests":`)) { - this._onDiscoveryDataReceived.fire({ - uuid, - data: rpcContent.extractedJSON, - }); - // if the rawData includes result then this is a run request - } else if (rawData.includes(`"result":`)) { - this._onRunDataReceived.fire({ - uuid, - data: rpcContent.extractedJSON, - }); - } else { - traceLog( - `Error processing test server request: request is not recognized as discovery or run.`, - ); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - // this.uuids = this.uuids.filter((u) => u !== uuid); WHERE DOES THIS GO?? - buffer = Buffer.alloc(0); - } else { + buffer = Buffer.concat([buffer, data]); // get the new data and add it to the buffer + while (buffer.length > 0) { + try { + // try to resolve data, returned unresolved data + const remainingBuffer = this._resolveData(buffer); + if (remainingBuffer.length === buffer.length) { + // if the remaining buffer is exactly the same as the buffer before processing, + // then there is no more data to process so loop should be exited. break; } + buffer = remainingBuffer; + } catch (ex) { + traceError(`Error reading data from buffer: ${ex} observed.`); + buffer = Buffer.alloc(0); + this._onDataReceived.fire({ uuid: '', data: '' }); } - } catch (ex) { - traceError(`Error processing test server request: ${ex} observe`); - this._onDataReceived.fire({ uuid: '', data: '' }); } }); }); @@ -107,6 +73,47 @@ export class PythonTestServer implements ITestServer, Disposable { }); } + savedBuffer = ''; + + public _resolveData(buffer: Buffer): Buffer { + try { + const extractedJsonPayload = extractJsonPayload(buffer.toString(), this.uuids); + // what payload is so small it doesn't include the whole UUID think got this + if (extractedJsonPayload.uuid !== undefined && extractedJsonPayload.cleanedJsonData !== undefined) { + // if a full json was found in the buffer, fire the data received event then keep cycling with the remaining raw data. + traceInfo(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); + this._fireDataReceived(extractedJsonPayload.uuid, extractedJsonPayload.cleanedJsonData); + } + buffer = Buffer.from(extractedJsonPayload.remainingRawData); + if (buffer.length === 0) { + // if the buffer is empty, then there is no more data to process so buffer should be cleared. + buffer = Buffer.alloc(0); + } + } catch (ex) { + traceError(`Error attempting to resolve data: ${ex}`); + this._onDataReceived.fire({ uuid: '', data: '' }); + } + return buffer; + } + + private _fireDataReceived(uuid: string, extractedJSON: string): void { + if (extractedJSON.includes(`"tests":`) || extractedJSON.includes(`"command_type": "discovery"`)) { + this._onDiscoveryDataReceived.fire({ + uuid, + data: extractedJSON, + }); + // if the rawData includes result then this is a run request + } else if (extractedJSON.includes(`"result":`) || extractedJSON.includes(`"command_type": "execution"`)) { + this._onRunDataReceived.fire({ + uuid, + data: extractedJSON, + }); + } else { + traceError(`Error processing test server request: request is not recognized as discovery or run.`); + this._onDataReceived.fire({ uuid: '', data: '' }); + } + } + public serverReady(): Promise { return this.ready; } @@ -208,10 +215,9 @@ export class PythonTestServer implements ITestServer, Disposable { traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } const deferred = createDeferred>(); - const result = execService.execObservable(args, spawnOptions); - runInstance?.token.onCancellationRequested(() => { + traceInfo('Test run cancelled, killing unittest subprocess.'); result?.proc?.kill(); }); @@ -226,18 +232,26 @@ export class PythonTestServer implements ITestServer, Disposable { result?.proc?.on('exit', (code, signal) => { // if the child has testIds then this is a run request if (code !== 0 && testIds) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + ); // if the child process exited with a non-zero exit code, then we need to send the error payload. this._onRunDataReceived.fire({ uuid, data: JSON.stringify(createExecutionErrorPayload(code, signal, testIds, options.cwd)), }); + // then send a EOT payload + this._onRunDataReceived.fire({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); } deferred.resolve({ stdout: '', stderr: '' }); - callback?.(); }); await deferred.promise; } } catch (ex) { + traceError(`Error while server attempting to run unittest command: ${ex}`); this.uuids = this.uuids.filter((u) => u !== uuid); this._onDataReceived.fire({ uuid, diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 610c3d76d17b..386c397b310c 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -14,6 +14,7 @@ import { } from 'vscode'; import { ITestDebugLauncher, TestDiscoveryOptions } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; +import { Deferred } from '../../../common/utils/async'; export type TestRunInstanceOptions = TestRunOptions & { exclude?: readonly TestItem[]; @@ -191,8 +192,18 @@ export interface ITestResultResolver { runIdToVSid: Map; runIdToTestItem: Map; vsIdToRunId: Map; - resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise; - resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise; + resolveDiscovery( + payload: DiscoveredTestPayload | EOTTestPayload, + deferredTillEOT: Deferred, + token?: CancellationToken, + ): Promise; + resolveExecution( + payload: ExecutionTestPayload | EOTTestPayload, + runInstance: TestRun, + deferredTillEOT: Deferred, + ): Promise; + _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise; + _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise; } export interface ITestDiscoveryAdapter { // ** first line old method signature, second line new method signature @@ -241,6 +252,11 @@ export type DiscoveredTestPayload = { error?: string[]; }; +export type EOTTestPayload = { + commandType: 'discovery' | 'execution'; + eot: boolean; +}; + export type ExecutionTestPayload = { cwd: string; status: 'success' | 'error'; diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index dd1b51551a45..572863ecdbfe 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -9,27 +9,100 @@ import { EnableTestAdapterRewrite } from '../../../common/experiments/groups'; import { IExperimentService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities'; -import { DiscoveredTestItem, DiscoveredTestNode, ExecutionTestPayload, ITestResultResolver } from './types'; +import { + DiscoveredTestItem, + DiscoveredTestNode, + EOTTestPayload, + ExecutionTestPayload, + ITestResultResolver, +} from './types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}\r\n`; } -export interface IJSONRPCContent { +export interface IJSONRPCData { extractedJSON: string; remainingRawData: string; } -export interface IJSONRPCHeaders { +export interface ParsedRPCHeadersAndData { headers: Map; remainingRawData: string; } +export interface ExtractOutput { + uuid: string | undefined; + cleanedJsonData: string | undefined; + remainingRawData: string; +} + export const JSONRPC_UUID_HEADER = 'Request-uuid'; export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length'; export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type'; -export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { +export function createEOTDeferred(): Deferred { + return createDeferred(); +} + +export function extractJsonPayload(rawData: string, uuids: Array): ExtractOutput { + /** + * Extracts JSON-RPC payload from the provided raw data. + * @param {string} rawData - The raw string data from which the JSON payload will be extracted. + * @param {Array} uuids - The list of UUIDs that are active. + * @returns {string} The remaining raw data after the JSON payload is extracted. + */ + + const rpcHeaders: ParsedRPCHeadersAndData = parseJsonRPCHeadersAndData(rawData); + + // verify the RPC has a UUID and that it is recognized + let uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); + uuid = checkUuid(uuid, uuids); + + const payloadLength = rpcHeaders.headers.get('Content-Length'); + + // separate out the data within context length of the given payload from the remaining data in the buffer + const rpcContent: IJSONRPCData = ExtractJsonRPCData(payloadLength, rpcHeaders.remainingRawData); + const cleanedJsonData = rpcContent.extractedJSON; + const { remainingRawData } = rpcContent; + + // if the given payload has the complete json, process it otherwise wait for the rest in the buffer + if (cleanedJsonData.length === Number(payloadLength)) { + // call to process this data + // remove this data from the buffer + return { uuid, cleanedJsonData, remainingRawData }; + } + // wait for the remaining + return { uuid: undefined, cleanedJsonData: undefined, remainingRawData: rawData }; +} + +export function checkUuid(uuid: string | undefined, uuids: Array): string | undefined { + if (!uuid) { + // no UUID found, this could occurred if the payload is full yet so send back without erroring + return undefined; + } + if (!uuids.includes(uuid)) { + // no UUID found, this could occurred if the payload is full yet so send back without erroring + throw new Error('On data received: Error occurred because the payload UUID is not recognized'); + } + return uuid; +} + +export function parseJsonRPCHeadersAndData(rawData: string): ParsedRPCHeadersAndData { + /** + * Parses the provided raw data to extract JSON-RPC specific headers and remaining data. + * + * This function aims to extract specific JSON-RPC headers (like UUID, content length, + * and content type) from the provided raw string data. Headers are expected to be + * delimited by newlines and the format should be "key:value". The function stops parsing + * once it encounters an empty line, and the rest of the data after this line is treated + * as the remaining raw data. + * + * @param {string} rawData - The raw string containing headers and possibly other data. + * @returns {ParsedRPCHeadersAndData} An object containing the parsed headers as a map and the + * remaining raw data after the headers. + */ const lines = rawData.split('\n'); let remainingRawData = ''; const headerMap = new Map(); @@ -51,8 +124,21 @@ export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { }; } -export function jsonRPCContent(headers: Map, rawData: string): IJSONRPCContent { - const length = parseInt(headers.get('Content-Length') ?? '0', 10); +export function ExtractJsonRPCData(payloadLength: string | undefined, rawData: string): IJSONRPCData { + /** + * Extracts JSON-RPC content based on provided headers and raw data. + * + * This function uses the `Content-Length` header from the provided headers map + * to determine how much of the rawData string represents the actual JSON content. + * After extracting the expected content, it also returns any remaining data + * that comes after the extracted content as remaining raw data. + * + * @param {string | undefined} payloadLength - The value of the `Content-Length` header. + * @param {string} rawData - The raw string data from which the JSON content will be extracted. + * + * @returns {IJSONRPCContent} An object containing the extracted JSON content and any remaining raw data. + */ + const length = parseInt(payloadLength ?? '0', 10); const data = rawData.slice(0, length); const remainingRawData = rawData.slice(length); return { @@ -212,3 +298,10 @@ export function createExecutionErrorPayload( } return etp; } + +export function createEOTPayload(executionBool: boolean): EOTTestPayload { + return { + commandType: executionBool ? 'execution' : 'discovery', + eot: true, + } as EOTTestPayload; +} diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 1550323ff8f8..0c7db5594004 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -371,6 +371,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); const dispose = token.onCancellationRequested(() => { + runInstance.appendOutput(`Run instance cancelled.\r\n`); runInstance.end(); }); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 450e2ef1edf2..bafa91847b42 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -9,9 +9,9 @@ import { SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; +import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, @@ -32,20 +32,20 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { ) {} async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { - const settings = this.configSettings.getSettings(uri); const uuid = this.testServer.createUUID(uri.fsPath); - const { pytestArgs } = settings.testing; - traceVerbose(pytestArgs); - const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { - this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + const deferredTillEOT: Deferred = createDeferred(); + const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived(async (e: DataReceivedEvent) => { + this.resultResolver?.resolveDiscovery(JSON.parse(e.data), deferredTillEOT); }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; pytest discovery.`); testServer.deleteUUID(uuid); dataReceivedDisposable.dispose(); }; try { await this.runPytestDiscovery(uri, uuid, executionFactory); } finally { + await deferredTillEOT.promise; disposeDataReceiver(this.testServer); } // this is only a placeholder to handle function overloading until rewrite is finished @@ -84,6 +84,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // delete UUID following entire discovery finishing. const deferredExec = createDeferred>(); const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + traceVerbose(`Running pytest discovery with command: ${execArgs.join(' ')}`); const result = execService?.execObservable(execArgs, spawnOptions); // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. @@ -94,7 +95,12 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { result?.proc?.stderr?.on('data', (data) => { spawnOptions.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', () => { + result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + ); + } deferredExec.resolve({ stdout: '', stderr: '' }); deferred.resolve(); }); diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 9705374d74af..085af40375d4 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -4,8 +4,8 @@ import { TestRun, Uri } from 'vscode'; import * as path from 'path'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; -import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { Deferred, createDeferred } from '../../../common/utils/async'; +import { traceError, traceInfo } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, @@ -42,29 +42,39 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { debugLauncher?: ITestDebugLauncher, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - traceVerbose(uri, testIds, debugBool); + const deferredTillEOT: Deferred = utils.createEOTDeferred(); const dataReceivedDisposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { - this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + const eParsed = JSON.parse(e.data); + this.resultResolver?.resolveExecution(eParsed, runInstance, deferredTillEOT); + } else { + traceError('No run instance found, cannot resolve execution.'); } }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; pytest execution.`); testServer.deleteUUID(uuid); dataReceivedDisposable.dispose(); }; runInstance?.token.onCancellationRequested(() => { - disposeDataReceiver(this.testServer); + traceInfo("Test run cancelled, resolving 'till EOT' deferred."); + deferredTillEOT.resolve(); }); - await this.runTestsNew( - uri, - testIds, - uuid, - runInstance, - debugBool, - executionFactory, - debugLauncher, - disposeDataReceiver, - ); + try { + this.runTestsNew( + uri, + testIds, + uuid, + runInstance, + debugBool, + executionFactory, + debugLauncher, + deferredTillEOT, + ); + } finally { + await deferredTillEOT.promise; + disposeDataReceiver(this.testServer); + } // placeholder until after the rewrite is adopted // TODO: remove after adoption. @@ -84,19 +94,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, - disposeDataReceiver?: (testServer: ITestServer) => void, + deferredTillEOT?: Deferred, ): Promise { - const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - this.configSettings.isTestExecution(); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - const spawnOptions: SpawnOptions = { cwd, throwOnStdErr: true, @@ -149,19 +156,19 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }; traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { - deferred.resolve(); - this.testServer.deleteUUID(uuid); + deferredTillEOT?.resolve(); }); } else { // combine path to run script with run args const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); const runArgs = [scriptPath, ...testArgs]; - traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); + traceInfo(`Running pytest with arguments: ${runArgs.join(' ')}\r\n`); const deferredExec = createDeferred>(); const result = execService?.execObservable(runArgs, spawnOptions); runInstance?.token.onCancellationRequested(() => { + traceInfo('Test run cancelled, killing pytest subprocess.'); result?.proc?.kill(); }); @@ -175,17 +182,24 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }); result?.proc?.on('exit', (code, signal) => { + traceInfo('Test run finished, subprocess exited.'); // if the child has testIds then this is a run request if (code !== 0 && testIds) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + ); // if the child process exited with a non-zero exit code, then we need to send the error payload. this.testServer.triggerRunDataReceivedEvent({ uuid, data: JSON.stringify(utils.createExecutionErrorPayload(code, signal, testIds, cwd)), }); + // then send a EOT payload + this.testServer.triggerRunDataReceivedEvent({ + uuid, + data: JSON.stringify(utils.createEOTPayload(true)), + }); } deferredExec.resolve({ stdout: '', stderr: '' }); - deferred.resolve(); - disposeDataReceiver?.(this.testServer); }); await deferredExec.promise; } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 9820aa89626c..440df4f94dc6 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -14,6 +14,7 @@ import { TestCommandOptions, TestDiscoveryCommand, } from '../common/types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. @@ -34,7 +35,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const command = buildDiscoveryCommand(unittestArgs); const uuid = this.testServer.createUUID(uri.fsPath); - + const deferredTillEOT: Deferred = createDeferred(); const options: TestCommandOptions = { workspaceFolder: uri, command, @@ -44,7 +45,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { - this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + this.resultResolver?.resolveDiscovery(JSON.parse(e.data), deferredTillEOT); }); const disposeDataReceiver = function (testServer: ITestServer) { testServer.deleteUUID(uuid); @@ -52,8 +53,10 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; await this.callSendCommand(options, () => { - disposeDataReceiver(this.testServer); + disposeDataReceiver?.(this.testServer); }); + await deferredTillEOT.promise; + disposeDataReceiver(this.testServer); // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index bc5e41d19d9d..9da0872ef601 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { TestRun, Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; +import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { DataReceivedEvent, @@ -15,7 +15,7 @@ import { TestCommandOptions, TestExecutionCommand, } from '../common/types'; -import { traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog } from '../../../logging'; import { startTestIdServer } from '../common/utils'; /** @@ -37,19 +37,30 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); + const deferredTillEOT: Deferred = createDeferred(); const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { - this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance, deferredTillEOT); + } else { + traceError('No run instance found, cannot resolve execution.'); } }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; unittest execution.`); testServer.deleteUUID(uuid); disposedDataReceived.dispose(); }; runInstance?.token.onCancellationRequested(() => { - disposeDataReceiver(this.testServer); + traceInfo("Test run cancelled, resolving 'till EOT' deferred."); + deferredTillEOT.resolve(); }); - await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, disposeDataReceiver); + try { + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, deferredTillEOT); + await deferredTillEOT.promise; + disposeDataReceiver(this.testServer); + } catch (error) { + traceError(`Error in running unittest tests: ${error}`); + } const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } @@ -60,7 +71,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uuid: string, runInstance?: TestRun, debugBool?: boolean, - disposeDataReceiver?: (testServer: ITestServer) => void, + deferredTillEOT?: Deferred, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -77,15 +88,12 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { testIds, outChannel: this.outputChannel, }; - - const deferred = createDeferred(); traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); const runTestIdsPort = await startTestIdServer(testIds); await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, testIds, () => { - deferred.resolve(); - disposeDataReceiver?.(this.testServer); + deferredTillEOT?.resolve(); }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 5af7e59f6a46..b445772d6958 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { TestRun, Uri } from 'vscode'; +import { TestController, TestRun, Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; import * as assert from 'assert'; import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; -import { ITestResultResolver, ITestServer } from '../../../client/testing/testController/common/types'; +import { ITestController, ITestResultResolver } from '../../../client/testing/testController/common/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { IPythonExecutionFactory } from '../../../client/common/process/types'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; @@ -17,16 +17,22 @@ import { traceLog } from '../../../client/logging'; import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; +import { TestProvider } from '../../../client/testing/types'; +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; suite('End to End Tests: test adapters', () => { - let resultResolver: typeMoq.IMock; - let pythonTestServer: ITestServer; + let resultResolver: ITestResultResolver; + let pythonTestServer: PythonTestServer; let pythonExecFactory: IPythonExecutionFactory; let debugLauncher: ITestDebugLauncher; let configService: IConfigurationService; - let testOutputChannel: ITestOutputChannel; let serviceContainer: IServiceContainer; let workspaceUri: Uri; + let testOutputChannel: typeMoq.IMock; + let testController: TestController; + const unittestProvider: TestProvider = UNITTEST_PROVIDER; + const pytestProvider: TestProvider = PYTEST_PROVIDER; const rootPathSmallWorkspace = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', @@ -54,83 +60,97 @@ suite('End to End Tests: test adapters', () => { configService = serviceContainer.get(IConfigurationService); pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); debugLauncher = serviceContainer.get(ITestDebugLauncher); - testOutputChannel = serviceContainer.get(ITestOutputChannel); - - // create mock resultResolver object - resultResolver = typeMoq.Mock.ofType(); + testController = serviceContainer.get(ITestController); // create objects that were not injected pythonTestServer = new PythonTestServer(pythonExecFactory, debugLauncher); await pythonTestServer.serverReady(); + + testOutputChannel = typeMoq.Mock.ofType(); + testOutputChannel + .setup((x) => x.append(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('output channel - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + testOutputChannel + .setup((x) => x.appendLine(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('output channel ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + }); + teardown(async () => { + pythonTestServer.dispose(); }); test('unittest discovery adapter small workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + workspaceUri = Uri.parse(rootPathSmallWorkspace); + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // set workspace to test workspace folder and set up settings - workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; // run unittest discovery const discoveryAdapter = new UnittestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { // verification after discovery is complete - // resultResolver.verify( - // (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - // typeMoq.Times.once(), - // ); // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); - }); - await discoveryAdapter.discoverTests(Uri.parse(rootPathErrorWorkspace)).finally(() => { - // verification after discovery is complete - - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(actualData.tests, 'Expected tests to be present'); + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('unittest discovery adapter large workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // set settings to work for the given workspace workspaceUri = Uri.parse(rootPathLargeWorkspace); @@ -139,84 +159,89 @@ suite('End to End Tests: test adapters', () => { const discoveryAdapter = new UnittestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('pytest discovery adapter small workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); - await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('pytest discovery adapter large workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - actualData = data; - return Promise.resolve(); - }); // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); // set workspace to test workspace folder @@ -224,32 +249,42 @@ suite('End to End Tests: test adapters', () => { await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('unittest execution adapter small workspace', async () => { // result resolver and saved data for assertions - let actualData: { - status: unknown; - error: string | any[]; - result: unknown; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - actualData = data; - return Promise.resolve(); - }); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); @@ -258,8 +293,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new UnittestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -273,33 +308,34 @@ suite('End to End Tests: test adapters', () => { await executionAdapter .runTests(workspaceUri, ['test_simple.SimpleClass.test_simple_unit'], false, testRun.object) .finally(() => { - // verification after execution is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm tests are found - assert.ok(actualData.result, 'Expected results to be present'); + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('unittest execution adapter large workspace', async () => { // result resolver and saved data for assertions - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveExecution ${data}`); - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status, can be subtest success or failure - assert( - data.status === 'subtest-success' || data.status === 'subtest-failure', - "Expected status to be 'subtest-success' or 'subtest-failure'", + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + const validStatuses = ['subtest-success', 'subtest-failure']; + assert.ok( + validStatuses.includes(payload.status), + `Expected status to be one of ${validStatuses.join(', ')}, but instead status is ${payload.status}`, ); - // 2. Confirm tests are found - assert.ok(data.result, 'Expected results to be present'); - return Promise.resolve(); - }); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathLargeWorkspace); @@ -309,8 +345,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new UnittestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -323,28 +359,35 @@ suite('End to End Tests: test adapters', () => { ); await executionAdapter .runTests(workspaceUri, ['test_parameterized_subtest.NumbersTest.test_even'], false, testRun.object) - .finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.atLeastOnce(), - ); + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('pytest execution adapter small workspace', async () => { // result resolver and saved data for assertions - let actualData: { - status: unknown; - error: string | any[]; - result: unknown; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - actualData = data; - return Promise.resolve(); - }); - // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); @@ -352,8 +395,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new PytestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -372,40 +415,42 @@ suite('End to End Tests: test adapters', () => { testRun.object, pythonExecFactory, ) - .finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error, null, "Expected no errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(actualData.result, 'Expected results to be present'); + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('pytest execution adapter large workspace', async () => { - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status is "success" - assert.strictEqual(data.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(data.error, null, "Expected no errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(data.result, 'Expected results to be present'); - return Promise.resolve(); - }); + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathLargeWorkspace); // generate list of test_ids const testIds: string[] = []; - for (let i = 0; i < 200; i = i + 1) { + for (let i = 0; i < 2000; i = i + 1) { const testId = `${rootPathLargeWorkspace}/test_parameterized_subtest.py::test_odd_even[${i}]`; testIds.push(testId); } @@ -414,8 +459,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new PytestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -426,35 +471,51 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); - await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { - // resolve execution should be called 200 times since there are 200 tests run. - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.exactly(200), - ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('unittest execution adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + console.log(`unittest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + assert.ok(data.result, 'Expected results to be present'); + // make sure the testID is found in the results + const indexOfTest = JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'); + assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + const testId = `test_seg_fault.TestSegmentationFault.test_segfault`; const testIds: string[] = [testId]; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status is "success" - assert.strictEqual(data.status, 'error', "Expected status to be 'error'"); - // 2. Confirm no errors - assert.ok(data.error, "Expected errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(data.result, 'Expected results to be present'); - // 4. make sure the testID is found in the results - assert.notDeepEqual( - JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'), - -1, - 'Expected testId to be present', - ); - return Promise.resolve(); - }); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathErrorWorkspace); @@ -463,8 +524,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new UnittestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -476,33 +537,45 @@ suite('End to End Tests: test adapters', () => { } as any), ); await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object).finally(() => { - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.exactly(1), - ); + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('pytest execution adapter seg fault error handling', async () => { - const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; - const testIds: string[] = [testId]; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status is "success" - assert.strictEqual(data.status, 'error', "Expected status to be 'error'"); - // 2. Confirm no errors - assert.ok(data.error, "Expected errors in 'error' field"); - // 3. Confirm tests are found + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + console.log(`pytest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + callCount = callCount + 1; + try { + if (data.status === 'error') { + assert.ok(data.error, "Expected errors in 'error' field"); + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } assert.ok(data.result, 'Expected results to be present'); - // 4. make sure the testID is found in the results - assert.notDeepEqual( - JSON.stringify(data).search('test_seg_fault.py::TestSegmentationFault::test_segfault'), - -1, - 'Expected testId to be present', + // make sure the testID is found in the results + const indexOfTest = JSON.stringify(data).search( + 'test_seg_fault.py::TestSegmentationFault::test_segfault', ); - return Promise.resolve(); - }); + assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + + const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; + const testIds: string[] = [testId]; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathErrorWorkspace); @@ -511,8 +584,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new PytestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -524,10 +597,8 @@ suite('End to End Tests: test adapters', () => { } as any), ); await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.exactly(1), - ); + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); }); diff --git a/src/test/testing/common/testingPayloadsEot.test.ts b/src/test/testing/common/testingPayloadsEot.test.ts new file mode 100644 index 000000000000..227ad5fa1697 --- /dev/null +++ b/src/test/testing/common/testingPayloadsEot.test.ts @@ -0,0 +1,210 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { TestController, TestRun, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as path from 'path'; +import * as assert from 'assert'; +import * as net from 'net'; +import { Observable } from 'rxjs'; +import * as crypto from 'crypto'; +// import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import * as sinon from 'sinon'; +import { ITestController, ITestResultResolver } from '../../../client/testing/testController/common/types'; +import { PythonTestServer } from '../../../client/testing/testController/common/server'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../../client/common/process/types'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { initialize } from '../../initialize'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; +import { PYTEST_PROVIDER } from '../../../client/testing/common/constants'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; +import { + PAYLOAD_SINGLE_CHUNK, + PAYLOAD_MULTI_CHUNK, + PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY, + DataWithPayloadChunks, + PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY, +} from '../testController/payloadTestCases'; +import { traceLog } from '../../../client/logging'; + +const FAKE_UUID = 'fake-u-u-i-d'; +export interface TestCase { + name: string; + value: DataWithPayloadChunks; +} + +const testCases: Array = [ + { + name: 'single payload single chunk', + value: PAYLOAD_SINGLE_CHUNK(FAKE_UUID), + }, + { + name: 'multiple payloads per buffer chunk', + value: PAYLOAD_MULTI_CHUNK(FAKE_UUID), + }, + { + name: 'single payload across multiple buffer chunks', + value: PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(FAKE_UUID), + }, + { + name: 'two chunks, payload split and two payloads in a chunk', + value: PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(FAKE_UUID), + }, +]; + +suite('EOT tests', () => { + let resultResolver: ITestResultResolver; + let pythonTestServer: PythonTestServer; + let debugLauncher: ITestDebugLauncher; + let configService: IConfigurationService; + let serviceContainer: IServiceContainer; + let workspaceUri: Uri; + let testOutputChannel: typeMoq.IMock; + let testController: TestController; + let stubExecutionFactory: typeMoq.IMock; + let client: net.Socket; + const sandbox = sinon.createSandbox(); + // const unittestProvider: TestProvider = UNITTEST_PROVIDER; + // const pytestProvider: TestProvider = PYTEST_PROVIDER; + const rootPathSmallWorkspace = path.join('src'); + suiteSetup(async () => { + serviceContainer = (await initialize()).serviceContainer; + }); + + setup(async () => { + // create objects that were injected + configService = serviceContainer.get(IConfigurationService); + debugLauncher = serviceContainer.get(ITestDebugLauncher); + testController = serviceContainer.get(ITestController); + + // create client to act as python server which sends testing result response + client = new net.Socket(); + client.on('error', (error) => { + traceLog('Socket connection error:', error); + }); + + const mockProc = new MockChildProcess('', ['']); + const output2 = new Observable>(() => { + /* no op */ + }); + + // stub out execution service and factory so mock data is returned from client. + const stubExecutionService = ({ + execObservable: () => { + client.connect(pythonTestServer.getPort()); + return { + proc: mockProc, + out: output2, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + stubExecutionFactory = typeMoq.Mock.ofType(); + stubExecutionFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(stubExecutionService)); + + // stub create UUID + + const v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); + + // create python test server + pythonTestServer = new PythonTestServer(stubExecutionFactory.object, debugLauncher); + await pythonTestServer.serverReady(); + // handles output from client + testOutputChannel = typeMoq.Mock.ofType(); + testOutputChannel + .setup((x) => x.append(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('out - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + testOutputChannel + .setup((x) => x.appendLine(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('outL - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + }); + teardown(async () => { + pythonTestServer.dispose(); + sandbox.restore(); + }); + testCases.forEach((testCase) => { + test(`Testing Payloads: ${testCase.name}`, async () => { + let actualCollectedResult = ''; + client.on('connect', async () => { + traceLog('socket connected, sending stubbed data'); + // payload is a string array, each string represents one line written to the buffer + const { payloadArray } = testCase.value; + for (let i = 0; i < payloadArray.length; i = i + 1) { + await (async (clientSub, payloadSub) => { + if (!clientSub.write(payloadSub)) { + // If write returns false, wait for the 'drain' event before proceeding + await new Promise((resolve) => clientSub.once('drain', resolve)); + } + })(client, payloadArray[i]); + } + client.end(); + }); + + resultResolver = new PythonResultResolver(testController, PYTEST_PROVIDER, workspaceUri); + resultResolver._resolveExecution = async (payload, _token?) => { + // the payloads that get to the _resolveExecution are all data and should be successful. + assert.strictEqual(payload.status, 'success', "Expected status to be 'success'"); + assert.ok(payload.result, 'Expected results to be present'); + actualCollectedResult = actualCollectedResult + JSON.stringify(payload.result); + return Promise.resolve(); + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathSmallWorkspace}/test_simple.py::test_a`], + false, + testRun.object, + stubExecutionFactory.object, + ) + .then(() => { + assert.strictEqual( + testCase.value.data, + actualCollectedResult, + "Expected collected result to match 'data'", + ); + // nervous about this not testing race conditions correctly + // client.end(); + // verify that the _resolveExecution was called once per test + // assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + }); + }); + }); +}); diff --git a/src/test/testing/testController/payloadTestCases.ts b/src/test/testing/testController/payloadTestCases.ts new file mode 100644 index 000000000000..5ddcc0edecf9 --- /dev/null +++ b/src/test/testing/testController/payloadTestCases.ts @@ -0,0 +1,149 @@ +export interface DataWithPayloadChunks { + payloadArray: string[]; + data: string; +} + +const EOT_PAYLOAD = `Content-Length: 42 +Content-Type: application/json +Request-uuid: fake-u-u-i-d + +{"command_type": "execution", "eot": true}`; + +const SINGLE_UNITTEST_SUBTEST = { + cwd: '/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace', + status: 'success', + result: { + 'test_parameterized_subtest.NumbersTest.test_even (i=0)': { + test: 'test_parameterized_subtest.NumbersTest.test_even', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'test_parameterized_subtest.NumbersTest.test_even (i=0)', + }, + }, +}; + +const SINGLE_PYTEST_PAYLOAD = { + cwd: 'path/to', + status: 'success', + result: { + 'path/to/file.py::test_funct': { + test: 'path/to/file.py::test_funct', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'path/to/file.py::test_funct', + }, + }, +}; + +const SINGLE_PYTEST_PAYLOAD_TWO = { + cwd: 'path/to/second', + status: 'success', + result: { + 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]': { + test: 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]', + outcome: 'success', + message: 'None', + traceback: null, + }, + }, +}; + +export function createPayload(uuid: string, data: unknown): string { + return `Content-Length: ${JSON.stringify(data).length} +Content-Type: application/json +Request-uuid: ${uuid} + +${JSON.stringify(data)}`; +} + +export function PAYLOAD_SINGLE_CHUNK(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + + return { + payloadArray: [payload, EOT_PAYLOAD], + data: JSON.stringify(SINGLE_UNITTEST_SUBTEST.result), + }; +} + +// more than one payload (item with header) per chunk sent +// payload has 3 SINGLE_UNITTEST_SUBTEST +export function PAYLOAD_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + let payload = ''; + let result = ''; + for (let i = 0; i < 3; i = i + 1) { + payload += createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + result += JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + } + return { + payloadArray: [payload, EOT_PAYLOAD], + data: result, + }; +} + +// single payload divided by an arbitrary character and split across payloads +export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); + // payload length is know to be >200 + const splitPayload: Array = [ + payload.substring(0, 50), + payload.substring(50, 100), + payload.substring(100, 150), + payload.substring(150), + ]; + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result); + splitPayload.push(EOT_PAYLOAD); + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +// here a payload is split across the buffer chunks and there are multiple payloads in a single buffer chunk +export function PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(uuid: string): DataWithPayloadChunks { + // payload1 length is know to be >200 + const payload1 = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); + const payload2 = createPayload(uuid, SINGLE_PYTEST_PAYLOAD_TWO); + + // chunk 1 is 50 char of payload1, chunk 2 is 50-end of payload1 and all of payload2 + const splitPayload: Array = [payload1.substring(0, 100), payload1.substring(100).concat(payload2)]; + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result).concat( + JSON.stringify(SINGLE_PYTEST_PAYLOAD_TWO.result), + ); + + splitPayload.push(EOT_PAYLOAD); + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +export function PAYLOAD_SPLIT_MULTI_CHUNK_RAN_ORDER_ARRAY(uuid: string): Array { + return [ + `Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=0)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=0)"}}} + +Content-Length: 411 +Content-Type: application/json +Request-uuid: 9${uuid} + +{"cwd": "/home/runner/work/vscode-`, + `python/vscode-python/path with`, + ` spaces/src" + +Content-Length: 959 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-failure", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=1)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-failure", "message": "(, AssertionError('1 != 0'), )", "traceback": " File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 57, in testPartExecutor\n yield\n File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 538, in subTest\n yield\n File \"/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py\", line 16, in test_even\n self.assertEqual(i % 2, 0)\nAssertionError: 1 != 0\n", "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=1)"}}} +Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=2)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=2)"}}}`, + ]; +} diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 43b763f56e6c..9cc428ab0a4c 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -21,6 +21,7 @@ import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/co import * as util from '../../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { traceInfo } from '../../../../client/logging'; suite('pytest test execution adapter', () => { let testServer: typeMoq.IMock; @@ -33,7 +34,7 @@ suite('pytest test execution adapter', () => { (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; let mockProc: MockChildProcess; - let utilsStub: sinon.SinonStub; + let utilsStartServerStub: sinon.SinonStub; setup(() => { testServer = typeMoq.Mock.ofType(); testServer.setup((t) => t.getPort()).returns(() => 12345); @@ -51,6 +52,8 @@ suite('pytest test execution adapter', () => { isTestExecution: () => false, } as unknown) as IConfigurationService; + // mock out the result resolver + // set up exec service with child process mockProc = new MockChildProcess('', ['']); const output = new Observable>(() => { @@ -67,7 +70,7 @@ suite('pytest test execution adapter', () => { }, })); execFactory = typeMoq.Mock.ofType(); - utilsStub = sinon.stub(util, 'startTestIdServer'); + utilsStartServerStub = sinon.stub(util, 'startTestIdServer'); debugLauncher = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) @@ -79,13 +82,6 @@ suite('pytest test execution adapter', () => { deferred.resolve(); return Promise.resolve({ stdout: '{}' }); }); - debugLauncher - .setup((d) => d.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve(); - }); - execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); @@ -104,7 +100,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -131,7 +127,7 @@ suite('pytest test execution adapter', () => { mockProc.trigger('close'); // assert - sinon.assert.calledWithExactly(utilsStub, testIds); + sinon.assert.calledWithExactly(utilsStartServerStub, testIds); }); test('pytest execution called with correct args', async () => { const deferred2 = createDeferred(); @@ -143,7 +139,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -175,7 +171,6 @@ suite('pytest test execution adapter', () => { TEST_UUID: 'uuid123', TEST_PORT: '12345', }; - // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); execService.verify( (x) => x.execObservable( @@ -203,7 +198,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -262,12 +257,28 @@ suite('pytest test execution adapter', () => { }); test('Debug launched correctly for pytest', async () => { const deferred3 = createDeferred(); - utilsStub.callsFake(() => { + const deferredEOT = createDeferred(); + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async () => { + traceInfo('stubs launch debugger'); + deferredEOT.resolve(); + }); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer @@ -298,5 +309,6 @@ suite('pytest test execution adapter', () => { ), typeMoq.Times.once(), ); + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); }); }); diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 694fdacb8049..2078c72e8cf6 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -14,6 +14,8 @@ import { import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; import * as util from '../../../client/testing/testController/common/utils'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { traceLog } from '../../../client/logging'; suite('Result Resolver tests', () => { suite('Test discovery', () => { @@ -87,7 +89,8 @@ suite('Result Resolver tests', () => { const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); // call resolve discovery - resultResolver.resolveDiscovery(payload, cancelationToken); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -126,7 +129,8 @@ suite('Result Resolver tests', () => { const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); // call resolve discovery - resultResolver.resolveDiscovery(payload); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -171,7 +175,8 @@ suite('Result Resolver tests', () => { // stub out functionality of populateTestTreeStub which is called in resolveDiscovery const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); // call resolve discovery - resultResolver.resolveDiscovery(payload, cancelationToken); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -286,7 +291,7 @@ suite('Result Resolver tests', () => { .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) .callback((id: string) => { generatedId = id; - console.log('createTestItem function called with id:', id); + traceLog('createTestItem function called with id:', id); }) .returns(() => ({ id: 'id_this', label: 'label_this', uri: workspaceUri } as TestItem)); @@ -307,7 +312,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item assert.ok(generatedId); @@ -347,7 +353,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); @@ -386,7 +393,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.once()); @@ -425,7 +433,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.errored(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); @@ -464,7 +473,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.once()); @@ -485,7 +495,8 @@ suite('Result Resolver tests', () => { error: 'error', }; - resultResolver.resolveExecution(errorPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(errorPayload, runInstance.object, deferredTillEOT); // verify that none of these functions are called diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 53c2b72e40f7..92a9a1135f55 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -6,58 +6,66 @@ import * as assert from 'assert'; import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; -import { OutputChannel, Uri } from 'vscode'; import { Observable } from 'rxjs'; import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; import { IPythonExecutionFactory, IPythonExecutionService, + ObservableExecutionResult, Output, - SpawnOptions, } from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; import { Deferred, createDeferred } from '../../../client/common/utils/async'; import { MockChildProcess } from '../../mocks/mockChildProcess'; - -suite('Python Test Server', () => { - const fakeUuid = 'fake-uuid'; - - let stubExecutionFactory: IPythonExecutionFactory; - let stubExecutionService: IPythonExecutionService; +import { + PAYLOAD_MULTI_CHUNK, + PAYLOAD_SINGLE_CHUNK, + PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY, + DataWithPayloadChunks, +} from './payloadTestCases'; +import { traceLog } from '../../../client/logging'; + +const testCases = [ + { + val: () => PAYLOAD_SINGLE_CHUNK('fake-uuid'), + }, + { + val: () => PAYLOAD_MULTI_CHUNK('fake-uuid'), + }, + { + val: () => PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY('fake-uuid'), + }, +]; + +suite('Python Test Server, DataWithPayloadChunks', () => { + const FAKE_UUID = 'fake-uuid'; let server: PythonTestServer; - let sandbox: sinon.SinonSandbox; let v4Stub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; let mockProc: MockChildProcess; let execService: typeMoq.IMock; let deferred: Deferred; - let execFactory = typeMoq.Mock.ofType(); - - setup(() => { - sandbox = sinon.createSandbox(); - v4Stub = sandbox.stub(crypto, 'randomUUID'); + const sandbox = sinon.createSandbox(); - v4Stub.returns(fakeUuid); - stubExecutionService = ({ - execObservable: () => Promise.resolve({ stdout: '', stderr: '' }), - } as unknown) as IPythonExecutionService; + setup(async () => { + // set up test command options - stubExecutionFactory = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService), - } as unknown) as IPythonExecutionFactory; + v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); // set up exec service with child process mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); - const output = new Observable>(() => { + const outputObservable = new Observable>(() => { /* no op */ }); execService .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ proc: mockProc, - out: output, + out: outputObservable, dispose: () => { /* no-body */ }, @@ -70,53 +78,153 @@ suite('Python Test Server', () => { server.dispose(); }); + testCases.forEach((testCase) => { + test(`run correctly`, async () => { + const testCaseDataObj: DataWithPayloadChunks = testCase.val(); + let eventData = ''; + const client = new net.Socket(); + + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output2 = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { + client.connect(server.getPort()); + return { + proc: mockProc, + out: output2, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + const uuid = server.createUUID(); + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid, + }; + + const dataWithPayloadChunks = testCaseDataObj; + + await server.serverReady(); + + server.onRunDataReceived(({ data }) => { + try { + const resultData = JSON.parse(data).result; + eventData = eventData + JSON.stringify(resultData); + } catch (e) { + assert(false, 'Error parsing data'); + } + deferred.resolve(); + }); + client.on('connect', () => { + traceLog('Socket connected, local port:', client.localPort); + // since this test is a single payload as a single chunk there should be a single line in the payload. + for (const line of dataWithPayloadChunks.payloadArray) { + client.write(line); + } + client.end(); + }); + client.on('error', (error) => { + traceLog('Socket connection error:', error); + }); + + server.sendCommand(options); + await deferred.promise; + const expectedResult = dataWithPayloadChunks.data; + assert.deepStrictEqual(eventData, expectedResult); + }); + }); +}); + +suite('Python Test Server, Send command etc', () => { + const FAKE_UUID = 'fake-uuid'; + let server: PythonTestServer; + let v4Stub: sinon.SinonStub; + let debugLauncher: ITestDebugLauncher; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock; + let deferred: Deferred; + const sandbox = sinon.createSandbox(); + + setup(async () => { + // set up test command options + + v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + execService = typeMoq.Mock.ofType(); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + }); + + teardown(() => { + sandbox.restore(); + server.dispose(); + }); test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { - const options = { - command: { - script: 'myscript', - args: ['-foo', 'foo'], - }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - const expectedSpawnOptions = { - cwd: '/foo/bar', - outputChannel: undefined, - token: undefined, - throwOnStdErr: true, - extraVariables: { - PYTHONPATH: '/foo/bar', - RUN_TEST_IDS_PORT: '56789', - }, - } as SpawnOptions; const deferred2 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); + const RUN_TEST_IDS_PORT_CONST = '5678'; + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_args, options2) => { + try { + assert.strictEqual( + options2.extraVariables.PYTHONPATH, + '/foo/bar', + 'Expect python path to exist as extra variable and be set correctly', + ); + assert.strictEqual( + options2.extraVariables.RUN_TEST_IDS_PORT, + RUN_TEST_IDS_PORT_CONST, + 'Expect test id port to be in extra variables and set correctly', + ); + } catch (e) { + assert(false, 'Error parsing data, extra variables do not match'); + } + return typeMoq.Mock.ofType>().object; + }); + const execFactory = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => { deferred2.resolve(); return Promise.resolve(execService.object); }); - server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - - server.sendCommand(options, '56789'); + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: FAKE_UUID, + }; + server.sendCommand(options, RUN_TEST_IDS_PORT_CONST); // add in await and trigger await deferred2.promise; mockProc.trigger('close'); const port = server.getPort(); - const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']; - execService.verify((x) => x.execObservable(expectedArgs, expectedSpawnOptions), typeMoq.Times.once()); + const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', FAKE_UUID, '-foo', 'foo']; + execService.verify((x) => x.execObservable(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); }); test('sendCommand should write to an output channel if it is provided as an option', async () => { - const output: string[] = []; + const output2: string[] = []; const outChannel = { appendLine: (str: string) => { - output.push(str); + output2.push(str); }, } as OutputChannel; const options = { @@ -126,11 +234,11 @@ suite('Python Test Server', () => { }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', - uuid: fakeUuid, + uuid: FAKE_UUID, outChannel, }; deferred = createDeferred(); - execFactory = typeMoq.Mock.ofType(); + const execFactory = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => { @@ -147,329 +255,49 @@ suite('Python Test Server', () => { mockProc.trigger('close'); const port = server.getPort(); - const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); + const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', FAKE_UUID, '-foo', 'foo'].join(' '); - assert.deepStrictEqual(output, [expected]); + assert.deepStrictEqual(output2, [expected]); }); test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { let eventData: { status: string; errors: string[] } | undefined; - stubExecutionService = ({ - execObservable: () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + const stubExecutionService = typeMoq.Mock.ofType(); + stubExecutionService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + stubExecutionService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred3.resolve(); throw new Error('Failed to execute'); - }, - } as unknown) as IPythonExecutionService; + }); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', - uuid: fakeUuid, + uuid: FAKE_UUID, }; + const stubExecutionFactory = typeMoq.Mock.ofType(); + stubExecutionFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(stubExecutionService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(stubExecutionFactory.object, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = JSON.parse(data); }); - await server.sendCommand(options); - + server.sendCommand(options); + await deferred2.promise; + await deferred3.promise; assert.notEqual(eventData, undefined); assert.deepStrictEqual(eventData?.status, 'error'); assert.deepStrictEqual(eventData?.errors, ['Failed to execute']); }); - - test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - - deferred = createDeferred(); - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('malformed data'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - // add in await and trigger - await deferred.promise; - mockProc.trigger('close'); - - assert.deepStrictEqual(eventData, ''); - }); - - test('If the server doesnt recognize the UUID it should ignore it', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('{"Request-uuid": "unknown-uuid"}'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, ''); - }); - - // required to have "tests" or "results" - // the heading length not being equal and yes being equal - // multiple payloads - test('Error if payload does not have a content length header', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('{"not content length": "5"}'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, ''); - }); - - const testData = [ - { - testName: 'fires discovery correctly on test payload', - payload: `Content-Length: 52 -Content-Type: application/json -Request-uuid: UUID_HERE - -{"cwd": "path", "status": "success", "tests": "xyz"}`, - expectedResult: '{"cwd": "path", "status": "success", "tests": "xyz"}', - }, - // Add more test data as needed - ]; - - testData.forEach(({ testName, payload, expectedResult }) => { - test(`test: ${testName}`, async () => { - // Your test logic here - let eventData: string | undefined; - const client = new net.Socket(); - - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - const uuid = server.createUUID(); - payload = payload.replace('UUID_HERE', uuid); - server.onDiscoveryDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write(payload); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, expectedResult); - }); - }); - - test('Calls run resolver if the result header is in the payload', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - await server.serverReady(); - const uuid = server.createUUID(); - server.onRunDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - const payload = `Content-Length: 87 -Content-Type: application/json -Request-uuid: ${uuid} - -{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}`; - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write(payload); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - const expectedResult = - '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; - assert.deepStrictEqual(eventData, expectedResult); - }); }); diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts new file mode 100644 index 000000000000..2aaffdda41df --- /dev/null +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -0,0 +1,350 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationTokenSource, TestRun, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Observable } from 'rxjs'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../../client/common/process/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { ITestServer } from '../../../client/testing/testController/common/types'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; +import * as util from '../../../client/testing/testController/common/utils'; + +suite('Execution Flow Run Adapters', () => { + let testServer: typeMoq.IMock; + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType(); + let adapter: PytestTestExecutionAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let debugLauncher: typeMoq.IMock; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsStartServerStub: sinon.SinonStub; + + setup(() => { + testServer = typeMoq.Mock.ofType(); + testServer.setup((t) => t.getPort()).returns(() => 12345); + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // mock out the result resolver + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execFactory = typeMoq.Mock.ofType(); + utilsStartServerStub = sinon.stub(util, 'startTestIdServer'); + debugLauncher = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + }); + teardown(() => { + sinon.restore(); + }); + test('PYTEST cancelation token called mid-run resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + execServiceMock + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc, + out: typeMoq.Mock.ofType>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token + const deferredEOT = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); + // set up test server + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + adapter = new PytestTestExecutionAdapter( + testServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await adapter.runTests( + Uri.file(myTestPath), + [], + false, + testRunMock.object, + execFactoryMock.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('PYTEST cancelation token called mid-debug resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async () => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token + const deferredEOT = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); + // set up test server + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + adapter = new PytestTestExecutionAdapter( + testServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await adapter.runTests( + Uri.file(myTestPath), + [], + true, + testRunMock.object, + execFactoryMock.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('UNITTEST cancelation token called mid-run resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // Stub send command to then have token canceled + const stubTestServer = typeMoq.Mock.ofType(); + stubTestServer + .setup((t) => + t.sendCommand( + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + ) + .returns(() => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + + stubTestServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + stubTestServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + execServiceMock + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc, + out: typeMoq.Mock.ofType>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token + const deferredEOT = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); + // set up test server + const unittestAdapter = new UnittestTestExecutionAdapter( + stubTestServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await unittestAdapter.runTests(Uri.file(myTestPath), [], false, testRunMock.object); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + stubTestServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('UNITTEST cancelation token called mid-debug resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // Stub send command to then have token canceled + const stubTestServer = typeMoq.Mock.ofType(); + stubTestServer + .setup((t) => + t.sendCommand( + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + ) + .returns(() => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + + stubTestServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + stubTestServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async () => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token + const deferredEOT = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); + // set up test server + const unittestAdapter = new UnittestTestExecutionAdapter( + stubTestServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await unittestAdapter.runTests(Uri.file(myTestPath), [], false, testRunMock.object); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + stubTestServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); +}); diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts index d971c7d37c9f..9168abc7041f 100644 --- a/src/test/testing/testController/utils.unit.test.ts +++ b/src/test/testing/testController/utils.unit.test.ts @@ -6,15 +6,15 @@ import { JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER, JSONRPC_UUID_HEADER, - jsonRPCContent, - jsonRPCHeaders, + ExtractJsonRPCData, + parseJsonRPCHeadersAndData, } from '../../../client/testing/testController/common/utils'; suite('Test Controller Utils: JSON RPC', () => { test('Empty raw data string', async () => { const rawDataString = ''; - const output = jsonRPCHeaders(rawDataString); + const output = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(output.headers.size, 0); assert.deepStrictEqual(output.remainingRawData, ''); }); @@ -22,20 +22,20 @@ suite('Test Controller Utils: JSON RPC', () => { test('Valid data empty JSON', async () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 2\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n{}`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, '{}'); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, '{}'); }); test('Valid data NO JSON', async () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 0\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, ''); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, ''); }); @@ -45,10 +45,10 @@ suite('Test Controller Utils: JSON RPC', () => { '{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}'; const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, json); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, json); }); @@ -58,9 +58,9 @@ suite('Test Controller Utils: JSON RPC', () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; const rawDataString2 = rawDataString + rawDataString; - const rpcHeaders = jsonRPCHeaders(rawDataString2); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString2); assert.deepStrictEqual(rpcHeaders.headers.size, 3); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, json); assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString); }); diff --git a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py index 8c6a29adf495..a76856ebb929 100644 --- a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py +++ b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py @@ -4,13 +4,13 @@ import unittest -@pytest.mark.parametrize("num", range(0, 200)) +@pytest.mark.parametrize("num", range(0, 2000)) def test_odd_even(num): assert num % 2 == 0 class NumbersTest(unittest.TestCase): def test_even(self): - for i in range(0, 200): + for i in range(0, 2000): with self.subTest(i=i): self.assertEqual(i % 2, 0) From a9d4df919d0039dc3e8006d5eb6c1b09fe1a9273 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 18 Sep 2023 01:16:31 -0700 Subject: [PATCH 275/336] Allow publish of pre-releases to VS Code stable this iteration (#22009) https://github.com/microsoft/vscode-python/pull/21997#discussion_r1327940466 For https://github.com/microsoft/vscode-python/issues/22005 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98f97c4a1905..8ce90a2a487e 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.83.0-20230915" + "vscode": "^1.82.0" }, "enableTelemetry": false, "keywords": [ From 7291d303078d6adbf367160781673c0f697f9487 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 18 Sep 2023 10:43:15 -0700 Subject: [PATCH 276/336] Remove repo labels corresponding to removing unrecognized label workflow (#22022) Workflow has been removed: https://github.com/microsoft/vscode-github-triage-actions/pull/188 --- .github/workflows/getLabels.js | 25 ------------------------- .github/workflows/issue-labels.yml | 7 ++----- .vscode/settings.json | 8 +++----- 3 files changed, 5 insertions(+), 35 deletions(-) delete mode 100644 .github/workflows/getLabels.js diff --git a/.github/workflows/getLabels.js b/.github/workflows/getLabels.js deleted file mode 100644 index 99060e7205eb..000000000000 --- a/.github/workflows/getLabels.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * To run this file: - * * npm install @octokit/rest - * * node .github/workflows/getLabels.js - * - * This script assumes the maximum number of labels to be 100. - */ - -const { Octokit } = require('@octokit/rest'); -const github = new Octokit(); -github.rest.issues - .listLabelsForRepo({ - owner: 'microsoft', - repo: 'vscode-python', - per_page: 100, - }) - .then((result) => { - const labels = result.data.map((label) => label.name); - console.log( - '\nNumber of labels found:', - labels.length, - ", verify that it's the same as number of labels listed in https://github.com/microsoft/vscode-python/labels\n", - ); - console.log(JSON.stringify(labels), '\n'); - }); diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index 7817ed62bd9b..8b084aef409f 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -5,8 +5,6 @@ on: types: [opened, reopened] env: - # To update the list of labels, see `getLabels.js`. - REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-repl","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd","anthonykim1"]' permissions: @@ -15,7 +13,7 @@ permissions: jobs: # From https://github.com/marketplace/actions/github-script#apply-a-label-to-an-issue. add-classify-label: - name: "Add 'triage-needed' and remove unrecognizable labels & assignees" + name: "Add 'triage-needed' and remove assignees" runs-on: ubuntu-latest steps: - name: Checkout Actions @@ -28,9 +26,8 @@ jobs: - name: Install Actions run: npm install --production --prefix ./actions - - name: "Add 'triage-needed' and remove unrecognizable labels & assignees" + - name: "Add 'triage-needed' and remove assignees" uses: ./actions/python-issue-labels with: triagers: ${{ env.TRIAGERS }} token: ${{secrets.GITHUB_TOKEN}} - repo_labels: ${{ env.REPO_LABELS }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 06011b3d13cd..a5dbb4869fd9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -67,12 +67,10 @@ "typescript.preferences.importModuleSpecifier": "relative", "debug.javascript.usePreview": false, // Branch name suggestion. + "git.branchProtectionPrompt": "alwaysCommitToNewBranch", "git.branchRandomName.enable": true, - "git.branchProtection": [ - "main", - "release/*" - ], + "git.branchProtection": ["main", "release/*"], "git.pullBeforeCheckout": true, // Open merge editor for resolving conflicts. - "git.mergeEditor": true, + "git.mergeEditor": true } From 2df331be969635bf8c425b5e26c01858561b7ed2 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 18 Sep 2023 11:20:17 -0700 Subject: [PATCH 277/336] switch | to unions to be 3.8 compatible (#22025) fixes https://github.com/microsoft/vscode-python/issues/22020 --- pythonFiles/vscode_pytest/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 21165a02bf4b..f5827f87e1b4 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -609,7 +609,7 @@ class ExecutionPayloadDict(Dict): class EOTPayloadDict(TypedDict): """A dictionary that is used to send a end of transmission post request to the server.""" - command_type: Literal["discovery"] | Literal["execution"] + command_type: Union[Literal["discovery"], Literal["execution"]] eot: bool @@ -672,7 +672,7 @@ def default(self, obj): def send_post_request( - payload: ExecutionPayloadDict | DiscoveryPayloadDict | EOTPayloadDict, + payload: Union[ExecutionPayloadDict, DiscoveryPayloadDict, EOTPayloadDict], cls_encoder=None, ): """ From 05ae2660c10ca8f94cdddc3c50aea18eba853eaa Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 18 Sep 2023 12:03:04 -0700 Subject: [PATCH 278/336] Add python 3.8 and 3x specific runs (#22023) Closes https://github.com/microsoft/vscode-python/issues/22024 --- .github/workflows/build.yml | 46 +++++++++++++++++++--- .github/workflows/pr-check.yml | 50 +++++++++++++++++++----- pythonFiles/unittestadapter/discovery.py | 6 ++- pythonFiles/unittestadapter/execution.py | 6 ++- 4 files changed, 88 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 873b81fa66f8..3d121564d385 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -106,7 +106,45 @@ jobs: version: 1.1.308 working-directory: 'pythonFiles' - ### Non-smoke tests + python-tests: + name: Python Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. + os: [ubuntu-latest, windows-latest] + # Run the tests on the oldest and most recent versions of Python. + python: ['3.8', '3.x'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: ${{ env.special-working-directory-relative }} + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@v1 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Python unit tests + run: python pythonFiles/tests/run_all.py + tests: name: Tests if: github.repository == 'microsoft/vscode-python' @@ -122,7 +160,7 @@ jobs: # and we assume that Ubuntu is enough to cover the UNIX case. os: [ubuntu-latest, windows-latest] python: ['3.x'] - test-suite: [ts-unit, python-unit, venv, single-workspace, multi-workspace, debugger, functional] + test-suite: [ts-unit, venv, single-workspace, multi-workspace, debugger, functional] steps: - name: Checkout uses: actions/checkout@v4 @@ -268,10 +306,6 @@ jobs: run: npm run test:unittests if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, '3.') - - name: Run Python unit tests - run: python pythonFiles/tests/run_all.py - if: matrix.test-suite == 'python-unit' - # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, # which is set in the "Prepare environment for venv tests" step. diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index c7335702e991..aa223782de62 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -80,7 +80,45 @@ jobs: version: 1.1.308 working-directory: 'pythonFiles' - ### Non-smoke tests + python-tests: + name: Python Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. + os: [ubuntu-latest, windows-latest] + # Run the tests on the oldest and most recent versions of Python. + python: ['3.8', '3.x'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: ${{ env.special-working-directory-relative }} + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@v1 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Python unit tests + run: python pythonFiles/tests/run_all.py + tests: name: Tests # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. @@ -96,7 +134,7 @@ jobs: os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. python: ['3.x'] - test-suite: [ts-unit, python-unit, venv, single-workspace, debugger, functional] + test-suite: [ts-unit, venv, single-workspace, debugger, functional] steps: - name: Checkout @@ -139,14 +177,12 @@ jobs: with: requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) - name: Install Jedi requirements uses: brettcannon/pip-secure-install@v1 with: requirements-file: '"${{ env.special-working-directory-relative }}/pythonFiles/jedilsp_requirements/requirements.txt"' options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/jedilsp" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt @@ -243,12 +279,6 @@ jobs: run: npm run test:unittests if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, 3.) - # Run the Python tests in our codebase. - - name: Run Python unit tests - run: | - python pythonFiles/tests/run_all.py - if: matrix.test-suite == 'python-unit' - # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, # which is set in the "Prepare environment for venv tests" step. diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index 6208d24bee9f..69c14cae34e6 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -51,7 +51,7 @@ class PayloadDict(TypedDict): class EOTPayloadDict(TypedDict): """A dictionary that is used to send a end of transmission post request to the server.""" - command_type: Literal["discovery"] | Literal["execution"] + command_type: Union[Literal["discovery"], Literal["execution"]] eot: bool @@ -113,7 +113,9 @@ def discover_tests( return payload -def post_response(payload: PayloadDict | EOTPayloadDict, port: int, uuid: str) -> None: +def post_response( + payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str +) -> None: # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) data = json.dumps(payload) diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 7bbc97e78f31..a208056c6682 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -172,7 +172,7 @@ class PayloadDict(TypedDict): class EOTPayloadDict(TypedDict): """A dictionary that is used to send a end of transmission post request to the server.""" - command_type: Literal["discovery"] | Literal["execution"] + command_type: Union[Literal["discovery"], Literal["execution"]] eot: bool @@ -250,7 +250,9 @@ def send_run_data(raw_data, port, uuid): post_response(payload, port, uuid) -def post_response(payload: PayloadDict | EOTPayloadDict, port: int, uuid: str) -> None: +def post_response( + payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str +) -> None: # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) global __socket From b41fee72564e569808fb617328b58b0364d1995a Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 18 Sep 2023 15:11:01 -0700 Subject: [PATCH 279/336] Remove old linter and formatter prompts and commands (#21979) --- package.json | 33 -- package.nls.json | 3 - src/client/common/application/commands.ts | 3 - src/client/common/constants.ts | 3 - .../common/installer/productInstaller.ts | 162 +--------- src/client/extensionActivation.ts | 3 - .../linters/errorHandlers/errorHandler.ts | 6 +- .../linters/errorHandlers/notInstalled.ts | 29 -- src/client/linters/errorHandlers/standard.ts | 18 ++ src/client/linters/linterCommands.ts | 114 ------- src/test/common/installer.test.ts | 12 +- .../installer.invalidPath.unit.test.ts | 5 +- .../common/installer/installer.unit.test.ts | 297 +----------------- .../installer/productInstaller.unit.test.ts | 264 +--------------- .../common/installer/productPath.unit.test.ts | 7 +- src/test/common/productsToTest.ts | 25 ++ src/test/linters/lint.multilinter.test.ts | 126 -------- src/test/linters/linterCommands.unit.test.ts | 182 ----------- 18 files changed, 64 insertions(+), 1228 deletions(-) delete mode 100644 src/client/linters/errorHandlers/notInstalled.ts delete mode 100644 src/client/linters/linterCommands.ts create mode 100644 src/test/common/productsToTest.ts delete mode 100644 src/test/linters/lint.multilinter.test.ts delete mode 100644 src/test/linters/linterCommands.unit.test.ts diff --git a/package.json b/package.json index 8ce90a2a487e..9cf564122d7b 100644 --- a/package.json +++ b/package.json @@ -364,11 +364,6 @@ "command": "python.createEnvironment-button", "title": "%python.command.python.createEnvironment.title%" }, - { - "category": "Python", - "command": "python.enableLinting", - "title": "%python.command.python.enableLinting.title%" - }, { "category": "Python", "command": "python.enableSourceMapSupport", @@ -430,21 +425,11 @@ "icon": "$(run-errors)", "title": "%python.command.testing.rerunFailedTests.title%" }, - { - "category": "Python", - "command": "python.runLinting", - "title": "%python.command.python.runLinting.title%" - }, { "category": "Python", "command": "python.setInterpreter", "title": "%python.command.python.setInterpreter.title%" }, - { - "category": "Python", - "command": "python.setLinter", - "title": "%python.command.python.setLinter.title%" - }, { "category": "Python Refactor", "command": "python.sortImports", @@ -1807,12 +1792,6 @@ "title": "%python.command.python.createTerminal.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.enableLinting", - "title": "%python.command.python.enableLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.enableSourceMapSupport", @@ -1885,24 +1864,12 @@ "title": "%python.command.testing.rerunFailedTests.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.runLinting", - "title": "%python.command.python.runLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.setInterpreter", "title": "%python.command.python.setInterpreter.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.setLinter", - "title": "%python.command.python.setLinter.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python Refactor", "command": "python.sortImports", diff --git a/package.nls.json b/package.nls.json index b8e82b150e76..d260c98b0b5a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -18,9 +18,6 @@ "python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal", "python.command.python.execSelectionInDjangoShell.title": "Run Selection/Line in Django Shell", "python.command.python.reportIssue.title": "Report Issue...", - "python.command.python.setLinter.title": "Select Linter", - "python.command.python.enableLinting.title": "Enable/Disable Linting", - "python.command.python.runLinting.title": "Run Linting", "python.command.python.enableSourceMapSupport.title": "Enable Source Map Support For Extension Debugging", "python.command.python.clearCacheAndReload.title": "Clear Cache and Reload Window", "python.command.python.analysis.restartLanguageServer.title": "Restart Language Server", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 2a4404440101..d8944fe2b057 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -23,8 +23,6 @@ interface ICommandNameWithoutArgumentTypeMapping { [Commands.ClearWorkspaceInterpreter]: []; [Commands.Set_Interpreter]: []; [Commands.Set_ShebangInterpreter]: []; - [Commands.Run_Linter]: []; - [Commands.Enable_Linter]: []; ['workbench.action.showCommands']: []; ['workbench.action.debug.continue']: []; ['workbench.action.debug.stepOver']: []; @@ -35,7 +33,6 @@ interface ICommandNameWithoutArgumentTypeMapping { ['editor.action.formatDocument']: []; ['editor.action.rename']: []; [Commands.ViewOutput]: []; - [Commands.Set_Linter]: []; [Commands.Start_REPL]: []; [Commands.Enable_SourceMap_Support]: []; [Commands.Exec_Selection_In_Terminal]: []; diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index bea0ef9e235c..f18fd20bb3dc 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -40,7 +40,6 @@ export namespace Commands { export const Create_Environment_Button = 'python.createEnvironment-button'; export const Create_Terminal = 'python.createTerminal'; export const Debug_In_Terminal = 'python.debugInTerminal'; - export const Enable_Linter = 'python.enableLinting'; export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; @@ -56,9 +55,7 @@ export namespace Commands { export const PickLocalProcess = 'python.pickLocalProcess'; export const RefreshTensorBoard = 'python.refreshTensorBoard'; export const ReportIssue = 'python.reportIssue'; - export const Run_Linter = 'python.runLinting'; export const Set_Interpreter = 'python.setInterpreter'; - export const Set_Linter = 'python.setLinter'; export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; export const Sort_Imports = 'python.sortImports'; export const Start_REPL = 'python.startREPL'; diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 526369f9e9ad..fba860aaa383 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -6,12 +6,10 @@ import { CancellationToken, l10n, Uri } from 'vscode'; import '../extensions'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { LinterId } from '../../linters/types'; import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../application/types'; -import { Commands } from '../constants'; +import { IApplicationShell, IWorkspaceService } from '../application/types'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types'; import { IConfigurationService, @@ -22,7 +20,7 @@ import { Product, ProductType, } from '../types'; -import { Common, Linters } from '../utils/localize'; +import { Common } from '../utils/localize'; import { isResource, noop } from '../utils/misc'; import { translateProductToModule } from './moduleInstaller'; import { ProductNames } from './productNames'; @@ -225,158 +223,6 @@ abstract class BaseInstaller implements IBaseInstaller { } } -const doNotDisplayFormatterPromptStateKey = 'FORMATTER_NOT_INSTALLED_KEY'; - -export class FormatterInstaller extends BaseInstaller { - protected async promptToInstallImplementation( - product: Product, - resource?: Uri, - cancel?: CancellationToken, - _flags?: ModuleInstallFlags, - ): Promise { - const neverShowAgain = this.persistentStateFactory.createGlobalPersistentState( - doNotDisplayFormatterPromptStateKey, - false, - ); - - if (neverShowAgain.value) { - return InstallerResponse.Ignore; - } - - // Hard-coded on purpose because the UI won't necessarily work having - // another formatter. - const formatters = [Product.autopep8, Product.black, Product.yapf]; - const formatterNames = formatters.map((formatter) => ProductNames.get(formatter)!); - const productName = ProductNames.get(product)!; - formatterNames.splice(formatterNames.indexOf(productName), 1); - const useOptions = formatterNames.map((name) => l10n.t('Use {0}', name)); - const yesChoice = Common.bannerLabelYes; - - const options = [...useOptions, Common.doNotShowAgain]; - let message = l10n.t('Formatter {0} is not installed. Install?', productName); - if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, yesChoice); - } else { - const executable = this.getExecutableNameFromSettings(product, resource); - message = l10n.t('Path to the {0} formatter is invalid ({1})', productName, executable); - } - - const item = await this.appShell.showErrorMessage(message, ...options); - if (item === yesChoice) { - return this.install(product, resource, cancel); - } - - if (item === Common.doNotShowAgain) { - neverShowAgain.updateValue(true); - return InstallerResponse.Ignore; - } - - if (typeof item === 'string') { - for (const formatter of formatters) { - const formatterName = ProductNames.get(formatter)!; - - if (item.endsWith(formatterName)) { - await this.configService.updateSetting('formatting.provider', formatterName, resource); - return this.install(formatter, resource, cancel); - } - } - } - - return InstallerResponse.Ignore; - } -} - -export class LinterInstaller extends BaseInstaller { - constructor(protected serviceContainer: IServiceContainer) { - super(serviceContainer); - } - - protected async promptToInstallImplementation( - product: Product, - resource?: Uri, - cancel?: CancellationToken, - _flags?: ModuleInstallFlags, - ): Promise { - return this.oldPromptForInstallation(product, resource, cancel); - } - - /** - * For installers that want to avoid prompting the user over and over, they can make use of a - * persisted true/false value representing user responses to 'stop showing this prompt'. This method - * gets the persisted value given the installer-defined key. - * - * @param key Key to use to get a persisted response value, each installer must define this for themselves. - * @returns Boolean: The current state of the stored response key given. - */ - protected getStoredResponse(key: string): boolean { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = factory.createGlobalPersistentState(key, undefined); - return state.value === true; - } - - private async oldPromptForInstallation(product: Product, resource?: Uri, cancel?: CancellationToken) { - const productName = ProductNames.get(product)!; - const { install } = Common; - const { doNotShowAgain } = Common; - const disableLinterInstallPromptKey = `${productName}_DisableLinterInstallPrompt`; - const { selectLinter } = Linters; - - if (this.getStoredResponse(disableLinterInstallPromptKey) === true) { - return InstallerResponse.Ignore; - } - - const options = [selectLinter, doNotShowAgain]; - - let message = l10n.t('Linter {0} is not installed.', productName); - if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, install); - } else { - const executable = this.getExecutableNameFromSettings(product, resource); - message = l10n.t('Path to the {0} linter is invalid ({1})', productName, executable); - } - const response = await this.appShell.showErrorMessage(message, ...options); - if (response === install) { - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { - tool: productName as LinterId, - action: 'install', - }); - return this.install(product, resource, cancel); - } - if (response === doNotShowAgain) { - await this.setStoredResponse(disableLinterInstallPromptKey, true); - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { - tool: productName as LinterId, - action: 'disablePrompt', - }); - return InstallerResponse.Ignore; - } - - if (response === selectLinter) { - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select' }); - const commandManager = this.serviceContainer.get(ICommandManager); - await commandManager.executeCommand(Commands.Set_Linter); - } - return InstallerResponse.Ignore; - } - - /** - * For installers that want to avoid prompting the user over and over, they can make use of a - * persisted true/false value representing user responses to 'stop showing this prompt'. This - * method will set that persisted value given the installer-defined key. - * - * @param key Key to use to get a persisted response value, each installer must define this for themselves. - * @param value Boolean value to store for the user - if they choose to not be prompted again for instance. - * @returns Boolean: The current state of the stored response key given. - */ - private async setStoredResponse(key: string, value: boolean): Promise { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = factory.createGlobalPersistentState(key, undefined); - if (state && state.value !== value) { - await state.updateValue(value); - } - } -} - export class TestFrameworkInstaller extends BaseInstaller { protected async promptToInstallImplementation( product: Product, @@ -687,10 +533,6 @@ export class ProductInstaller implements IInstaller { private createInstaller(product: Product): IBaseInstaller { const productType = this.productService.getProductType(product); switch (productType) { - case ProductType.Formatter: - return new FormatterInstaller(this.serviceContainer); - case ProductType.Linter: - return new LinterInstaller(this.serviceContainer); case ProductType.TestFramework: return new TestFrameworkInstaller(this.serviceContainer); case ProductType.DataScience: diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 8dcea063676a..c82bddef3c20 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -28,7 +28,6 @@ import { IDebugConfigurationService, IDynamicDebugConfigurationService } from '. import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; -import { LinterCommands } from './linters/linterCommands'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; import { PythonFormattingEditProvider } from './providers/formatProvider'; import { ReplProvider } from './providers/replProvider'; @@ -168,8 +167,6 @@ async function activateLegacy(ext: ExtensionState): Promise { serviceManager.get(ICodeExecutionManager).registerCommands(); - disposables.push(new LinterCommands(serviceManager)); - if ( pythonSettings && pythonSettings.formatting && diff --git a/src/client/linters/errorHandlers/errorHandler.ts b/src/client/linters/errorHandlers/errorHandler.ts index dc884e97739c..af28dd61c3a4 100644 --- a/src/client/linters/errorHandlers/errorHandler.ts +++ b/src/client/linters/errorHandlers/errorHandler.ts @@ -3,17 +3,13 @@ import { ExecutionInfo, Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { IErrorHandler } from '../types'; import { BaseErrorHandler } from './baseErrorHandler'; -import { NotInstalledErrorHandler } from './notInstalled'; import { StandardErrorHandler } from './standard'; export class ErrorHandler implements IErrorHandler { private handler: BaseErrorHandler; constructor(product: Product, serviceContainer: IServiceContainer) { - // Create chain of handlers. - const standardErrorHandler = new StandardErrorHandler(product, serviceContainer); - this.handler = new NotInstalledErrorHandler(product, serviceContainer); - this.handler.setNextHandler(standardErrorHandler); + this.handler = new StandardErrorHandler(product, serviceContainer); } public handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts deleted file mode 100644 index 8c598ae5ece2..000000000000 --- a/src/client/linters/errorHandlers/notInstalled.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Uri } from 'vscode'; -import { IPythonExecutionFactory } from '../../common/process/types'; -import { ExecutionInfo } from '../../common/types'; -import { traceError, traceLog, traceWarn } from '../../logging'; -import { ILinterManager } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; - -export class NotInstalledErrorHandler extends BaseErrorHandler { - public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - const pythonExecutionService = await this.serviceContainer - .get(IPythonExecutionFactory) - .create({ resource }); - const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); - if (isModuleInstalled) { - return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : false; - } - - this.installer - .promptToInstall(this.product, resource) - .catch((ex) => traceError('NotInstalledErrorHandler.promptToInstall', ex)); - - const linterManager = this.serviceContainer.get(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - const customError = `Linter '${info.id}' is not installed. Please install it or select another linter".`; - traceLog(`\n${customError}\n${error}`); - traceWarn(customError, error); - return true; - } -} diff --git a/src/client/linters/errorHandlers/standard.ts b/src/client/linters/errorHandlers/standard.ts index f6e04b50ff19..6367da7abe4a 100644 --- a/src/client/linters/errorHandlers/standard.ts +++ b/src/client/linters/errorHandlers/standard.ts @@ -18,6 +18,24 @@ export class StandardErrorHandler extends BaseErrorHandler { const info = linterManager.getLinterInfo(execInfo.product!); traceError(`There was an error in running the linter ${info.id}`, error); + if (info.id === LinterId.PyLint) { + traceError('Support for "pylint" is moved to ms-python.pylint extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.pylint', + ); + } else if (info.id === LinterId.Flake8) { + traceError('Support for "flake8" is moved to ms-python.flake8 extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.flake8', + ); + } else if (info.id === LinterId.MyPy) { + traceError('Support for "mypy" is moved to ms-python.mypy-type-checker extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker', + ); + } + traceError(`If the error is due to missing ${info.id}, please install ${info.id} using pip manually.`); + traceError('Learn more here: https://aka.ms/AAlgvkb'); traceLog(`Linting with ${info.id} failed.`); traceLog(error.toString()); diff --git a/src/client/linters/linterCommands.ts b/src/client/linters/linterCommands.ts deleted file mode 100644 index cc35e80f26b1..000000000000 --- a/src/client/linters/linterCommands.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DiagnosticCollection, Disposable, l10n, QuickPickOptions, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { IDisposable } from '../common/types'; -import { Common } from '../common/utils/localize'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { ILinterManager, ILintingEngine, LinterId } from './types'; - -export class LinterCommands implements IDisposable { - private disposables: Disposable[] = []; - - private linterManager: ILinterManager; - - private readonly appShell: IApplicationShell; - - private readonly documentManager: IDocumentManager; - - constructor(private serviceContainer: IServiceContainer) { - this.linterManager = this.serviceContainer.get(ILinterManager); - this.appShell = this.serviceContainer.get(IApplicationShell); - this.documentManager = this.serviceContainer.get(IDocumentManager); - - const commandManager = this.serviceContainer.get(ICommandManager); - commandManager.registerCommand(Commands.Set_Linter, this.setLinterAsync.bind(this)); - commandManager.registerCommand(Commands.Enable_Linter, this.enableLintingAsync.bind(this)); - commandManager.registerCommand(Commands.Run_Linter, this.runLinting.bind(this)); - } - - public dispose(): void { - this.disposables.forEach((disposable) => disposable.dispose()); - } - - public async setLinterAsync(): Promise { - const linters = this.linterManager.getAllLinterInfos(); - const suggestions = linters.map((x) => x.id).sort(); - const linterList = ['Disable Linting', ...suggestions]; - const activeLinters = await this.linterManager.getActiveLinters(this.settingsUri); - - let current: string; - switch (activeLinters.length) { - case 0: - current = 'none'; - break; - case 1: - current = activeLinters[0].id; - break; - default: - current = 'multiple selected'; - break; - } - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}`, - }; - - const selection = await this.appShell.showQuickPick(linterList, quickPickOptions); - if (selection !== undefined) { - if (selection === 'Disable Linting') { - await this.linterManager.enableLintingAsync(false); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { enabled: false }); - } else { - const index = linters.findIndex((x) => x.id === selection); - if (activeLinters.length > 1) { - const response = await this.appShell.showWarningMessage( - l10n.t("Multiple linters are enabled in settings. Replace with '{0}'?", selection), - Common.bannerLabelYes, - Common.bannerLabelNo, - ); - if (response !== Common.bannerLabelYes) { - return; - } - } - await this.linterManager.setActiveLintersAsync([linters[index].product], this.settingsUri); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { tool: selection as LinterId, enabled: true }); - } - } - } - - public async enableLintingAsync(): Promise { - const options = ['Enable', 'Disable']; - const current = (await this.linterManager.isLintingEnabled(this.settingsUri)) ? options[0] : options[1]; - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}`, - }; - - const selection = await this.appShell.showQuickPick(options, quickPickOptions); - - if (selection !== undefined) { - const enable: boolean = selection === options[0]; - await this.linterManager.enableLintingAsync(enable, this.settingsUri); - } - } - - public runLinting(): Promise { - const engine = this.serviceContainer.get(ILintingEngine); - return engine.lintOpenPythonFiles('manual'); - } - - private get settingsUri(): Uri | undefined { - return this.documentManager.activeTextEditor ? this.documentManager.activeTextEditor.document.uri : undefined; - } -} diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 7ff0ee81c27f..5c1842a2c97c 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -87,7 +87,6 @@ import { ProductType, } from '../../client/common/types'; import { createDeferred } from '../../client/common/utils/async'; -import { getNamesAndValues } from '../../client/common/utils/enum'; import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; import { Random } from '../../client/common/utils/random'; import { ImportTracker } from '../../client/telemetry/importTracker'; @@ -105,6 +104,7 @@ import { } from '../../client/interpreter/configuration/types'; import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { getProductsForInstallerTests } from './productsToTest'; suite('Installer', () => { let ioc: UnitTestIocContainer; @@ -276,7 +276,8 @@ suite('Installer', () => { await installer.isInstalled(product, resource); await checkInstalledDef.promise; } - getNamesAndValues(Product).forEach((prod) => { + + getProductsForInstallerTests().forEach((prod) => { test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async function () { if ( new ProductService().getProductType(prod.value) === ProductType.DataScience || @@ -293,7 +294,7 @@ suite('Installer', () => { new MockModuleInstaller('two', true), ); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.isort) { + if (prod.value === Product.unittest) { return undefined; } await testCheckingIfProductIsInstalled(prod.value); @@ -316,7 +317,8 @@ suite('Installer', () => { await installer.install(product); await checkInstalledDef.promise; } - getNamesAndValues(Product).forEach((prod) => { + + getProductsForInstallerTests().forEach((prod) => { test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async function () { const productType = new ProductService().getProductType(prod.value); if (productType === ProductType.DataScience || productType === ProductType.Python) { @@ -331,7 +333,7 @@ suite('Installer', () => { new MockModuleInstaller('two', true), ); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.isort) { + if (prod.value === Product.unittest) { return undefined; } await testInstallingProduct(prod.value); diff --git a/src/test/common/installer/installer.invalidPath.unit.test.ts b/src/test/common/installer/installer.invalidPath.unit.test.ts index 7e8392204600..b6738759f0d7 100644 --- a/src/test/common/installer/installer.invalidPath.unit.test.ts +++ b/src/test/common/installer/installer.invalidPath.unit.test.ts @@ -14,10 +14,10 @@ import { ProductInstaller } from '../../../client/common/installer/productInstal import { ProductService } from '../../../client/common/installer/productService'; import { IProductPathService, IProductService } from '../../../client/common/installer/types'; import { IPersistentState, IPersistentStateFactory, Product, ProductType } from '../../../client/common/types'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); @@ -26,7 +26,7 @@ suite('Module Installer - Invalid Paths', () => { ['moduleName', path.join('users', 'dev', 'tool', 'executable')].forEach((pathToExecutable) => { const isExecutableAModule = path.basename(pathToExecutable) === pathToExecutable; - getNamesAndValues(Product).forEach((product) => { + getProductsForInstallerTests().forEach((product) => { let installer: ProductInstaller; let serviceContainer: TypeMoq.IMock; let app: TypeMoq.IMock; @@ -78,7 +78,6 @@ suite('Module Installer - Invalid Paths', () => { }); switch (product.value) { - case Product.isort: case Product.unittest: { return; } diff --git a/src/test/common/installer/installer.unit.test.ts b/src/test/common/installer/installer.unit.test.ts index 38b9d9174472..69a5f3678f69 100644 --- a/src/test/common/installer/installer.unit.test.ts +++ b/src/test/common/installer/installer.unit.test.ts @@ -6,24 +6,11 @@ import { assert, expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { Commands } from '../../../client/common/constants'; -import { ExperimentService } from '../../../client/common/experiments/service'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; import '../../../client/common/extensions'; -import { - FormatterInstaller, - LinterInstaller, - ProductInstaller, -} from '../../../client/common/installer/productInstaller'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { LinterProductPathService } from '../../../client/common/installer/productPath'; +import { ProductInstaller } from '../../../client/common/installer/productInstaller'; import { ProductService } from '../../../client/common/installer/productService'; import { IInstallationChannelManager, @@ -39,9 +26,7 @@ import { IPythonExecutionService, } from '../../../client/common/process/types'; import { - IConfigurationService, IDisposableRegistry, - IExperimentService, InstallerResponse, IPersistentState, IPersistentStateFactory, @@ -49,20 +34,17 @@ import { ProductType, } from '../../../client/common/types'; import { createDeferred, Deferred } from '../../../client/common/utils/async'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { ServiceContainer } from '../../../client/ioc/container'; import { IServiceContainer } from '../../../client/ioc/types'; -import { LinterManager } from '../../../client/linters/linterManager'; -import { ILinterManager } from '../../../client/linters/types'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { sleep } from '../../common'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); suite('Module Installer only', () => { [undefined, Uri.file('resource')].forEach((resource) => { - getNamesAndValues(Product) + getProductsForInstallerTests() .concat([{ name: 'Unknown product', value: 404 }]) .forEach((product) => { @@ -183,9 +165,6 @@ suite('Module Installer only', () => { }); return; } - case Product.isort: { - return; - } case Product.unittest: { test(`Ensure resource info is passed into the module installer ${product.name} (${ resource ? 'With a resource' : 'without a resource' @@ -638,273 +617,5 @@ suite('Module Installer only', () => { workspaceService.verifyAll(); }); }); - - suite('Test FormatterInstaller.promptToInstallImplementation', () => { - class FormatterInstallerTest extends FormatterInstaller { - public async promptToInstallImplementation(product: Product, uri?: Uri): Promise { - return super.promptToInstallImplementation(product, uri); - } - - // eslint-disable-next-line class-methods-use-this - protected getStoredResponse(_key: string) { - return false; - } - - // eslint-disable-next-line class-methods-use-this - protected isExecutableAModule(_product: Product, _resource?: Uri) { - return true; - } - } - let installer: FormatterInstallerTest; - let appShell: IApplicationShell; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - let productService: IProductService; - let cmdManager: ICommandManager; - setup(() => { - const serviceContainer = mock(ServiceContainer); - appShell = mock(ApplicationShell); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - productService = mock(ProductService); - cmdManager = mock(CommandManager); - - when(serviceContainer.get(IApplicationShell)).thenReturn(instance(appShell)); - when(serviceContainer.get(IConfigurationService)).thenReturn( - instance(configService), - ); - when(serviceContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get(IProductService)).thenReturn(instance(productService)); - when(serviceContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - - installer = new FormatterInstallerTest(instance(serviceContainer)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('If nothing is selected, return Ignore as response', async () => { - const product = Product.autopep8; - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn((undefined as unknown) as Thenable); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Ignore); - }); - - test('If `Yes` is selected, install product', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Yes' as unknown) as Thenable); - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - assert.ok(install.calledOnceWith(product, resource, undefined)); - }); - - test('If `Use black` is selected, install black formatter', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Use black' as unknown) as Thenable); - when(configService.updateSetting('formatting.provider', 'black', resource)).thenResolve(); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - verify(configService.updateSetting('formatting.provider', 'black', resource)).once(); - assert.ok(install.calledOnceWith(Product.black, resource, undefined)); - }); - - test('If `Use yapf` is selected, install black formatter', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Use yapf' as unknown) as Thenable); - when(configService.updateSetting('formatting.provider', 'yapf', resource)).thenResolve(); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - verify(configService.updateSetting('formatting.provider', 'yapf', resource)).once(); - assert.ok(install.calledOnceWith(Product.yapf, resource, undefined)); - }); - }); - }); -}); - -[undefined, Uri.file('resource')].forEach((resource) => { - suite(`Test LinterInstaller with resource: ${resource}`, () => { - class LinterInstallerTest extends LinterInstaller { - public isModuleExecutable = true; - - public async promptToInstallImplementation(product: Product, uri?: Uri): Promise { - return super.promptToInstallImplementation(product, uri); - } - - // eslint-disable-next-line class-methods-use-this - protected getStoredResponse(_key: string) { - return false; - } - - protected isExecutableAModule(_product: Product, _resource?: Uri) { - return this.isModuleExecutable; - } - } - - let installer: LinterInstallerTest; - let appShell: IApplicationShell; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - let productService: IProductService; - let cmdManager: ICommandManager; - let experimentsService: IExperimentService; - let linterManager: ILinterManager; - let serviceContainer: IServiceContainer; - let productPathService: IProductPathService; - setup(() => { - serviceContainer = mock(ServiceContainer); - appShell = mock(ApplicationShell); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - productService = mock(ProductService); - cmdManager = mock(CommandManager); - experimentsService = mock(ExperimentService); - linterManager = mock(LinterManager); - productPathService = mock(LinterProductPathService); - - when(serviceContainer.get(IApplicationShell)).thenReturn(instance(appShell)); - when(serviceContainer.get(IConfigurationService)).thenReturn( - instance(configService), - ); - when(serviceContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get(IProductService)).thenReturn(instance(productService)); - when(serviceContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - - const exp = instance(experimentsService); - when(serviceContainer.get(IExperimentService)).thenReturn(exp); - when(experimentsService.inExperiment(anything())).thenResolve(false); - - when(serviceContainer.get(ILinterManager)).thenReturn(instance(linterManager)); - when(serviceContainer.get(IProductPathService, ProductType.Linter)).thenReturn( - instance(productPathService), - ); - - installer = new LinterInstallerTest(instance(serviceContainer)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Ensure 3 options for pylint', async () => { - const product = Product.pylint; - const options = ['Select Linter', "Don't show again"]; - const productName = ProductNames.get(product)!; - - await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).once(); - }); - test('Ensure select linter command is invoked', async () => { - const product = Product.pylint; - const options = ['Select Linter', "Don't show again"]; - const productName = ProductNames.get(product)!; - when( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).thenResolve(('Select Linter' as unknown) as void); - when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).once(); - verify(cmdManager.executeCommand(Commands.Set_Linter)).once(); - expect(response).to.be.equal(InstallerResponse.Ignore); - }); - test('If install button is selected, install linter and return response', async () => { - const product = Product.pylint; - const options = ['Select Linter', "Don't show again"]; - const productName = ProductNames.get(product)!; - when( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).thenResolve(('Install' as unknown) as void); - when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); - const install = sinon.stub(LinterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - const response = await installer.promptToInstallImplementation(product, resource); - - expect(response).to.be.equal(InstallerResponse.Installed); - assert.ok(install.calledOnceWith(product, resource, undefined)); - }); }); }); diff --git a/src/test/common/installer/productInstaller.unit.test.ts b/src/test/common/installer/productInstaller.unit.test.ts index 66e0cc005870..2934d613f88f 100644 --- a/src/test/common/installer/productInstaller.unit.test.ts +++ b/src/test/common/installer/productInstaller.unit.test.ts @@ -3,26 +3,15 @@ 'use strict'; -import * as assert from 'assert'; import { expect } from 'chai'; -import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; import { IApplicationShell } from '../../../client/common/application/types'; -import { DataScienceInstaller, FormatterInstaller } from '../../../client/common/installer/productInstaller'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { - IInstallationChannelManager, - IModuleInstaller, - InterpreterUri, - IProductPathService, - IProductService, -} from '../../../client/common/installer/types'; -import { InstallerResponse, IPersistentStateFactory, Product, ProductType } from '../../../client/common/types'; -import { Common } from '../../../client/common/utils/localize'; +import { DataScienceInstaller } from '../../../client/common/installer/productInstaller'; +import { IInstallationChannelManager, IModuleInstaller, InterpreterUri } from '../../../client/common/installer/types'; +import { InstallerResponse, Product } from '../../../client/common/types'; import { Architecture } from '../../../client/common/utils/platform'; import { IServiceContainer } from '../../../client/ioc/types'; import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { MockMemento } from '../../mocks/mementos'; class AlwaysInstalledDataScienceInstaller extends DataScienceInstaller { // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this @@ -89,250 +78,3 @@ suite('DataScienceInstaller install', async () => { expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); }); }); - -suite('Formatter installer', async () => { - let serviceContainer: TypeMoq.IMock; - // let outputChannel: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let persistentStateFactory: TypeMoq.IMock; - let productPathService: TypeMoq.IMock; - // let isExecutableAsModuleStub: sinon.SinonStub; - - // constructor(protected serviceContainer: IServiceContainer, protected outputChannel: OutputChannel) { - // this.appShell = serviceContainer.get(IApplicationShell); - // this.configService = serviceContainer.get(IConfigurationService); - // this.workspaceService = serviceContainer.get(IWorkspaceService); - // this.productService = serviceContainer.get(IProductService); - // this.persistentStateFactory = serviceContainer.get(IPersistentStateFactory); - // } - - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - // outputChannel = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - persistentStateFactory = TypeMoq.Mock.ofType(); - productPathService = TypeMoq.Mock.ofType(); - - const installStub = sinon.stub(FormatterInstaller.prototype, 'install'); - installStub.returns(Promise.resolve(InstallerResponse.Installed)); - - const productService = TypeMoq.Mock.ofType(); - productService.setup((p) => p.getProductType(TypeMoq.It.isAny())).returns(() => ProductType.Formatter); - - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory))) - .returns(() => persistentStateFactory.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IProductService))).returns(() => productService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), ProductType.Formatter)) - .returns(() => productPathService.object); - }); - - teardown(() => { - sinon.restore(); - }); - - // - if black not installed, offer autopep8 and yapf options - // - if autopep8 not installed, offer black and yapf options - // - if yapf not installed, offer black and autopep8 options - // - if not executable as a module, display error message - // - if never show again was set to true earlier, ignore - // if never show again is selected, ignore - - test('If black is not installed, offer autopep8 and yapf as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If autopep8 is not installed, offer black and yapf as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - - 'Use {0}'.format(ProductNames.get(Product.black)!), - 'Use {0}'.format(ProductNames.get(Product.yapf)!), - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.autopep8); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If yapf is not installed, offer autopep8 and black as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.black)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.yapf); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If the formatter is not executable as a module, display an error message', async () => { - const messageOptions = [ - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => 'foo'); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - }); - - test('If "Do not show again" has been selected earlier, do not display the prompt', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.never()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: true, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - assert.strictEqual(result, InstallerResponse.Ignore); - }); - - test('If "Do not show again" is selected, do not install the formatter and do not show the prompt again', async () => { - let value = false; - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.doNotShowAgain)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value, - updateValue: (newValue) => { - value = newValue; - return Promise.resolve(); - }, - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - const resultTwo = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Ignore); - assert.strictEqual(resultTwo, InstallerResponse.Ignore); - }); -}); diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts index 1e64ca63e117..0f627289da70 100644 --- a/src/test/common/installer/productPath.unit.test.ts +++ b/src/test/common/installer/productPath.unit.test.ts @@ -26,18 +26,18 @@ import { Product, ProductType, } from '../../../client/common/types'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IFormatterHelper } from '../../../client/formatters/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { ILinterInfo, ILinterManager } from '../../../client/linters/types'; import { ITestsHelper } from '../../../client/testing/common/types'; import { ITestingSettings } from '../../../client/testing/configuration/types'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); suite('Product Path', () => { [undefined, Uri.file('resource')].forEach((resource) => { - getNamesAndValues(Product).forEach((product) => { + getProductsForInstallerTests().forEach((product) => { class TestBaseProductPathsService extends BaseProductPathsService { public getExecutableNameFromSettings(_: Product, _resource?: Uri): string { return ''; @@ -75,9 +75,6 @@ suite('Product Path', () => { .returns(() => new ProductService()); }); - if (product.value === Product.isort) { - return; - } suite('Method isExecutableAModule()', () => { test('Returns true if User has customized the executable name', () => { productInstaller.translateProductToModuleName = () => 'moduleName'; diff --git a/src/test/common/productsToTest.ts b/src/test/common/productsToTest.ts new file mode 100644 index 000000000000..7fc06863f67c --- /dev/null +++ b/src/test/common/productsToTest.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Product } from '../../client/common/types'; +import { getNamesAndValues } from '../../client/common/utils/enum'; + +export function getProductsForInstallerTests(): { name: string; value: Product }[] { + return getNamesAndValues(Product).filter( + (p) => + ![ + 'pylint', + 'flake8', + 'pycodestyle', + 'pylama', + 'prospector', + 'pydocstyle', + 'yapf', + 'autopep8', + 'mypy', + 'isort', + 'black', + 'bandit', + ].includes(p.name), + ); +} diff --git a/src/test/linters/lint.multilinter.test.ts b/src/test/linters/lint.multilinter.test.ts deleted file mode 100644 index dba263e78479..000000000000 --- a/src/test/linters/lint.multilinter.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { ConfigurationTarget, DiagnosticCollection, Uri, window, workspace } from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { ICommandManager } from '../../client/common/application/types'; -import { Product } from '../../client/common/installer/productInstaller'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { ExecutionResult, IPythonToolExecutionService, SpawnOptions } from '../../client/common/process/types'; -import { ExecutionInfo, IConfigurationService } from '../../client/common/types'; -import { ILinterManager } from '../../client/linters/types'; -import { deleteFile, IExtensionTestApi, PythonSettingKeys, rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; - -const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); -const pythonFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'linting'); - -// Mocked out python tool execution (all we need is mocked linter return values). -class MockPythonToolExecService extends PythonToolExecutionService { - // Mocked samples of linter messages from flake8 and pylint: - public flake8Msg = - '1,1,W,W391:blank line at end of file\ns:142:13), :1\n1,7,E,E999:SyntaxError: invalid syntax\n'; - - public pylintMsg = `[ - { - "type": "error", - "module": "print", - "obj": "", - "line": 1, - "column": 0, - "path": "print.py", - "symbol": "syntax-error", - "message": "Missing parentheses in call to 'print'. Did you mean print(x)? (, line 1)", - "message-id": "E0001" - } -]`; - - // Depending on moduleName being exec'd, return the appropriate sample. - public async execForLinter( - executionInfo: ExecutionInfo, - _options: SpawnOptions, - _resource: Uri, - ): Promise> { - let msg = this.flake8Msg; - if (executionInfo.moduleName === 'pylint') { - msg = this.pylintMsg; - } - return { stdout: msg }; - } -} - -suite('Linting - Multiple Linters Enabled Test', () => { - let api: IExtensionTestApi; - let configService: IConfigurationService; - let linterManager: ILinterManager; - - suiteSetup(async () => { - api = await initialize(); - configService = api.serviceContainer.get(IConfigurationService); - linterManager = api.serviceContainer.get(ILinterManager); - }); - setup(async () => { - await initializeTest(); - await resetSettings(); - - // We only want to return some valid strings from linters, we don't care if they - // are being returned by actual linters (we aren't testing linters here, only how - // our code responds to those linters). - api.serviceManager.rebind(IPythonToolExecutionService, MockPythonToolExecService); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await resetSettings(); - await deleteFile(path.join(workspaceUri.fsPath, '.pylintrc')); - await deleteFile(path.join(workspaceUri.fsPath, '.pydocstyle')); - - // Restore the execution service as it was... - api.serviceManager.rebind(IPythonToolExecutionService, PythonToolExecutionService); - }); - - async function resetSettings() { - // Don't run these updates in parallel, as they are updating the same file. - const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - await configService.updateSetting('linting.enabled', true, rootWorkspaceUri, target); - await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); - - linterManager.getAllLinterInfos().forEach(async (x) => { - await configService.updateSetting(makeSettingKey(x.product), false, rootWorkspaceUri, target); - }); - } - - function makeSettingKey(product: Product): PythonSettingKeys { - return `linting.${linterManager.getLinterInfo(product).enabledSettingName}` as PythonSettingKeys; - } - - test('Multiple linters', async () => { - await closeActiveWindows(); - const document = await workspace.openTextDocument(path.join(pythonFilesPath, 'print.py')); - await window.showTextDocument(document); - await configService.updateSetting( - 'languageServer', - LanguageServerType.Jedi, - undefined, - ConfigurationTarget.Workspace, - ); - await configService.updateSetting('linting.enabled', true, workspaceUri); - await configService.updateSetting('linting.pylintEnabled', true, workspaceUri); - await configService.updateSetting('linting.flake8Enabled', true, workspaceUri); - - const commands = api.serviceContainer.get(ICommandManager); - - const collection = (await commands.executeCommand('python.runLinting')) as DiagnosticCollection; - assert.notStrictEqual(collection, undefined, 'python.runLinting did not return valid diagnostics collection.'); - - const messages = collection!.get(document.uri); - assert.notStrictEqual(messages!.length, 0, 'No diagnostic messages.'); - assert.notStrictEqual(messages!.filter((x) => x.source === 'pylint').length, 0, 'No pylint messages.'); - assert.notStrictEqual(messages!.filter((x) => x.source === 'flake8').length, 0, 'No flake8 messages.'); - }); -}); diff --git a/src/test/linters/linterCommands.unit.test.ts b/src/test/linters/linterCommands.unit.test.ts deleted file mode 100644 index b3d5c4693832..000000000000 --- a/src/test/linters/linterCommands.unit.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import { DiagnosticCollection } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { DocumentManager } from '../../client/common/application/documentManager'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../../client/common/application/types'; -import { Commands } from '../../client/common/constants'; -import { Product } from '../../client/common/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { LinterCommands } from '../../client/linters/linterCommands'; -import { LinterManager } from '../../client/linters/linterManager'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { ILinterInfo, ILinterManager, ILintingEngine } from '../../client/linters/types'; - -suite('Linting - Linter Commands', () => { - let linterCommands: LinterCommands; - let manager: ILinterManager; - let shell: IApplicationShell; - let docManager: IDocumentManager; - let cmdManager: ICommandManager; - let lintingEngine: ILintingEngine; - setup(() => { - const svcContainer = mock(ServiceContainer); - manager = mock(LinterManager); - shell = mock(ApplicationShell); - docManager = mock(DocumentManager); - cmdManager = mock(CommandManager); - lintingEngine = mock(LintingEngine); - when(svcContainer.get(ILinterManager)).thenReturn(instance(manager)); - when(svcContainer.get(IApplicationShell)).thenReturn(instance(shell)); - when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get(ILintingEngine)).thenReturn(instance(lintingEngine)); - linterCommands = new LinterCommands(instance(svcContainer)); - }); - - test('Commands are registered', () => { - verify(cmdManager.registerCommand(Commands.Set_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Enable_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Run_Linter, anything())).once(); - }); - - test('Run Linting method will lint all open files', async () => { - when(lintingEngine.lintOpenPythonFiles('manual')).thenResolve(('Hello' as unknown) as DiagnosticCollection); - - const result = await linterCommands.runLinting(); - - expect(result).to.be.equal('Hello'); - }); - - async function testEnableLintingWithCurrentState( - currentState: boolean, - selectedState: 'Enable' | 'Disable' | undefined, - ) { - when(manager.isLintingEnabled(anything())).thenResolve(currentState); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${currentState ? 'Enable' : 'Disable'}`, - }; - when(shell.showQuickPick(anything(), anything())).thenReturn(Promise.resolve(selectedState)); - - await linterCommands.enableLintingAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - const options = capture(shell.showQuickPick).last()[0]; - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(options).to.deep.equal(['Enable', 'Disable']); - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - - if (selectedState) { - verify(manager.enableLintingAsync(selectedState === 'Enable', anything())).once(); - } else { - verify(manager.enableLintingAsync(anything(), anything())).never(); - } - } - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select nothing", async () => { - await testEnableLintingWithCurrentState(true, undefined); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select 'Enable'", async () => { - await testEnableLintingWithCurrentState(true, 'Enable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select 'Disable'", async () => { - await testEnableLintingWithCurrentState(true, 'Disable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Disable' and select 'Enable'", async () => { - await testEnableLintingWithCurrentState(true, 'Enable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Disable' and select 'Disable'", async () => { - await testEnableLintingWithCurrentState(true, 'Disable'); - }); - - test('Set Linter should display a quickpick', async () => { - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve([]); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: none', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Set Linter should display a quickpick and currently active linter when only one is enabled', async () => { - const linterId = 'Hello World'; - const activeLinters: ILinterInfo[] = [({ id: linterId } as unknown) as ILinterInfo]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${linterId}`, - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Set Linter should display a quickpick and with message about multiple linters being enabled', async () => { - const activeLinters: ILinterInfo[] = ([{ id: 'linterId' }, { id: 'linterId2' }] as unknown) as ILinterInfo[]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: multiple selected', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Selecting a linter should display warning message about multiple linters', async () => { - const linters: ILinterInfo[] = ([ - { id: '1' }, - { id: '2' }, - { id: '3', product: 'Three' }, - ] as unknown) as ILinterInfo[]; - const activeLinters: ILinterInfo[] = ([{ id: '1' }, { id: '3' }] as unknown) as ILinterInfo[]; - when(manager.getAllLinterInfos()).thenReturn(linters); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenReturn(Promise.resolve('3')); - when(shell.showWarningMessage(anything(), 'Yes', 'No')).thenReturn(Promise.resolve('Yes')); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: multiple selected', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - verify(shell.showWarningMessage(anything(), 'Yes', 'No')).once(); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - verify(manager.setActiveLintersAsync(deepEqual([('Three' as unknown) as Product]), anything())).once(); - }); -}); From f2600075d40eba14d30bbdfd4a1a41e53f5511d8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 19 Sep 2023 11:00:34 -0700 Subject: [PATCH 280/336] Fix duplicate environments showing up on macOS (#22030) Closes https://github.com/microsoft/vscode-python/issues/22006 --- .../base/locators/lowLevel/globalVirtualEnvronmentLocator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts index d86b2182d50c..71ac3bca8eef 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts @@ -40,7 +40,8 @@ async function getGlobalVirtualEnvDirs(): Promise { const homeDir = getUserHomeDir(); if (homeDir && (await pathExists(homeDir))) { const subDirs = ['Envs', '.direnv', '.venvs', '.virtualenvs', path.join('.local', 'share', 'virtualenvs')]; - if (getOSType() !== OSType.Windows) { + if (![OSType.Windows, OSType.OSX].includes(getOSType())) { + // Not a case insensitive platform, push both upper and lower cases. subDirs.push('envs'); } const filtered = await asyncFilter( From ae81fcbab9b2a3cd9346bfde5adc32e0d1eaf419 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 19 Sep 2023 13:30:44 -0700 Subject: [PATCH 281/336] handle exceptions during test discovery (#22026) closes https://github.com/microsoft/vscode-python/issues/21999 and https://github.com/microsoft/vscode-python/issues/21826 --- .../testController/common/resultResolver.ts | 5 + .../testing/testController/common/server.ts | 27 +++- .../testing/testController/common/types.ts | 1 + .../testing/testController/common/utils.ts | 15 +++ .../pytest/pytestDiscoveryAdapter.ts | 11 ++ .../testing/common/testingAdapter.test.ts | 117 +++++++++++++++++- .../test_seg_fault_discovery.py | 16 +++ 7 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 5ef6695ca280..78883bdcf8fb 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -92,7 +92,12 @@ export class PythonResultResolver implements ITestResultResolver { populateTestTree(this.testController, rawTestData.tests, undefined, this, token); } else { // Delete everything from the test controller. + const errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); this.testController.items.replace([]); + // Add back the error node if it exists. + if (errorNode !== undefined) { + this.testController.items.add(errorNode); + } } sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index f59c486f7a85..699f7f754122 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -15,7 +15,12 @@ import { traceError, traceInfo, traceLog } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; -import { createEOTPayload, createExecutionErrorPayload, extractJsonPayload } from './utils'; +import { + createDiscoveryErrorPayload, + createEOTPayload, + createExecutionErrorPayload, + extractJsonPayload, +} from './utils'; import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { @@ -144,6 +149,10 @@ export class PythonTestServer implements ITestServer, Disposable { this._onRunDataReceived.fire(payload); } + public triggerDiscoveryDataReceivedEvent(payload: DataReceivedEvent): void { + this._onDiscoveryDataReceived.fire(payload); + } + public dispose(): void { this.server.close(); this._onDataReceived.dispose(); @@ -231,7 +240,7 @@ export class PythonTestServer implements ITestServer, Disposable { }); result?.proc?.on('exit', (code, signal) => { // if the child has testIds then this is a run request - if (code !== 0 && testIds) { + if (code !== 0 && testIds && testIds?.length !== 0) { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, ); @@ -245,6 +254,20 @@ export class PythonTestServer implements ITestServer, Disposable { uuid, data: JSON.stringify(createEOTPayload(true)), }); + } else if (code !== 0) { + // This occurs when we are running discovery + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + ); + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createDiscoveryErrorPayload(code, signal, options.cwd)), + }); + // then send a EOT payload + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); } deferred.resolve({ stdout: '', stderr: '' }); }); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 386c397b310c..32e0c4ba8cc6 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -187,6 +187,7 @@ export interface ITestServer { createUUID(cwd: string): string; deleteUUID(uuid: string): void; triggerRunDataReceivedEvent(data: DataReceivedEvent): void; + triggerDiscoveryDataReceivedEvent(data: DataReceivedEvent): void; } export interface ITestResultResolver { runIdToVSid: Map; diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 572863ecdbfe..a854c2780a75 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -12,6 +12,7 @@ import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilit import { DiscoveredTestItem, DiscoveredTestNode, + DiscoveredTestPayload, EOTTestPayload, ExecutionTestPayload, ITestResultResolver, @@ -299,6 +300,20 @@ export function createExecutionErrorPayload( return etp; } +export function createDiscoveryErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + cwd: string, +): DiscoveredTestPayload { + return { + cwd, + status: 'error', + error: [ + ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal}`, + ], + }; +} + export function createEOTPayload(executionBool: boolean): EOTTestPayload { return { commandType: executionBool ? 'execution' : 'discovery', diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index bafa91847b42..c03baeae0421 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -19,6 +19,7 @@ import { ITestResultResolver, ITestServer, } from '../common/types'; +import { createDiscoveryErrorPayload, createEOTPayload } from '../common/utils'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied @@ -100,6 +101,16 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, ); + // if the child process exited with a non-zero exit code, then we need to send the error payload. + this.testServer.triggerDiscoveryDataReceivedEvent({ + uuid, + data: JSON.stringify(createDiscoveryErrorPayload(code, signal, cwd)), + }); + // then send a EOT payload + this.testServer.triggerDiscoveryDataReceivedEvent({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); } deferredExec.resolve({ stdout: '', stderr: '' }); deferred.resolve(); diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index b445772d6958..4f46f1cf738c 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -51,6 +51,12 @@ suite('End to End Tests: test adapters', () => { 'testTestingRootWkspc', 'errorWorkspace', ); + const rootPathDiscoveryErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'discoveryErrorWorkspace', + ); suiteSetup(async () => { serviceContainer = (await initialize()).serviceContainer; }); @@ -477,6 +483,115 @@ suite('End to End Tests: test adapters', () => { assert.strictEqual(failureOccurred, false, failureMsg); }); }); + test('unittest discovery adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveDiscovery = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`unittest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + + const discoveryAdapter = new UnittestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await discoveryAdapter.discoverTests(workspaceUri).finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('pytest discovery seg fault error handling', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveDiscovery = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`add one to call count, is now ${callCount}`); + traceLog(`pytest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + assert.ok( + callCount >= 1, + `Expected _resolveDiscovery to be called at least once, call count was instead ${callCount}`, + ); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); test('unittest execution adapter seg fault error handling', async () => { resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; @@ -485,7 +600,7 @@ suite('End to End Tests: test adapters', () => { resultResolver._resolveExecution = async (data, _token?) => { // do the following asserts for each time resolveExecution is called, should be called once per test. callCount = callCount + 1; - console.log(`unittest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + traceLog(`unittest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); try { if (data.status === 'error') { if (data.error === undefined) { diff --git a/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py b/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py new file mode 100644 index 000000000000..5aac911b575a --- /dev/null +++ b/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +import ctypes + +ctypes.string_at(0) # Dereference a NULL pointer + + +class TestSegmentationFault(unittest.TestCase): + def test_segfault(self): + assert True + + +if __name__ == "__main__": + unittest.main() From f38ea44affa7ab62d7a17bb4d1835dba5bab71bf Mon Sep 17 00:00:00 2001 From: Kyle Gottfried Date: Wed, 20 Sep 2023 14:33:21 -0400 Subject: [PATCH 282/336] Update language to encourage reading "Migration to Python Tools Extensions" (#22019) There are other formatter options besides Black and Autopep8, but the language of this notice suggests otherwise. --- package.nls.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.nls.json b/package.nls.json index d260c98b0b5a..08f3a136584f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -54,14 +54,14 @@ "python.formatting.blackPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", "python.formatting.blackPath.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.provider.description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", - "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8) or the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension or the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension.
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.yapfArgs.markdownDeprecationMessage": "Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfArgs.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.yapfArgs.markdownDeprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.yapfArgs.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfPath.description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", "python.formatting.yapfPath.markdownDeprecationMessage": "Yapf support will soon be deprecated.
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfPath.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.yapfPath.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServer.description": "Defines type of the language server.", "python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.", From 849be34d4772086ae7bfef6986c7aef36887f7a5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 21 Sep 2023 01:10:15 -0700 Subject: [PATCH 283/336] De-duplicate directories at the very end in Global virtual env locators (#22040) --- .../lowLevel/globalVirtualEnvronmentLocator.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts index 71ac3bca8eef..71f3d69e9067 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { uniq } from 'lodash'; +import { toLower, uniq, uniqBy } from 'lodash'; import * as path from 'path'; import { chain, iterable } from '../../../../common/utils/async'; import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../../common/utils/platform'; @@ -39,11 +39,14 @@ async function getGlobalVirtualEnvDirs(): Promise { const homeDir = getUserHomeDir(); if (homeDir && (await pathExists(homeDir))) { - const subDirs = ['Envs', '.direnv', '.venvs', '.virtualenvs', path.join('.local', 'share', 'virtualenvs')]; - if (![OSType.Windows, OSType.OSX].includes(getOSType())) { - // Not a case insensitive platform, push both upper and lower cases. - subDirs.push('envs'); - } + const subDirs = [ + 'envs', + 'Envs', + '.direnv', + '.venvs', + '.virtualenvs', + path.join('.local', 'share', 'virtualenvs'), + ]; const filtered = await asyncFilter( subDirs.map((d) => path.join(homeDir, d)), pathExists, @@ -51,7 +54,7 @@ async function getGlobalVirtualEnvDirs(): Promise { filtered.forEach((d) => venvDirs.push(d)); } - return uniq(venvDirs); + return [OSType.Windows, OSType.OSX].includes(getOSType()) ? uniqBy(venvDirs, toLower) : uniq(venvDirs); } /** From 42cdaf302d5c5bc5db341aba06539ae1c6ef1670 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 21 Sep 2023 13:37:00 -0700 Subject: [PATCH 284/336] Make sure `PATH` ends with a separator before prepending (#22046) Introduced with https://github.com/microsoft/vscode-python/pull/21906 For #20950 Fixes https://github.com/microsoft/vscode-python/issues/22047 --- .../activation/terminalEnvVarCollectionService.ts | 9 ++++++++- .../terminalEnvVarCollectionService.unit.test.ts | 5 +++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 9bc95ee6d2e3..660dbdece257 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -63,6 +63,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ */ private processEnvVars: EnvironmentVariables | undefined; + private separator: string; + constructor( @inject(IPlatformService) private readonly platform: IPlatformService, @inject(IInterpreterService) private interpreterService: IInterpreterService, @@ -75,7 +77,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, @inject(IPathUtils) private readonly pathUtils: IPathUtils, - ) {} + ) { + this.separator = platform.osType === OSType.Windows ? ';' : ':'; + } public async activate(resource: Resource): Promise { try { @@ -196,6 +200,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ applyAtProcessCreation: true, }); } else { + if (!value.endsWith(this.separator)) { + value = value.concat(this.separator); + } traceVerbose(`Prepending environment variable ${key} in collection to ${value}`); envVarCollection.prepend(key, value, { applyAtShellIntegration: true, diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index cedc2701112f..3ebcea376045 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -248,12 +248,13 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); }); - test('Prepend full PATH otherwise', async () => { + test('Prepend full PATH with separator otherwise', async () => { const processEnv = { PATH: 'hello/1/2/3' }; reset(environmentActivationService); when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( processEnv, ); + const separator = getOSType() === OSType.Windows ? ';' : ':'; const finalPath = 'hello/3/2/1'; const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; when( @@ -275,7 +276,7 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.clear()).once(); - verify(collection.prepend('PATH', finalPath, anything())).once(); + verify(collection.prepend('PATH', `${finalPath}${separator}`, anything())).once(); verify(collection.replace('PATH', anything(), anything())).never(); assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); }); From 00b198af9cc41a674243958ee4290de71968130a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 21 Sep 2023 14:24:23 -0700 Subject: [PATCH 285/336] Fix bugs related to discovery blocking other features (#22041) For #21755 --- src/client/common/utils/multiStepInput.ts | 10 ++++-- src/client/interpreter/autoSelection/index.ts | 13 +++++++- .../commands/setInterpreter.ts | 11 ++++--- .../composite/envsCollectionService.ts | 4 +-- .../commands/setInterpreter.unit.test.ts | 31 +++++++++++++++---- 5 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/client/common/utils/multiStepInput.ts b/src/client/common/utils/multiStepInput.ts index e44879e8bbbb..e2b2567b5b4e 100644 --- a/src/client/common/utils/multiStepInput.ts +++ b/src/client/common/utils/multiStepInput.ts @@ -47,7 +47,7 @@ export interface IQuickPickParameters { totalSteps?: number; canGoBack?: boolean; items: T[]; - activeItem?: T | Promise; + activeItem?: T | ((quickPick: QuickPick) => Promise); placeholder: string | undefined; customButtonSetups?: QuickInputButtonSetup[]; matchOnDescription?: boolean; @@ -156,7 +156,13 @@ export class MultiStepInput implements IMultiStepInput { initialize(input); } if (activeItem) { - input.activeItems = [await activeItem]; + if (typeof activeItem === 'function') { + activeItem(input).then((item) => { + if (input.activeItems.length === 0) { + input.activeItems = [item]; + } + }); + } } else { input.activeItems = []; } diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts index a57577c8c918..7714c487ed30 100644 --- a/src/client/interpreter/autoSelection/index.ts +++ b/src/client/interpreter/autoSelection/index.ts @@ -181,6 +181,11 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio return this.stateFactory.createWorkspacePersistentState(key, undefined); } + private getAutoSelectionQueriedOnceState(): IPersistentState { + const key = `autoSelectionInterpretersQueriedOnce`; + return this.stateFactory.createWorkspacePersistentState(key, undefined); + } + /** * Auto-selection logic: * 1. If there are cached interpreters (not the first session in this workspace) @@ -200,7 +205,12 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio }); } - await this.interpreterService.refreshPromise; + const globalQueriedState = this.getAutoSelectionQueriedOnceState(); + if (!globalQueriedState.value) { + // Global interpreters are loaded the first time an extension loads, after which we don't need to + // wait on global interpreter promise refresh. + await this.interpreterService.refreshPromise; + } const interpreters = this.interpreterService.getInterpreters(resource); const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); @@ -215,6 +225,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } queriedState.updateValue(true); + globalQueriedState.updateValue(true); this.didAutoSelectedInterpreterEmitter.fire(); } diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index c0876ff518dd..9b8ecec74f9f 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -50,7 +50,7 @@ import { BaseInterpreterSelectorCommand } from './base'; const untildify = require('untildify'); export type InterpreterStateArgs = { path?: string; workspace: Resource }; -type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; +export type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; function isInterpreterQuickPickItem(item: QuickPickType): item is IInterpreterQuickPickItem { return 'interpreter' in item; @@ -177,7 +177,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem items: suggestions, sortByLabel: !preserveOrderWhenFiltering, keepScrollPosition: true, - activeItem: this.getActiveItem(state.workspace, suggestions), // Use a promise here to ensure quickpick is initialized synchronously. + activeItem: (quickPick) => this.getActiveItem(state.workspace, quickPick), // Use a promise here to ensure quickpick is initialized synchronously. matchOnDetail: true, matchOnDescription: true, title, @@ -277,8 +277,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return getGroupedQuickPickItems(items, recommended, workspaceFolder?.uri.fsPath); } - private async getActiveItem(resource: Resource, suggestions: QuickPickType[]) { + private async getActiveItem(resource: Resource, quickPick: QuickPick) { const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const suggestions = quickPick.items; const activeInterpreterItem = suggestions.find( (i) => isInterpreterQuickPickItem(i) && i.interpreter.id === interpreter?.id, ); @@ -339,7 +340,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return false; }) : undefined; - quickPick.activeItems = activeItem ? [activeItem] : []; + if (activeItem) { + quickPick.activeItems = [activeItem]; + } } /** diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index 2567168c6325..fb1a791d07ed 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -115,9 +115,9 @@ export class EnvsCollectionService extends PythonEnvsWatcher this.sendTelemetry(query, stopWatch)); } - return refreshPromise.then(() => this.sendTelemetry(query, stopWatch)); + return refreshPromise; } private startRefresh(query: PythonLocatorQuery | undefined): Promise { diff --git a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index 7059fb7ab26f..f177db5c2a32 100644 --- a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -31,6 +31,7 @@ import { import { EnvGroups, InterpreterStateArgs, + QuickPickType, SetInterpreterCommand, } from '../../../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter'; import { @@ -265,8 +266,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, recommended); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -308,8 +315,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, noPythonInstalled); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, noPythonInstalled); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -666,8 +679,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, recommended); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); From 242a333787db32066fc31579fc1bb2b1475e597a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 21 Sep 2023 18:58:55 -0700 Subject: [PATCH 286/336] Respect `VIRTUAL_ENV_DISABLE_PROMPT` when activating virtual envs (#22053) --- .../terminalEnvVarCollectionService.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 660dbdece257..489dd1b7c6eb 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -37,6 +37,7 @@ import { EnvironmentVariables } from '../../common/variables/types'; import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; import { normCase } from '../../common/platform/fs-paths'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { @@ -264,8 +265,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (this.platform.osType !== OSType.Windows) { // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. const interpreter = await this.interpreterService.getActiveInterpreter(resource); - const shouldPS1BeSet = interpreter?.type !== undefined; - if (shouldPS1BeSet && !env.PS1) { + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1 && !env.PS1) { // PS1 should be set but no PS1 was set. return; } @@ -291,8 +292,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (this.platform.osType !== OSType.Windows) { // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. const interpreter = await this.interpreterService.getActiveInterpreter(resource); - const shouldPS1BeSet = interpreter?.type !== undefined; - if (shouldPS1BeSet && !env.PS1) { + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1 && !env.PS1) { // PS1 should be set but no PS1 was set. return getPromptForEnv(interpreter); } @@ -367,6 +368,15 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } +function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariables): boolean { + if (type === PythonEnvType.Virtual) { + const promptDisabledVar = env.VIRTUAL_ENV_DISABLE_PROMPT; + const isPromptDisabled = promptDisabledVar && promptDisabledVar !== undefined; + return !isPromptDisabled; + } + return type !== undefined; +} + function shouldSkip(env: string) { return ['_', 'SHLVL'].includes(env); } From 7693fcb3aeb08d5e3d6c6d66afda70dc1c99188a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 22 Sep 2023 00:02:43 -0700 Subject: [PATCH 287/336] Respect conda changeps1 config when setting PS1 (#22054) For https://github.com/microsoft/vscode-python/issues/22048 --- .../terminalEnvVarCollectionService.ts | 7 ++ ...rminalEnvVarCollectionService.unit.test.ts | 85 ++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 489dd1b7c6eb..50a15d25f3de 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -374,6 +374,13 @@ function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariabl const isPromptDisabled = promptDisabledVar && promptDisabledVar !== undefined; return !isPromptDisabled; } + if (type === PythonEnvType.Conda) { + // Instead of checking config value using `conda config --get changeps1`, simply check + // `CONDA_PROMPT_MODIFER` to avoid the cost of launching the conda binary. + const promptEnabledVar = env.CONDA_PROMPT_MODIFIER; + const isPromptEnabled = promptEnabledVar && promptEnabledVar !== ''; + return !!isPromptEnabled; + } return type !== undefined; } diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 3ebcea376045..091986b7e18b 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -216,6 +216,85 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); + test('Respect VIRTUAL_ENV_DISABLE_PROMPT when setting PS1 for venv', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + VIRTUAL_ENV_DISABLE_PROMPT: '1', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', anything(), anything())).never(); + }); + + test('Otherwise set PS1 for venv even if PS1 is not returned', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', '(envName) ', anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', '(envName) ', anything())).once(); + }); + + test('Respect CONDA_PROMPT_MODIFIER when setting PS1 for conda', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + CONDA_PROMPT_MODIFIER: '(envName)', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + test('Prepend only "prepend portion of PATH" where applicable', async () => { const processEnv = { PATH: 'hello/1/2/3' }; reset(environmentActivationService); @@ -416,7 +495,11 @@ suite('Terminal Environment Variable Collection Service', () => { test('Correct track that prompt was not set for non-Windows where PS1 is not set but env name is base', async () => { when(platform.osType).thenReturn(OSType.Linux); - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + CONDA_PROMPT_MODIFIER: '(base)', + }; const ps1Shell = 'zsh'; const resource = Uri.file('a'); const workspaceFolder: WorkspaceFolder = { From 4ed3fa06b6a7e45b29c736c82c81821c2e1df330 Mon Sep 17 00:00:00 2001 From: Anna Burlyaeva Date: Fri, 22 Sep 2023 12:43:25 -0700 Subject: [PATCH 288/336] =?UTF-8?q?Changed=20order=20of=20options=20in=20C?= =?UTF-8?q?reate=20Environment=20flow=20when=20.venv=20exists=E2=80=A6=20(?= =?UTF-8?q?#22055)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://github.com/microsoft/vscode-python/issues/22038 --- src/client/pythonEnvironments/creation/provider/venvUtils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/src/client/pythonEnvironments/creation/provider/venvUtils.ts index 723337b2a7fa..f0828be8d1f7 100644 --- a/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -292,11 +292,14 @@ export async function pickExistingVenvAction( if (workspaceFolder) { if (await hasVenv(workspaceFolder)) { const items: QuickPickItem[] = [ - { label: CreateEnv.Venv.recreate, description: CreateEnv.Venv.recreateDescription }, { label: CreateEnv.Venv.useExisting, description: CreateEnv.Venv.useExistingDescription, }, + { + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }, ]; const selection = (await showQuickPickWithBack( From dfc939b87db1466aae7334f6d21797fecaed78b7 Mon Sep 17 00:00:00 2001 From: Ludi Zhan Date: Fri, 22 Sep 2023 13:16:20 -0700 Subject: [PATCH 289/336] Remove sort imports from command palette and context menu (#22058) Fixes https://github.com/microsoft/vscode-python/issues/20233 --- package.json | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/package.json b/package.json index 9cf564122d7b..57a2b34f7c03 100644 --- a/package.json +++ b/package.json @@ -430,11 +430,6 @@ "command": "python.setInterpreter", "title": "%python.command.python.setInterpreter.title%" }, - { - "category": "Python Refactor", - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%" - }, { "category": "Python", "command": "python.startREPL", @@ -1870,12 +1865,6 @@ "title": "%python.command.python.setInterpreter.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python Refactor", - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.startREPL", @@ -1914,12 +1903,6 @@ "group": "Python", "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && isWorkspaceTrusted" }, - { - "command": "python.sortImports", - "group": "Refactor", - "title": "%python.command.python.sortImports.title%", - "when": "editorLangId == python && !notebookEditorFocused && !virtualWorkspace && shellExecutionSupported" - }, { "submenu": "python.runFileInteractive", "group": "Jupyter2", From 3b6c47b4816eff2de44f5a99892b3a8bdf7f2535 Mon Sep 17 00:00:00 2001 From: Himani Madan Date: Fri, 22 Sep 2023 18:55:39 -0400 Subject: [PATCH 290/336] Pytest to pytest (#22062) --- src/client/common/utils/localize.ts | 2 +- src/client/testing/testController/common/utils.ts | 2 +- src/client/testing/testController/pytest/pytestController.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 05b525bdf5bf..a785ab4bc75a 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -415,7 +415,7 @@ export namespace Testing { export const cancelUnittestExecution = l10n.t('Canceled unittest test execution'); export const errorUnittestExecution = l10n.t('Unittest test execution error'); export const cancelPytestExecution = l10n.t('Canceled pytest test execution'); - export const errorPytestExecution = l10n.t('Pytest test execution error'); + export const errorPytestExecution = l10n.t('pytest test execution error'); } export namespace OutdatedDebugger { diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index a854c2780a75..dc254a9900a1 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -211,7 +211,7 @@ export async function startTestIdServer(testIds: string[]): Promise { } export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { - const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error'; + const labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; return { id: `DiscoveryError:${uri.fsPath}`, label: `${labelText} [${path.basename(uri.fsPath)}]`, diff --git a/src/client/testing/testController/pytest/pytestController.ts b/src/client/testing/testController/pytest/pytestController.ts index 997e3e29b7ec..d23cac842cda 100644 --- a/src/client/testing/testController/pytest/pytestController.ts +++ b/src/client/testing/testController/pytest/pytestController.ts @@ -235,7 +235,7 @@ export class PytestController implements ITestFrameworkController { testController.items.add( createErrorTestItem(testController, { id: `DiscoveryError:${workspace.uri.fsPath}`, - label: `Pytest Discovery Error [${path.basename(workspace.uri.fsPath)}]`, + label: `pytest Discovery Error [${path.basename(workspace.uri.fsPath)}]`, error: util.format( `${cancel} discovering pytest tests (see Output > Python):\r\n`, message.length > 0 ? message : ex, From 337b8626c80b0d066e9865e07b9e520771637945 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 25 Sep 2023 11:11:54 -0700 Subject: [PATCH 291/336] Show a prompt asking users if they want to create environment (#22071) Criteria for showing prompts: 1. It has to be a workspace or multiroot workspace. 2. The workspace or workspace folder should not have ".venv" or ".conda" environments. 3. The selected python should be a global python, i.e., there is no workspace specific environment selected. 4. The workspace should **not** have any `pipfile`, `poetry.lock` etc. 5. The workspace should have files that match `*requirements*.txt` or `requirements/*.txt` pattern. There is a setting to enable this behavior: `python.createEnvironment.trigger` and default is `off` closes https://github.com/microsoft/vscode-python/issues/21965 --- package.json | 13 + package.nls.json | 1 + src/client/common/constants.ts | 1 + src/client/common/persistentState.ts | 45 ++- src/client/common/utils/localize.ts | 9 + .../configuration/resolvers/launch.ts | 8 + .../debugger/extension/debugCommands.ts | 6 + src/client/extensionActivation.ts | 4 + .../creation/common/createEnvTriggerUtils.ts | 113 +++++++ .../creation/createEnvironmentTrigger.ts | 160 ++++++++++ .../creation/provider/venvUtils.ts | 2 +- src/client/telemetry/constants.ts | 3 + src/client/telemetry/index.ts | 28 ++ .../codeExecution/codeExecutionManager.ts | 12 + .../resolvers/launch.unit.test.ts | 7 + .../extension/debugCommands.unit.test.ts | 7 + .../createEnvironmentTrigger.unit.test.ts | 285 ++++++++++++++++++ .../codeExecutionManager.unit.test.ts | 9 + 18 files changed, 711 insertions(+), 2 deletions(-) create mode 100644 src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts create mode 100644 src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts create mode 100644 src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts diff --git a/package.json b/package.json index 57a2b34f7c03..ef9d0aa6c033 100644 --- a/package.json +++ b/package.json @@ -484,6 +484,19 @@ "experimental" ] }, + "python.createEnvironment.trigger": { + "default": "off", + "markdownDescription": "%python.createEnvironment.trigger.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "off", + "prompt" + ], + "tags": [ + "experimental" + ] + }, "python.condaPath": { "default": "", "description": "%python.condaPath.description%", diff --git a/package.nls.json b/package.nls.json index 08f3a136584f..7d14baf40d64 100644 --- a/package.nls.json +++ b/package.nls.json @@ -24,6 +24,7 @@ "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", "python.createEnvironment.contentButton.description": "Show or hide Create Environment button in the editor for `requirements.txt` or other dependency files.", + "python.createEnvironment.trigger.description": "Detect if environment creation is required for the current project", "python.menu.createNewFile.title": "Python File", "python.editor.context.submenu.runPython": "Run Python", "python.editor.context.submenu.runPythonInteractive": "Run in Interactive window", diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index f18fd20bb3dc..c3705a3c6504 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -38,6 +38,7 @@ export namespace Commands { export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; export const Create_Environment = 'python.createEnvironment'; export const Create_Environment_Button = 'python.createEnvironment-button'; + export const Create_Environment_Check = 'python.createEnvironmentCheck'; export const Create_Terminal = 'python.createTerminal'; export const Debug_In_Terminal = 'python.debugInTerminal'; export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index 76f6d2112fe0..2959a2dc8216 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -20,6 +20,46 @@ import { import { cache } from './utils/decorators'; import { noop } from './utils/misc'; +let _workspaceState: Memento | undefined; +const _workspaceKeys: string[] = []; +export function initializePersistentStateForTriggers(context: IExtensionContext) { + _workspaceState = context.workspaceState; +} + +export function getWorkspaceStateValue(key: string, defaultValue?: T): T | undefined { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + if (defaultValue === undefined) { + return _workspaceState.get(key); + } + return _workspaceState.get(key, defaultValue); +} + +export async function updateWorkspaceStateValue(key: string, value: T): Promise { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + try { + _workspaceKeys.push(key); + await _workspaceState.update(key, value); + const after = getWorkspaceStateValue(key); + if (JSON.stringify(after) !== JSON.stringify(value)) { + await _workspaceState.update(key, undefined); + await _workspaceState.update(key, value); + traceError('Error while updating workspace state for key:', key); + } + } catch (ex) { + traceError(`Error while updating workspace state for key [${key}]:`, ex); + } +} + +async function clearWorkspaceState(): Promise { + if (_workspaceState !== undefined) { + await Promise.all(_workspaceKeys.map((key) => updateWorkspaceStateValue(key, undefined))); + } +} + export class PersistentState implements IPersistentState { constructor( public readonly storage: Memento, @@ -93,7 +133,10 @@ export class PersistentStateFactory implements IPersistentStateFactory, IExtensi ) {} public async activate(): Promise { - this.cmdManager?.registerCommand(Commands.ClearStorage, this.cleanAllPersistentStates.bind(this)); + this.cmdManager?.registerCommand(Commands.ClearStorage, async () => { + await clearWorkspaceState(); + await this.cleanAllPersistentStates(); + }); const globalKeysStorageDeprecated = this.createGlobalPersistentState(GLOBAL_PERSISTENT_KEYS_DEPRECATED, []); const workspaceKeysStorageDeprecated = this.createWorkspacePersistentState( WORKSPACE_PERSISTENT_KEYS_DEPRECATED, diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index a785ab4bc75a..bc32c1078cad 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -499,6 +499,15 @@ export namespace CreateEnv { export const deletingEnvironmentProgress = l10n.t('Deleting existing ".conda" environment...'); export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".conda" environment.'); } + + export namespace Trigger { + export const workspaceTriggerMessage = l10n.t( + 'A virtual environment is not currently selected for your Python interpreter. Would you like to create a virtual environment?', + ); + export const createEnvironment = l10n.t('Create'); + export const disableCheck = l10n.t('Disable'); + export const disableCheckWorkspace = l10n.t('Disable (Workspace)'); + } } export namespace ToolsExtensions { diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index c4ae6a204d71..274758797eb9 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -16,6 +16,12 @@ import { DebuggerTypeName } from '../../../constants'; import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../types'; import { BaseConfigurationResolver } from './base'; import { getProgram, IDebugEnvironmentVariablesService } from './helper'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../../../pythonEnvironments/creation/createEnvironmentTrigger'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; @injectable() export class LaunchConfigurationResolver extends BaseConfigurationResolver { @@ -84,6 +90,8 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver debugConfiguration.debugOptions!.indexOf(item) === pos, ); } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.Workspace, workspaceFolder); return debugConfiguration; } diff --git a/src/client/debugger/extension/debugCommands.ts b/src/client/debugger/extension/debugCommands.ts index 14a108d27793..b3322e8e7dd1 100644 --- a/src/client/debugger/extension/debugCommands.ts +++ b/src/client/debugger/extension/debugCommands.ts @@ -14,6 +14,10 @@ import { DebugPurpose, LaunchRequestArguments } from '../types'; import { IInterpreterService } from '../../interpreter/contracts'; import { noop } from '../../common/utils/misc'; import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; @injectable() export class DebugCommands implements IExtensionSingleActivationService { @@ -35,6 +39,8 @@ export class DebugCommands implements IExtensionSingleActivationService { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug-in-terminal' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); const config = await DebugCommands.getDebugConfiguration(file); this.debugService.startDebugging(undefined, config); }), diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index c82bddef3c20..807698f3ec29 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -53,6 +53,8 @@ import { DynamicPythonDebugConfigurationService } from './debugger/extension/con import { IInterpreterQuickPick } from './interpreter/configuration/types'; import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; +import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; +import { initializePersistentStateForTriggers } from './common/persistentState'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -199,6 +201,8 @@ async function activateLegacy(ext: ExtensionState): Promise { ); registerInstallFormatterPrompt(serviceContainer); + registerCreateEnvironmentTriggers(disposables); + initializePersistentStateForTriggers(ext.context); } } diff --git a/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts b/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts new file mode 100644 index 000000000000..0c1c2b38eab2 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as fsapi from 'fs-extra'; +import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode'; +import { getPipRequirementsFiles } from '../provider/venvUtils'; +import { getExtension } from '../../../common/vscodeApis/extensionsApi'; +import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import { PythonExtension } from '../../../api/types'; +import { traceVerbose } from '../../../logging'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; +import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../../../common/persistentState'; + +export const CREATE_ENV_TRIGGER_SETTING_PART = 'createEnvironment.trigger'; +export const CREATE_ENV_TRIGGER_SETTING = `python.${CREATE_ENV_TRIGGER_SETTING_PART}`; + +export async function fileContainsInlineDependencies(_uri: Uri): Promise { + // This is a placeholder for the real implementation of inline dependencies support + // For now we don't detect anything. Once PEP-722/PEP-723 are accepted we can implement + // this properly. + return false; +} + +export async function hasRequirementFiles(workspace: WorkspaceFolder): Promise { + const files = await getPipRequirementsFiles(workspace); + const found = (files?.length ?? 0) > 0; + if (found) { + traceVerbose(`Found requirement files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function hasKnownFiles(workspace: WorkspaceFolder): Promise { + const filePaths: string[] = [ + 'poetry.lock', + 'conda.yaml', + 'environment.yaml', + 'conda.yml', + 'environment.yml', + 'Pipfile', + 'Pipfile.lock', + ].map((fileName) => path.join(workspace.uri.fsPath, fileName)); + const result = await Promise.all(filePaths.map((f) => fsapi.pathExists(f))); + const found = result.some((r) => r); + if (found) { + traceVerbose(`Found known files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function isGlobalPythonSelected(workspace: WorkspaceFolder): Promise { + const extension = getExtension(PVSC_EXTENSION_ID); + if (!extension) { + return false; + } + const extensionApi: PythonExtension = extension.exports as PythonExtension; + const interpreter = extensionApi.environments.getActiveEnvironmentPath(workspace.uri); + const details = await extensionApi.environments.resolveEnvironment(interpreter); + const isGlobal = details?.environment === undefined; + if (isGlobal) { + traceVerbose(`Selected python for [${workspace.uri.fsPath}] is [global] type: ${interpreter.path}`); + } + return isGlobal; +} + +/** + * Checks the setting `python.createEnvironment.trigger` to see if we should perform the checks + * to prompt to create an environment. + * @export + * @returns : True if we should prompt to create an environment. + */ +export function shouldPromptToCreateEnv(): boolean { + const config = getConfiguration('python'); + if (config) { + const value = config.get(CREATE_ENV_TRIGGER_SETTING_PART, 'off'); + return value !== 'off'; + } + + return getWorkspaceStateValue(CREATE_ENV_TRIGGER_SETTING, 'off') !== 'off'; +} + +/** + * Sets `python.createEnvironment.trigger` to 'off' in the user settings. + */ +export function disableCreateEnvironmentTrigger(): void { + const config = getConfiguration('python'); + if (config) { + config.update('createEnvironment.trigger', 'off', ConfigurationTarget.Global); + } +} + +/** + * Sets trigger to 'off' in workspace persistent state. This disables trigger check + * for the current workspace only. In multi root case, it is disabled for all folders + * in the multi root workspace. + */ +export async function disableWorkspaceCreateEnvironmentTrigger(): Promise { + await updateWorkspaceStateValue(CREATE_ENV_TRIGGER_SETTING, 'off'); +} + +let _alreadyCreateEnvCriteriaCheck = false; +/** + * Run-once wrapper function for the workspace check to prompt to create an environment. + * @returns : True if we should prompt to c environment. + */ +export function isCreateEnvWorkspaceCheckNotRun(): boolean { + if (_alreadyCreateEnvCriteriaCheck) { + return false; + } + _alreadyCreateEnvCriteriaCheck = true; + return true; +} diff --git a/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts b/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts new file mode 100644 index 000000000000..1737d351ca7b --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { + fileContainsInlineDependencies, + hasKnownFiles, + hasRequirementFiles, + isGlobalPythonSelected, + shouldPromptToCreateEnv, + isCreateEnvWorkspaceCheckNotRun, + disableCreateEnvironmentTrigger, + disableWorkspaceCreateEnvironmentTrigger, +} from './common/createEnvTriggerUtils'; +import { getWorkspaceFolder } from '../../common/vscodeApis/workspaceApis'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { hasPrefixCondaEnv, hasVenv } from './common/commonUtils'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { CreateEnv } from '../../common/utils/localize'; +import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; +import { Commands } from '../../common/constants'; +import { Resource } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +export enum CreateEnvironmentCheckKind { + /** + * Checks if environment creation is needed based on file location and content. + */ + File = 'file', + + /** + * Checks if environment creation is needed based on workspace contents. + */ + Workspace = 'workspace', +} + +export interface CreateEnvironmentTriggerOptions { + force?: boolean; +} + +async function createEnvironmentCheckForWorkspace(uri: Uri): Promise { + const workspace = getWorkspaceFolder(uri); + if (!workspace) { + traceInfo(`CreateEnv Trigger - Workspace not found for ${uri.fsPath}`); + return; + } + + const missingRequirements = async (workspaceFolder: WorkspaceFolder) => + !(await hasRequirementFiles(workspaceFolder)); + + const isNonGlobalPythonSelected = async (workspaceFolder: WorkspaceFolder) => + !(await isGlobalPythonSelected(workspaceFolder)); + + // Skip showing the Create Environment prompt if one of the following is True: + // 1. The workspace already has a ".venv" or ".conda" env + // 2. The workspace does NOT have "requirements.txt" or "requirements/*.txt" files + // 3. The workspace has known files for other environment types like environment.yml, conda.yml, poetry.lock, etc. + // 4. The selected python is NOT classified as a global python interpreter + const skipPrompt: boolean = ( + await Promise.all([ + hasVenv(workspace), + hasPrefixCondaEnv(workspace), + missingRequirements(workspace), + hasKnownFiles(workspace), + isNonGlobalPythonSelected(workspace), + ]) + ).some((r) => r); + + if (skipPrompt) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-not-met' }); + traceInfo(`CreateEnv Trigger - Skipping for ${uri.fsPath}`); + return; + } + + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-met' }); + const selection = await showInformationMessage( + CreateEnv.Trigger.workspaceTriggerMessage, + CreateEnv.Trigger.createEnvironment, + CreateEnv.Trigger.disableCheckWorkspace, + CreateEnv.Trigger.disableCheck, + ); + + if (selection === CreateEnv.Trigger.createEnvironment) { + try { + await executeCommand(Commands.Create_Environment); + } catch (error) { + traceError('CreateEnv Trigger - Error while creating environment: ', error); + } + } else if (selection === CreateEnv.Trigger.disableCheck) { + disableCreateEnvironmentTrigger(); + } else if (selection === CreateEnv.Trigger.disableCheckWorkspace) { + disableWorkspaceCreateEnvironmentTrigger(); + } +} + +function runOnceWorkspaceCheck(uri: Uri, options: CreateEnvironmentTriggerOptions = {}): Promise { + if (isCreateEnvWorkspaceCheckNotRun() || options?.force) { + return createEnvironmentCheckForWorkspace(uri); + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'already-ran' }); + traceVerbose('CreateEnv Trigger - skipping this because it was already run'); + return Promise.resolve(); +} + +async function createEnvironmentCheckForFile(uri: Uri, options?: CreateEnvironmentTriggerOptions): Promise { + if (await fileContainsInlineDependencies(uri)) { + // TODO: Handle create environment for each file here. + // pending acceptance of PEP-722/PEP-723 + + // For now we do the same thing as for workspace. + await runOnceWorkspaceCheck(uri, options); + } + + // If the file does not have any inline dependencies, then we do the same thing + // as for workspace. + await runOnceWorkspaceCheck(uri, options); +} + +export async function triggerCreateEnvironmentCheck( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): Promise { + if (!uri) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'no-uri' }); + traceVerbose('CreateEnv Trigger - Skipping No URI provided'); + return; + } + + if (shouldPromptToCreateEnv()) { + if (kind === CreateEnvironmentCheckKind.File) { + await createEnvironmentCheckForFile(uri, options); + } else { + await runOnceWorkspaceCheck(uri, options); + } + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'turned-off' }); + traceVerbose('CreateEnv Trigger - turned off in settings'); + } +} + +export function triggerCreateEnvironmentCheckNonBlocking( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): void { + // The Event loop for Node.js runs functions with setTimeout() with lower priority than setImmediate. + // This is done to intentionally avoid blocking anything that the user wants to do. + setTimeout(() => triggerCreateEnvironmentCheck(kind, uri, options).ignoreErrors(), 0); +} + +export function registerCreateEnvironmentTriggers(disposables: Disposable[]): void { + disposables.push( + registerCommand(Commands.Create_Environment_Check, (file: Resource) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'as-command' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file, { force: true }); + }), + ); +} diff --git a/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/src/client/pythonEnvironments/creation/provider/venvUtils.ts index f0828be8d1f7..cc506a11a88f 100644 --- a/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -35,7 +35,7 @@ export const OPEN_REQUIREMENTS_BUTTON = { tooltip: CreateEnv.Venv.openRequirementsFile, }; const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; -async function getPipRequirementsFiles( +export async function getPipRequirementsFiles( workspaceFolder: WorkspaceFolder, token?: CancellationToken, ): Promise { diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index a729b3d491e8..c680b91094cb 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -115,6 +115,9 @@ export enum EventName { ENVIRONMENT_DELETE = 'ENVIRONMENT.DELETE', ENVIRONMENT_REUSE = 'ENVIRONMENT.REUSE', + ENVIRONMENT_CHECK_TRIGGER = 'ENVIRONMENT.CHECK.TRIGGER', + ENVIRONMENT_CHECK_RESULT = 'ENVIRONMENT.CHECK.RESULT', + TOOLS_EXTENSIONS_ALREADY_INSTALLED = 'TOOLS_EXTENSIONS.ALREADY_INSTALLED', TOOLS_EXTENSIONS_PROMPT_SHOWN = 'TOOLS_EXTENSIONS.PROMPT_SHOWN', TOOLS_EXTENSIONS_INSTALL_SELECTED = 'TOOLS_EXTENSIONS.INSTALL_SELECTED', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index f4947cd73f05..600f9a2d48ff 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2144,6 +2144,34 @@ export interface IEventNamePropertyMapping { [EventName.ENVIRONMENT_REUSE]: { environmentType: 'venv' | 'conda'; }; + /** + * Telemetry event sent when a check for environment creation conditions is triggered. + */ + /* __GDPR__ + "environemt.check.trigger" : { + "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CHECK_TRIGGER]: { + trigger: + | 'run-in-terminal' + | 'debug-in-terminal' + | 'run-selection' + | 'on-workspace-load' + | 'as-command' + | 'debug'; + }; + /** + * Telemetry event sent when a check for environment creation condition is computed. + */ + /* __GDPR__ + "environemt.check.result" : { + "result" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CHECK_RESULT]: { + result: 'criteria-met' | 'criteria-not-met' | 'already-ran' | 'turned-off' | 'no-uri'; + }; /** * Telemetry event sent when a linter or formatter extension is already installed. */ diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 9f1ba6e90d90..2dd619a1816a 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -18,6 +18,10 @@ import { traceError } from '../../logging'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from '../../terminals/types'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; @injectable() export class CodeExecutionManager implements ICodeExecutionManager { @@ -48,6 +52,10 @@ export class CodeExecutionManager implements ICodeExecutionManager { .then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { + trigger: 'run-in-terminal', + }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; await this.executeFileInTerminal(file, trigger, { newTerminalPerFile: cmd === Commands.Exec_In_Separate_Terminal, @@ -69,6 +77,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); await this.executeSelectionInTerminal().then(() => { if (this.shouldTerminalFocusOnStart(file)) this.commandManager.executeCommand('workbench.action.terminal.focus'); @@ -85,6 +95,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); await this.executeSelectionInDjangoShell().then(() => { if (this.shouldTerminalFocusOnStart(file)) this.commandManager.executeCommand('workbench.action.terminal.focus'); diff --git a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts index 2aec3dcfd041..59f61f81cd85 100644 --- a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -22,6 +22,7 @@ import * as platform from '../../../../../client/common/utils/platform'; import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; import { IEnvironmentActivationService } from '../../../../../client/interpreter/activation/types'; +import * as triggerApis from '../../../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; getInfoPerOS().forEach(([osName, osType, path]) => { if (osType === platform.OSType.Unknown) { @@ -42,12 +43,18 @@ getInfoPerOS().forEach(([osName, osType, path]) => { let getActiveTextEditorStub: sinon.SinonStub; let getOSTypeStub: sinon.SinonStub; let getWorkspaceFolderStub: sinon.SinonStub; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); getOSTypeStub = sinon.stub(platform, 'getOSType'); getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); getOSTypeStub.returns(osType); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { diff --git a/src/test/debugger/extension/debugCommands.unit.test.ts b/src/test/debugger/extension/debugCommands.unit.test.ts index 3c023f3f1450..7d2463072f06 100644 --- a/src/test/debugger/extension/debugCommands.unit.test.ts +++ b/src/test/debugger/extension/debugCommands.unit.test.ts @@ -14,6 +14,7 @@ import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; import * as telemetry from '../../../client/telemetry'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; suite('Debugging - commands', () => { let commandManager: typemoq.IMock; @@ -21,6 +22,7 @@ suite('Debugging - commands', () => { let disposables: typemoq.IMock; let interpreterService: typemoq.IMock; let debugCommands: IExtensionSingleActivationService; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { commandManager = typemoq.Mock.ofType(); @@ -36,6 +38,11 @@ suite('Debugging - commands', () => { sinon.stub(telemetry, 'sendTelemetryEvent').callsFake(() => { /** noop */ }); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { sinon.restore(); diff --git a/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts new file mode 100644 index 000000000000..f751d270219e --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as triggerUtils from '../../../client/pythonEnvironments/creation/common/createEnvTriggerUtils'; +import * as commonUtils from '../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheck, +} from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { Commands } from '../../../client/common/constants'; +import { CreateEnv } from '../../../client/common/utils/localize'; + +suite('Create Environment Trigger', () => { + let shouldPromptToCreateEnvStub: sinon.SinonStub; + let hasVenvStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let hasRequirementFilesStub: sinon.SinonStub; + let hasKnownFilesStub: sinon.SinonStub; + let isGlobalPythonSelectedStub: sinon.SinonStub; + let showInformationMessageStub: sinon.SinonStub; + let isCreateEnvWorkspaceCheckNotRunStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let executeCommandStub: sinon.SinonStub; + let disableCreateEnvironmentTriggerStub: sinon.SinonStub; + let disableWorkspaceCreateEnvironmentTriggerStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + shouldPromptToCreateEnvStub = sinon.stub(triggerUtils, 'shouldPromptToCreateEnv'); + hasVenvStub = sinon.stub(commonUtils, 'hasVenv'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + hasRequirementFilesStub = sinon.stub(triggerUtils, 'hasRequirementFiles'); + hasKnownFilesStub = sinon.stub(triggerUtils, 'hasKnownFiles'); + isGlobalPythonSelectedStub = sinon.stub(triggerUtils, 'isGlobalPythonSelected'); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + + isCreateEnvWorkspaceCheckNotRunStub = sinon.stub(triggerUtils, 'isCreateEnvWorkspaceCheckNotRun'); + isCreateEnvWorkspaceCheckNotRunStub.returns(true); + + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getWorkspaceFolderStub.returns(workspace1); + + executeCommandStub = sinon.stub(commandApis, 'executeCommand'); + disableCreateEnvironmentTriggerStub = sinon.stub(triggerUtils, 'disableCreateEnvironmentTrigger'); + disableWorkspaceCreateEnvironmentTriggerStub = sinon.stub( + triggerUtils, + 'disableWorkspaceCreateEnvironmentTrigger', + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No Uri', async () => { + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, undefined); + sinon.assert.notCalled(shouldPromptToCreateEnvStub); + }); + + test('Should not perform checks if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not perform checks even if force is true, if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri, { + force: true, + }); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".venv"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(true); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".conda"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(true); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are no requirements', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are known files', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(true); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if selected python is not global', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should show prompt if all conditions met: User closes prompt', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + showInformationMessageStub.resolves(undefined); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks create', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.createEnvironment); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.calledOnceWithExactly(executeCommandStub, Commands.Create_Environment); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks disable global', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.disableCheck); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.calledOnce(disableCreateEnvironmentTriggerStub); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks disable workspace', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.disableCheckWorkspace); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + sinon.assert.calledOnce(disableWorkspaceCreateEnvironmentTriggerStub); + }); +}); diff --git a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 30f95c94d217..29c310f6c724 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import { Disposable, TextDocument, TextEditor, Uri } from 'vscode'; import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; @@ -13,6 +14,7 @@ import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } fr import { IConfigurationService } from '../../../client/common/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; suite('Terminal - Code Execution Manager', () => { let executionManager: ICodeExecutionManager; @@ -24,6 +26,7 @@ suite('Terminal - Code Execution Manager', () => { let configService: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { fileSystem = TypeMoq.Mock.ofType(); fileSystem.setup((f) => f.readFile(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); @@ -52,8 +55,14 @@ suite('Terminal - Code Execution Manager', () => { configService.object, serviceContainer.object, ); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { + sinon.restore(); disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); From 998a0a54a8107b2033a25df36569d090a64244d4 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 25 Sep 2023 15:01:50 -0700 Subject: [PATCH 292/336] Add await for stdout (#22049) Add await so all output is read before ending the run instance. --- .../testing/testController/common/server.ts | 29 ++++++++-- .../testing/testController/common/utils.ts | 2 +- .../testing/testController/controller.ts | 1 - .../pytest/pytestDiscoveryAdapter.ts | 21 ++++--- .../pytest/pytestExecutionAdapter.ts | 55 +++++++++++++------ src/test/mocks/mockChildProcess.ts | 6 +- .../testing/common/testingPayloadsEot.test.ts | 11 ++-- .../pytestExecutionAdapter.unit.test.ts | 2 +- .../testCancellationRunAdapters.unit.test.ts | 53 ++++++++++++++---- 9 files changed, 126 insertions(+), 54 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 699f7f754122..d47587386069 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -5,6 +5,7 @@ import * as net from 'net'; import * as crypto from 'crypto'; import { Disposable, Event, EventEmitter, TestRun } from 'vscode'; import * as path from 'path'; +import { ChildProcess } from 'child_process'; import { ExecutionFactoryCreateWithEnvironmentOptions, ExecutionResult, @@ -86,7 +87,7 @@ export class PythonTestServer implements ITestServer, Disposable { // what payload is so small it doesn't include the whole UUID think got this if (extractedJsonPayload.uuid !== undefined && extractedJsonPayload.cleanedJsonData !== undefined) { // if a full json was found in the buffer, fire the data received event then keep cycling with the remaining raw data. - traceInfo(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); + traceLog(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); this._fireDataReceived(extractedJsonPayload.uuid, extractedJsonPayload.cleanedJsonData); } buffer = Buffer.from(extractedJsonPayload.remainingRawData); @@ -223,13 +224,23 @@ export class PythonTestServer implements ITestServer, Disposable { // This means it is running discovery traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } - const deferred = createDeferred>(); - const result = execService.execObservable(args, spawnOptions); + const deferredTillExecClose = createDeferred>(); + + let resultProc: ChildProcess | undefined; + runInstance?.token.onCancellationRequested(() => { traceInfo('Test run cancelled, killing unittest subprocess.'); - result?.proc?.kill(); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose?.resolve(); + } }); + const result = execService?.execObservable(args, spawnOptions); + resultProc = result?.proc; + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { @@ -238,6 +249,12 @@ export class PythonTestServer implements ITestServer, Disposable { result?.proc?.stderr?.on('data', (data) => { spawnOptions?.outputChannel?.append(data.toString()); }); + result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}`); + } + }); + result?.proc?.on('exit', (code, signal) => { // if the child has testIds then this is a run request if (code !== 0 && testIds && testIds?.length !== 0) { @@ -269,9 +286,9 @@ export class PythonTestServer implements ITestServer, Disposable { data: JSON.stringify(createEOTPayload(true)), }); } - deferred.resolve({ stdout: '', stderr: '' }); + deferredTillExecClose.resolve({ stdout: '', stderr: '' }); }); - await deferred.promise; + await deferredTillExecClose.promise; } } catch (ex) { traceError(`Error while server attempting to run unittest command: ${ex}`); diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index dc254a9900a1..1272ff37fb5d 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -43,7 +43,7 @@ export const JSONRPC_UUID_HEADER = 'Request-uuid'; export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length'; export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type'; -export function createEOTDeferred(): Deferred { +export function createTestingDeferred(): Deferred { return createDeferred(); } diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 0c7db5594004..af77ab2b2525 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -462,7 +462,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); } } - if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { unconfiguredWorkspaces.push(workspace); } diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index c03baeae0421..c0e1a310ee4a 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -4,7 +4,6 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -19,7 +18,7 @@ import { ITestResultResolver, ITestServer, } from '../common/types'; -import { createDiscoveryErrorPayload, createEOTPayload } from '../common/utils'; +import { createDiscoveryErrorPayload, createEOTPayload, createTestingDeferred } from '../common/utils'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied @@ -47,6 +46,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { await this.runPytestDiscovery(uri, uuid, executionFactory); } finally { await deferredTillEOT.promise; + traceVerbose('deferredTill EOT resolved'); disposeDataReceiver(this.testServer); } // this is only a placeholder to handle function overloading until rewrite is finished @@ -55,7 +55,6 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } async runPytestDiscovery(uri: Uri, uuid: string, executionFactory?: IPythonExecutionFactory): Promise { - const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); const settings = this.configSettings.getSettings(uri); @@ -83,9 +82,10 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. - const deferredExec = createDeferred>(); const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); traceVerbose(`Running pytest discovery with command: ${execArgs.join(' ')}`); + + const deferredTillExecClose: Deferred = createTestingDeferred(); const result = execService?.execObservable(execArgs, spawnOptions); // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. @@ -97,6 +97,11 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { spawnOptions.outputChannel?.append(data.toString()); }); result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); + } + }); + result?.proc?.on('close', (code, signal) => { if (code !== 0) { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, @@ -112,10 +117,10 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { data: JSON.stringify(createEOTPayload(true)), }); } - deferredExec.resolve({ stdout: '', stderr: '' }); - deferred.resolve(); + // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs + // due to the sync reading of the output. + deferredTillExecClose?.resolve(); }); - - await deferredExec.promise; + await deferredTillExecClose.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 085af40375d4..8020be17cf90 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -3,9 +3,10 @@ import { TestRun, Uri } from 'vscode'; import * as path from 'path'; +import { ChildProcess } from 'child_process'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { Deferred, createDeferred } from '../../../common/utils/async'; -import { traceError, traceInfo } from '../../../logging'; +import { Deferred } from '../../../common/utils/async'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, @@ -15,7 +16,6 @@ import { } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -42,7 +42,9 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { debugLauncher?: ITestDebugLauncher, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - const deferredTillEOT: Deferred = utils.createEOTDeferred(); + // deferredTillEOT is resolved when all data sent over payload is received + const deferredTillEOT: Deferred = utils.createTestingDeferred(); + const dataReceivedDisposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { const eParsed = JSON.parse(e.data); @@ -60,8 +62,9 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo("Test run cancelled, resolving 'till EOT' deferred."); deferredTillEOT.resolve(); }); + try { - this.runTestsNew( + await this.runTestsNew( uri, testIds, uuid, @@ -73,6 +76,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ); } finally { await deferredTillEOT.promise; + traceVerbose('deferredTill EOT resolved'); disposeDataReceiver(this.testServer); } @@ -123,7 +127,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }; // need to check what will happen in the exec service is NOT defined and is null const execService = await executionFactory?.createActivatedEnvironment(creationOptions); - try { // Remove positional test folders and files, we will add as needed per node const testArgs = removePositionalFoldersAndFiles(pytestArgs); @@ -159,19 +162,28 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { deferredTillEOT?.resolve(); }); } else { + // deferredTillExecClose is resolved when all stdout and stderr is read + const deferredTillExecClose: Deferred = utils.createTestingDeferred(); // combine path to run script with run args const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); const runArgs = [scriptPath, ...testArgs]; traceInfo(`Running pytest with arguments: ${runArgs.join(' ')}\r\n`); - const deferredExec = createDeferred>(); - const result = execService?.execObservable(runArgs, spawnOptions); + let resultProc: ChildProcess | undefined; runInstance?.token.onCancellationRequested(() => { traceInfo('Test run cancelled, killing pytest subprocess.'); - result?.proc?.kill(); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose?.resolve(); + } }); + const result = execService?.execObservable(runArgs, spawnOptions); + resultProc = result?.proc; + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { @@ -180,15 +192,20 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { result?.proc?.stderr?.on('data', (data) => { this.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', (code, signal) => { - traceInfo('Test run finished, subprocess exited.'); + if (code !== 0 && testIds) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); + } + }); + + result?.proc?.on('close', (code, signal) => { + traceVerbose('Test run finished, subprocess closed.'); // if the child has testIds then this is a run request + // if the child process exited with a non-zero exit code, then we need to send the error payload. if (code !== 0 && testIds) { traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + `Subprocess closed unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, ); - // if the child process exited with a non-zero exit code, then we need to send the error payload. this.testServer.triggerRunDataReceivedEvent({ uuid, data: JSON.stringify(utils.createExecutionErrorPayload(code, signal, testIds, cwd)), @@ -199,16 +216,22 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { data: JSON.stringify(utils.createEOTPayload(true)), }); } - deferredExec.resolve({ stdout: '', stderr: '' }); + // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs + // due to the sync reading of the output. + deferredTillExecClose?.resolve(); }); - await deferredExec.promise; + await deferredTillExecClose?.promise; } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); } - const executionPayload: ExecutionTestPayload = { cwd, status: 'success', error: '' }; + const executionPayload: ExecutionTestPayload = { + cwd, + status: 'success', + error: '', + }; return executionPayload; } } diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts index a46d66d79ca0..c0a24b1c955f 100644 --- a/src/test/mocks/mockChildProcess.ts +++ b/src/test/mocks/mockChildProcess.ts @@ -133,9 +133,9 @@ export class MockChildProcess extends EventEmitter { emit(event: string | symbol, ...args: unknown[]): boolean { if (this.eventMap.has(event.toString())) { - this.eventMap.get(event.toString()).forEach((listener: (arg0: unknown) => void) => { - const argsArray = Array.isArray(args) ? args : [args]; - listener(argsArray); + this.eventMap.get(event.toString()).forEach((listener: (...arg0: unknown[]) => void) => { + const argsArray: unknown[] = Array.isArray(args) ? args : [args]; + listener(...argsArray); }); } return true; diff --git a/src/test/testing/common/testingPayloadsEot.test.ts b/src/test/testing/common/testingPayloadsEot.test.ts index 227ad5fa1697..9025248c66ff 100644 --- a/src/test/testing/common/testingPayloadsEot.test.ts +++ b/src/test/testing/common/testingPayloadsEot.test.ts @@ -66,6 +66,7 @@ suite('EOT tests', () => { let testController: TestController; let stubExecutionFactory: typeMoq.IMock; let client: net.Socket; + let mockProc: MockChildProcess; const sandbox = sinon.createSandbox(); // const unittestProvider: TestProvider = UNITTEST_PROVIDER; // const pytestProvider: TestProvider = PYTEST_PROVIDER; @@ -86,7 +87,7 @@ suite('EOT tests', () => { traceLog('Socket connection error:', error); }); - const mockProc = new MockChildProcess('', ['']); + mockProc = new MockChildProcess('', ['']); const output2 = new Observable>(() => { /* no op */ }); @@ -156,15 +157,17 @@ suite('EOT tests', () => { } })(client, payloadArray[i]); } + mockProc.emit('close', 0, null); client.end(); }); resultResolver = new PythonResultResolver(testController, PYTEST_PROVIDER, workspaceUri); resultResolver._resolveExecution = async (payload, _token?) => { // the payloads that get to the _resolveExecution are all data and should be successful. + actualCollectedResult = actualCollectedResult + JSON.stringify(payload.result); assert.strictEqual(payload.status, 'success', "Expected status to be 'success'"); assert.ok(payload.result, 'Expected results to be present'); - actualCollectedResult = actualCollectedResult + JSON.stringify(payload.result); + return Promise.resolve(); }; // set workspace to test workspace folder @@ -200,10 +203,6 @@ suite('EOT tests', () => { actualCollectedResult, "Expected collected result to match 'data'", ); - // nervous about this not testing race conditions correctly - // client.end(); - // verify that the _resolveExecution was called once per test - // assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); }); }); }); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 9cc428ab0a4c..a2e5c810dc86 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -268,7 +268,7 @@ suite('pytest test execution adapter', () => { traceInfo('stubs launch debugger'); deferredEOT.resolve(); }); - const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); utilsCreateEOTStub.callsFake(() => deferredEOT); const testRun = typeMoq.Mock.ofType(); testRun diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index 2aaffdda41df..e85cd2b62834 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -117,10 +117,16 @@ suite('Execution Flow Run Adapters', () => { deferredStartServer.resolve(); return Promise.resolve(54321); }); - // mock EOT token + // mock EOT token & ExecClose token const deferredEOT = createDeferred(); - const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); - utilsCreateEOTStub.callsFake(() => deferredEOT); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); // set up test server testServer .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -158,6 +164,11 @@ suite('Execution Flow Run Adapters', () => { const execServiceMock = typeMoq.Mock.ofType(); debugLauncher .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .callback((_options, callback) => { + if (callback) { + callback(); + } + }) .returns(async () => { cancellationToken.cancel(); return Promise.resolve(); @@ -174,10 +185,16 @@ suite('Execution Flow Run Adapters', () => { deferredStartServer.resolve(); return Promise.resolve(54321); }); - // mock EOT token + // mock EOT token & ExecClose token const deferredEOT = createDeferred(); - const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); - utilsCreateEOTStub.callsFake(() => deferredEOT); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); // set up test server testServer .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -263,10 +280,16 @@ suite('Execution Flow Run Adapters', () => { deferredStartServer.resolve(); return Promise.resolve(54321); }); - // mock EOT token + // mock EOT token & ExecClose token const deferredEOT = createDeferred(); - const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); - utilsCreateEOTStub.callsFake(() => deferredEOT); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); // set up test server const unittestAdapter = new UnittestTestExecutionAdapter( stubTestServer.object, @@ -331,10 +354,16 @@ suite('Execution Flow Run Adapters', () => { deferredStartServer.resolve(); return Promise.resolve(54321); }); - // mock EOT token + // mock EOT token & ExecClose token const deferredEOT = createDeferred(); - const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); - utilsCreateEOTStub.callsFake(() => deferredEOT); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); // set up test server const unittestAdapter = new UnittestTestExecutionAdapter( stubTestServer.object, From b3c5698cee0abe48e4fe471ebbc47ba34c5a7eb5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 25 Sep 2023 21:40:35 -0700 Subject: [PATCH 293/336] Explicitly continue execution after timeout on launching conda binary is reached (#22072) Closes https://github.com/microsoft/vscode-python/issues/22050 --- .../common/environmentManagers/conda.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts index 88178d02d58a..8f048ddd0676 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -23,6 +23,7 @@ import { traceError, traceVerbose } from '../../../logging'; import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; import { splitLines } from '../../../common/stringUtils'; import { SpawnOptions } from '../../../common/process/types'; +import { sleep } from '../../../common/utils/async'; export const AnacondaCompanyName = 'Anaconda, Inc.'; export const CONDAPATH_SETTING_KEY = 'condaPath'; @@ -238,7 +239,7 @@ export function getCondaInterpreterPath(condaEnvironmentPath: string): string { // Minimum version number of conda required to be able to use 'conda run' with '--no-capture-output' flag. export const CONDA_RUN_VERSION = '4.9.0'; export const CONDA_ACTIVATION_TIMEOUT = 45000; -const CONDA_GENERAL_TIMEOUT = 50000; +const CONDA_GENERAL_TIMEOUT = 45000; /** Wraps the "conda" utility, and exposes its functionality. */ @@ -439,9 +440,19 @@ export class Conda { if (shellPath) { options.shell = shellPath; } - const result = await exec(command, ['info', '--json'], options); - traceVerbose(`${command} info --json: ${result.stdout}`); - return JSON.parse(result.stdout); + const resultPromise = exec(command, ['info', '--json'], options); + // It has been observed that specifying a timeout is still not reliable to terminate the Conda process, see #27915. + // Hence explicitly continue execution after timeout has been reached. + const success = await Promise.race([ + resultPromise.then(() => true), + sleep(CONDA_GENERAL_TIMEOUT + 3000).then(() => false), + ]); + if (success) { + const result = await resultPromise; + traceVerbose(`${command} info --json: ${result.stdout}`); + return JSON.parse(result.stdout); + } + throw new Error(`Launching '${command} info --json' timed out`); } /** From 4f44fa917a1fe2c451490b1d252a195dbc9e08ee Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 25 Sep 2023 21:51:49 -0700 Subject: [PATCH 294/336] Calculate PS1 instead of using PS1 returned by shell (#22078) Closes https://github.com/microsoft/vscode-python/issues/22056 `PS1` returned by shell is not predictable, it can be `(.venv) ` or already have the context of the terminal: ``` (venv) [\u@\h \W]\[\e[91m\]$(parse_git_branch)\[\e[00m\]$ ``` Calculate it to be safe and not double prepend it. --- .../terminalEnvVarCollectionService.ts | 23 +++++++++++------ ...rminalEnvVarCollectionService.unit.test.ts | 25 ++++++++++++------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 50a15d25f3de..a4a5137953a6 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -282,22 +282,25 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } private async getPS1(shell: string, resource: Resource, env: EnvironmentVariables) { - if (env.PS1) { - return env.PS1; - } const customShellType = identifyShellFromShellPath(shell); if (this.noPromptVariableShells.includes(customShellType)) { - return undefined; + return env.PS1; } if (this.platform.osType !== OSType.Windows) { // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. const interpreter = await this.interpreterService.getActiveInterpreter(resource); const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); - if (shouldSetPS1 && !env.PS1) { - // PS1 should be set but no PS1 was set. - return getPromptForEnv(interpreter); + if (shouldSetPS1) { + const prompt = getPromptForEnv(interpreter); + if (prompt) { + return prompt; + } } } + if (env.PS1) { + // Prefer PS1 set by env vars, as env.PS1 may or may not contain the full PS1: #22056. + return env.PS1; + } return undefined; } @@ -369,6 +372,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariables): boolean { + if (env.PS1) { + // Activated variables contain PS1, meaning it was supposed to be set. + return true; + } if (type === PythonEnvType.Virtual) { const promptDisabledVar = env.VIRTUAL_ENV_DISABLE_PROMPT; const isPromptDisabled = promptDisabledVar && promptDisabledVar !== undefined; @@ -381,7 +388,7 @@ function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariabl const isPromptEnabled = promptEnabledVar && promptEnabledVar !== ''; return !!isPromptEnabled; } - return type !== undefined; + return false; } function shouldSkip(env: string) { diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 091986b7e18b..e41d6ce4d53c 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -191,21 +191,28 @@ suite('Terminal Environment Variable Collection Service', () => { verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); }); - test('If activated variables contain PS1, prefix it using shell integration', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env, PS1: '(prompt)' }; + // eslint-disable-next-line consistent-return + test('If activated variables contain PS1, prefix it using shell integration', async function () { + if (getOSType() === OSType.Windows) { + return this.skip(); + } + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + PS1: '(envName) extra prompt', // Should not use this + }; when( - environmentActivationService.getActivatedEnvironmentVariables( - anything(), - undefined, - undefined, - customShell, - ), + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName: 'envName', + } as unknown) as PythonEnvironment); + when(collection.replace(anything(), anything(), anything())).thenResolve(); when(collection.delete(anything())).thenResolve(); let opts: EnvironmentVariableMutatorOptions | undefined; - when(collection.prepend('PS1', '(prompt)', anything())).thenCall((_, _v, o) => { + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { opts = o; }); From 8aad45710a2b1df45978aeac3b45c321b50e6897 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 26 Sep 2023 02:18:54 -0700 Subject: [PATCH 295/336] Fix progress indicator when reactivating terminals (#22082) --- .../activation/terminalEnvVarCollectionService.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index a4a5137953a6..8d99ffd48445 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -102,21 +102,17 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (!this.registeredOnce) { this.interpreterService.onDidChangeInterpreter( async (r) => { - this.showProgress(); await this._applyCollection(r).ignoreErrors(); - this.hideProgress(); }, this, this.disposables, ); this.applicationEnvironment.onDidChangeShell( async (shell: string) => { - this.showProgress(); this.processEnvVars = undefined; // Pass in the shell where known instead of relying on the application environment, because of bug // on VSCode: https://github.com/microsoft/vscode/issues/160694 await this._applyCollection(undefined, shell).ignoreErrors(); - this.hideProgress(); }, this, this.disposables, @@ -130,6 +126,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { + this.showProgress(); const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); @@ -224,6 +221,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); envVarCollection.description = description; + this.hideProgress(); await this.trackTerminalPrompt(shell, resource, env); } From bd3590d3b6e78aa47de978cc4103ead9accb560e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 26 Sep 2023 13:45:08 -0700 Subject: [PATCH 296/336] Fix "reactivating terminals..." for global interpreters (#22096) Closes https://github.com/microsoft/vscode-python/issues/22085 closes https://github.com/microsoft/vscode-python/issues/22087 Will add tests in a follow up PR --- .../activation/terminalEnvVarCollectionService.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 8d99ffd48445..c11ec221d4d7 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -125,8 +125,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } - public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { + public async _applyCollection(resource: Resource, shell?: string): Promise { this.showProgress(); + await this._applyCollectionImpl(resource, shell); + this.hideProgress(); + } + + private async _applyCollectionImpl(resource: Resource, shell = this.applicationEnvironment.shell): Promise { const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); @@ -221,7 +226,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); envVarCollection.description = description; - this.hideProgress(); await this.trackTerminalPrompt(shell, resource, env); } From 2d3ce9839f35b7aeb26655956b2dc21fb52793c7 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Wed, 27 Sep 2023 00:42:16 +0100 Subject: [PATCH 297/336] Bump jedi-language-server and jedi (#22069) This picks up the latest versions of each of these, removing pydantic as a dependency and adding support for Python 3.11. Fixes https://github.com/microsoft/vscode-python/issues/22011 Note: this doesn't yet include Jedi support for Python 3.12 so it's likely we'll want to bump Jedi again once that support is released. --- .../jedilsp_requirements/requirements.txt | 92 ++++++------------- 1 file changed, 26 insertions(+), 66 deletions(-) diff --git a/pythonFiles/jedilsp_requirements/requirements.txt b/pythonFiles/jedilsp_requirements/requirements.txt index 64b1bb958f67..3759193344a6 100644 --- a/pythonFiles/jedilsp_requirements/requirements.txt +++ b/pythonFiles/jedilsp_requirements/requirements.txt @@ -13,33 +13,32 @@ attrs==23.1.0 \ cattrs==23.1.2 \ --hash=sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4 \ --hash=sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657 - # via lsprotocol + # via + # jedi-language-server + # lsprotocol docstring-to-markdown==0.12 \ --hash=sha256:40004224b412bd6f64c0f3b85bb357a41341afd66c4b4896709efa56827fb2bb \ --hash=sha256:7df6311a887dccf9e770f51242ec002b19f0591994c4783be49d24cdc1df3737 # via jedi-language-server -exceptiongroup==1.1.2 \ - --hash=sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5 \ - --hash=sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f +exceptiongroup==1.1.3 \ + --hash=sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9 \ + --hash=sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3 # via cattrs -importlib-metadata==3.10.1 \ - --hash=sha256:2ec0faae539743ae6aaa84b49a169670a465f7f5d64e6add98388cc29fd1f2f6 \ - --hash=sha256:c9356b657de65c53744046fa8f7358afe0714a1af7d570c00c3835c2d724a7c1 - # via - # attrs - # jedi-language-server - # typeguard -jedi==0.18.2 \ - --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \ - --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612 +importlib-metadata==6.8.0 \ + --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ + --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 + # via typeguard +jedi==0.19.0 \ + --hash=sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4 \ + --hash=sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e # via jedi-language-server -jedi-language-server==0.40.0 \ - --hash=sha256:53e590400b5cd2f6e363e77a4d824b1883798994b731cb0b4370d103748d30e2 \ - --hash=sha256:bacbae2930b6a8a0f1f284c211672fceec94b4808b0415d1c3352fa4b1ac5ad6 +jedi-language-server==0.41.1 \ + --hash=sha256:3f15ca5cc28e728564f7d63583e171b418025582447ce023512e3f2b2d71ebae \ + --hash=sha256:ca9b3e7f48b70f0988d85ffde4f01dd1ab94c8e0f69e8c6424e6657117b44f91 # via -r pythonFiles\jedilsp_requirements\requirements.in -lsprotocol==2023.0.0a2 \ - --hash=sha256:80aae7e39171b49025876a524937c10be2eb986f4be700ca22ee7d186b8488aa \ - --hash=sha256:c4f2f77712b50d065b17f9b50d2b88c480dc2ce4bbaa56eea8269dbf54bc9701 +lsprotocol==2023.0.0b1 \ + --hash=sha256:ade2cd0fa0ede7965698cb59cd05d3adbd19178fd73e83f72ef57a032fbb9d62 \ + --hash=sha256:f7a2d4655cbd5639f373ddd1789807450c543341fa0a32b064ad30dbb9f510d4 # via # jedi-language-server # pygls @@ -47,44 +46,6 @@ parso==0.8.3 \ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 # via jedi -pydantic==1.10.12 \ - --hash=sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303 \ - --hash=sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe \ - --hash=sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47 \ - --hash=sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494 \ - --hash=sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33 \ - --hash=sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86 \ - --hash=sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d \ - --hash=sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c \ - --hash=sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a \ - --hash=sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565 \ - --hash=sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb \ - --hash=sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62 \ - --hash=sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62 \ - --hash=sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0 \ - --hash=sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523 \ - --hash=sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d \ - --hash=sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405 \ - --hash=sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f \ - --hash=sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b \ - --hash=sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718 \ - --hash=sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed \ - --hash=sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb \ - --hash=sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5 \ - --hash=sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc \ - --hash=sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942 \ - --hash=sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe \ - --hash=sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246 \ - --hash=sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350 \ - --hash=sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303 \ - --hash=sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09 \ - --hash=sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33 \ - --hash=sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8 \ - --hash=sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a \ - --hash=sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1 \ - --hash=sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6 \ - --hash=sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d - # via jedi-language-server pygls==1.0.2 \ --hash=sha256:6d278d29fa6559b0f7a448263c85cb64ec6e9369548b02f1a7944060848b21f9 \ --hash=sha256:888ed63d1f650b4fc64d603d73d37545386ec533c0caac921aed80f80ea946a4 @@ -95,15 +56,14 @@ typeguard==3.0.2 \ --hash=sha256:bbe993854385284ab42fd5bd3bee6f6556577ce8b50696d6cb956d704f286c8e \ --hash=sha256:fee5297fdb28f8e9efcb8142b5ee219e02375509cd77ea9d270b5af826358d5a # via pygls -typing-extensions==4.7.1 \ - --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ - --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 +typing-extensions==4.8.0 \ + --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ + --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef # via # cattrs - # importlib-metadata - # pydantic + # jedi-language-server # typeguard -zipp==3.15.0 \ - --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ - --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 +zipp==3.17.0 \ + --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ + --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 # via importlib-metadata From 6d74f8d0c5a26a7b51429e44d9f19439d4aaddb2 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 27 Sep 2023 10:51:53 -0700 Subject: [PATCH 298/336] Ensure we don't show version selection when user selects useExisting (#22099) Closes https://github.com/microsoft/vscode-python/issues/22084 --- .../provider/condaCreationProvider.ts | 33 ++++++++++++------- .../condaCreationProvider.unit.test.ts | 23 +++++++++++++ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 86e0b56801cd..9dff50c5586d 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -200,21 +200,30 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { - try { - version = await pickPythonVersion(); - } catch (ex) { - if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { - return ex; + async (context) => { + if ( + existingCondaAction === ExistingCondaAction.Recreate || + existingCondaAction === ExistingCondaAction.Create + ) { + try { + version = await pickPythonVersion(); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + if (version === undefined) { + traceError('Python version was not selected for creating conda environment.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected Python version ${version} for creating conda environment.`); + } else if (existingCondaAction === ExistingCondaAction.UseExisting) { + if (context === MultiStepAction.Back) { + return MultiStepAction.Back; } - throw ex; } - if (version === undefined) { - traceError('Python version was not selected for creating conda environment.'); - return MultiStepAction.Cancel; - } - traceInfo(`Selected Python version ${version} for creating conda environment.`); return MultiStepAction.Continue; }, undefined, diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index 3195d1f88ea9..e1344dc5f3ad 100644 --- a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -36,6 +36,7 @@ suite('Conda Creation provider tests', () => { let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; let pickExistingCondaActionStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; setup(() => { pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); @@ -50,6 +51,8 @@ suite('Conda Creation provider tests', () => { pickExistingCondaActionStub = sinon.stub(condaUtils, 'pickExistingCondaAction'); pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.Create); + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + progressMock = typemoq.Mock.ofType(); condaProvider = condaCreationProvider(); }); @@ -254,4 +257,24 @@ suite('Conda Creation provider tests', () => { assert.isTrue(showErrorMessageWithLogsStub.calledOnce); assert.isTrue(pickExistingCondaActionStub.calledOnce); }); + + test('Use existing conda environment', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.UseExisting); + getPrefixCondaEnvPathStub.returns('existing_environment'); + + const result = await condaProvider.createEnvironment(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickPythonVersionStub.notCalled); + assert.isTrue(execObservableStub.notCalled); + assert.isTrue(withProgressStub.notCalled); + + assert.deepStrictEqual(result, { path: 'existing_environment', workspaceFolder: workspace1 }); + }); }); From cc2a5678047a396d6d36092f8ff21182ca18e6b7 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 28 Sep 2023 09:47:41 -0700 Subject: [PATCH 299/336] fix regex split for subtest names (#22107) fixes https://github.com/microsoft/vscode-python/issues/21733. Handles both cases of subtest naming as described here by ChatGPT: When you use self.subTest(i=i), you're explicitly naming the argument i. This causes subTest to use the key=value format for the sub-test's description. Therefore, the sub-test name becomes: `test_subtests.NumbersTest2.test_even2 (i='h i')` However, when you use self.subTest(i), you're passing a positional argument. In this case, subTest doesn't have a key for the argument, so it simply uses the value in square brackets: `test_subtests.NumbersTest2.test_even2 [h i]` --- .../testController/common/resultResolver.ts | 19 +++---- .../testing/testController/common/utils.ts | 17 ++++++ .../testing/testController/utils.unit.test.ts | 56 +++++++++++++++++++ 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 78883bdcf8fb..79cee6452a8c 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -11,7 +11,7 @@ import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testI import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { splitLines } from '../../../common/stringUtils'; -import { buildErrorNodeOptions, fixLogLines, populateTestTree } from './utils'; +import { buildErrorNodeOptions, fixLogLines, populateTestTree, splitTestNameWithRegex } from './utils'; import { Deferred } from '../../../common/utils/async'; export class PythonResultResolver implements ITestResultResolver { @@ -216,9 +216,8 @@ export class PythonResultResolver implements ITestResultResolver { }); } } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const subtestId = keyTemp.split(' ')[1]; + // split on [] or () based on how the subtest is setup. + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); const data = rawTestExecData.result[keyTemp]; // find the subtest's parent test item @@ -227,7 +226,10 @@ export class PythonResultResolver implements ITestResultResolver { if (subtestStats) { subtestStats.failed += 1; } else { - this.subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); + this.subTestStats.set(parentTestCaseId, { + failed: 1, + passed: 0, + }); runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); @@ -253,11 +255,8 @@ export class PythonResultResolver implements ITestResultResolver { throw new Error('Parent test item not found'); } } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { - // split only on first " [" since the subtest ID has the parent test ID in the first part of the ID. - const index = keyTemp.indexOf(' ['); - const parentTestCaseId = keyTemp.substring(0, index); - // add one to index to remove the space from the start of the subtest ID - const subtestId = keyTemp.substring(index + 1, keyTemp.length); + // split on [] or () based on how the subtest is setup. + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); // find the subtest's parent test item diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 1272ff37fb5d..c58850050590 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -320,3 +320,20 @@ export function createEOTPayload(executionBool: boolean): EOTTestPayload { eot: true, } as EOTTestPayload; } + +/** + * Splits a test name into its parent test name and subtest unique section. + * + * @param testName The full test name string. + * @returns A tuple where the first item is the parent test name and the second item is the subtest section or `testName` if no subtest section exists. + */ +export function splitTestNameWithRegex(testName: string): [string, string] { + // If a match is found, return the parent test name and the subtest (whichever was captured between parenthesis or square brackets). + // Otherwise, return the entire testName for the parent and entire testName for the subtest. + const regex = /^(.*?) ([\[(].*[\])])$/; + const match = testName.match(regex); + if (match) { + return [match[1].trim(), match[2] || match[3] || testName]; + } + return [testName, testName]; +} diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts index 9168abc7041f..12100252d1a9 100644 --- a/src/test/testing/testController/utils.unit.test.ts +++ b/src/test/testing/testController/utils.unit.test.ts @@ -8,6 +8,7 @@ import { JSONRPC_UUID_HEADER, ExtractJsonRPCData, parseJsonRPCHeadersAndData, + splitTestNameWithRegex, } from '../../../client/testing/testController/common/utils'; suite('Test Controller Utils: JSON RPC', () => { @@ -65,3 +66,58 @@ suite('Test Controller Utils: JSON RPC', () => { assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString); }); }); + +suite('Test Controller Utils: Other', () => { + interface TestCase { + name: string; + input: string; + expectedParent: string; + expectedSubtest: string; + } + + const testCases: Array = [ + { + name: 'Single parameter, named', + input: 'test_package.ClassName.test_method (param=value)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param=value)', + }, + { + name: 'Single parameter, unnamed', + input: 'test_package.ClassName.test_method [value]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value]', + }, + { + name: 'Multiple parameters, named', + input: 'test_package.ClassName.test_method (param1=value1, param2=value2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value1, param2=value2)', + }, + { + name: 'Multiple parameters, unnamed', + input: 'test_package.ClassName.test_method [value1, value2]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value1, value2]', + }, + { + name: 'Names with special characters', + input: 'test_package.ClassName.test_method (param1=value/1, param2=value+2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value/1, param2=value+2)', + }, + { + name: 'Names with spaces', + input: 'test_package.ClassName.test_method ["a b c d"]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '["a b c d"]', + }, + ]; + + testCases.forEach((testCase) => { + test(`splitTestNameWithRegex: ${testCase.name}`, () => { + const splitResult = splitTestNameWithRegex(testCase.input); + assert.deepStrictEqual(splitResult, [testCase.expectedParent, testCase.expectedSubtest]); + }); + }); +}); From 0fe920f067ce67632a820d22687ad182c1550610 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 28 Sep 2023 11:10:21 -0700 Subject: [PATCH 300/336] ignore payload key-value if value is empty (#22105) fixes https://github.com/microsoft/vscode-python/issues/22104 --- .../testing/testController/common/utils.ts | 6 ++++-- .../testing/common/testingPayloadsEot.test.ts | 5 +++++ .../testController/payloadTestCases.ts | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index c58850050590..f34d0172abd6 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -114,8 +114,10 @@ export function parseJsonRPCHeadersAndData(rawData: string): ParsedRPCHeadersAnd break; } const [key, value] = line.split(':'); - if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { - headerMap.set(key.trim(), value.trim()); + if (value.trim()) { + if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { + headerMap.set(key.trim(), value.trim()); + } } } diff --git a/src/test/testing/common/testingPayloadsEot.test.ts b/src/test/testing/common/testingPayloadsEot.test.ts index 9025248c66ff..a30b1efe288c 100644 --- a/src/test/testing/common/testingPayloadsEot.test.ts +++ b/src/test/testing/common/testingPayloadsEot.test.ts @@ -27,6 +27,7 @@ import { PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY, DataWithPayloadChunks, PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY, + PAYLOAD_ONLY_HEADER_MULTI_CHUNK, } from '../testController/payloadTestCases'; import { traceLog } from '../../../client/logging'; @@ -37,6 +38,10 @@ export interface TestCase { } const testCases: Array = [ + { + name: 'header in single chunk edge case', + value: PAYLOAD_ONLY_HEADER_MULTI_CHUNK(FAKE_UUID), + }, { name: 'single payload single chunk', value: PAYLOAD_SINGLE_CHUNK(FAKE_UUID), diff --git a/src/test/testing/testController/payloadTestCases.ts b/src/test/testing/testController/payloadTestCases.ts index 5ddcc0edecf9..40b2113904dd 100644 --- a/src/test/testing/testController/payloadTestCases.ts +++ b/src/test/testing/testController/payloadTestCases.ts @@ -82,6 +82,25 @@ export function PAYLOAD_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { }; } +// more than one payload, split so the first one is only 'Content-Length' to confirm headers +// with null values are ignored +export function PAYLOAD_ONLY_HEADER_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + const payloadArray: string[] = []; + const result = JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + + const val = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + const firstSpaceIndex = val.indexOf(' '); + const payload1 = val.substring(0, firstSpaceIndex); + const payload2 = val.substring(firstSpaceIndex); + payloadArray.push(payload1); + payloadArray.push(payload2); + payloadArray.push(EOT_PAYLOAD); + return { + payloadArray, + data: result, + }; +} + // single payload divided by an arbitrary character and split across payloads export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayloadChunks { const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); From c3214c0344fdb96dde38ff424f0abfe56af13a51 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 28 Sep 2023 11:22:19 -0700 Subject: [PATCH 301/336] switch to verbose for raw data logs (#22110) fixes https://github.com/microsoft/vscode-python/issues/22095 --- src/client/testing/testController/common/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index d47587386069..1f9c0223d3fd 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -12,7 +12,7 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { traceError, traceInfo, traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; @@ -87,7 +87,7 @@ export class PythonTestServer implements ITestServer, Disposable { // what payload is so small it doesn't include the whole UUID think got this if (extractedJsonPayload.uuid !== undefined && extractedJsonPayload.cleanedJsonData !== undefined) { // if a full json was found in the buffer, fire the data received event then keep cycling with the remaining raw data. - traceLog(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); + traceVerbose(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); this._fireDataReceived(extractedJsonPayload.uuid, extractedJsonPayload.cleanedJsonData); } buffer = Buffer.from(extractedJsonPayload.remainingRawData); From 2579b15ca68461f578371e132f29747ad26130d6 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 28 Sep 2023 13:03:33 -0700 Subject: [PATCH 302/336] Fire active environment change event if selected environment is deleted (#22113) Closes https://github.com/microsoft/vscode-python/issues/22066 --- src/client/interpreter/interpreterService.ts | 29 +++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index b595fc2365a8..c97a35c4a973 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -85,6 +85,11 @@ export class InterpreterService implements Disposable, IInterpreterService { private readonly didChangeInterpreterInformation = new EventEmitter(); + private readonly activeInterpreterPaths = new Map< + string, + { path: string; workspaceFolder: WorkspaceFolder | undefined } + >(); + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, @@ -100,10 +105,12 @@ export class InterpreterService implements Disposable, IInterpreterService { const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource); - this.ensureEnvironmentContainsPython( - this.configService.getSettings(resource).pythonPath, - workspaceFolder, - ).ignoreErrors(); + const path = this.configService.getSettings(resource).pythonPath; + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path, workspaceFolder }); + this.ensureEnvironmentContainsPython(path, workspaceFolder).ignoreErrors(); } public initialize(): void { @@ -155,6 +162,16 @@ export class InterpreterService implements Disposable, IInterpreterService { const interpreter = e.old ?? e.new; if (interpreter) { this.didChangeInterpreterInformation.fire(interpreter); + for (const { path, workspaceFolder } of this.activeInterpreterPaths.values()) { + if (path === interpreter.path && !e.new) { + // If the active environment got deleted, notify it. + this.didChangeInterpreterEmitter.fire(workspaceFolder?.uri); + reportActiveInterpreterChanged({ + path, + resource: workspaceFolder, + }); + } + } } }), ); @@ -246,6 +263,10 @@ export class InterpreterService implements Disposable, IInterpreterService { path: pySettings.pythonPath, resource: workspaceFolder, }); + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path: pySettings.pythonPath, workspaceFolder }); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); await this.ensureEnvironmentContainsPython(this._pythonPathSetting, workspaceFolder); From f577ce6d15af5a68a4c06494ea1500921bc81af3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:51:23 -0700 Subject: [PATCH 303/336] Align env type capitalization with tool recommendation (#22103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maybe it should also be Poetry? 🤷 Fixes #22094 --- src/client/pythonEnvironments/info/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index ee2ff9d7cc22..17e8958f6310 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -98,7 +98,7 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string return 'conda'; } case EnvironmentType.Pipenv: { - return 'pipenv'; + return 'Pipenv'; } case EnvironmentType.Pyenv: { return 'pyenv'; @@ -110,16 +110,16 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string return 'virtualenv'; } case EnvironmentType.MicrosoftStore: { - return 'microsoft store'; + return 'Microsoft Store'; } case EnvironmentType.Poetry: { - return 'poetry'; + return 'Poetry'; } case EnvironmentType.VirtualEnvWrapper: { return 'virtualenvwrapper'; } case EnvironmentType.ActiveState: { - return 'activestate'; + return 'ActiveState'; } default: { return ''; From e87a83cc34aa72ed6e29f29255c29ff4eb28978a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 28 Sep 2023 14:52:14 -0700 Subject: [PATCH 304/336] Correct display name for env kinds (#22115) Closes https://github.com/microsoft/vscode-python/issues/22094 --- src/client/pythonEnvironments/base/info/envKind.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/envKind.ts b/src/client/pythonEnvironments/base/info/envKind.ts index 8828003c5ce7..ff53a57d2f45 100644 --- a/src/client/pythonEnvironments/base/info/envKind.ts +++ b/src/client/pythonEnvironments/base/info/envKind.ts @@ -12,15 +12,15 @@ export function getKindDisplayName(kind: PythonEnvKind): string { for (const [candidate, value] of [ // Note that Unknown is excluded here. [PythonEnvKind.System, 'system'], - [PythonEnvKind.MicrosoftStore, 'microsoft store'], + [PythonEnvKind.MicrosoftStore, 'Microsoft Store'], [PythonEnvKind.Pyenv, 'pyenv'], - [PythonEnvKind.Poetry, 'poetry'], + [PythonEnvKind.Poetry, 'Poetry'], [PythonEnvKind.Custom, 'custom'], // For now we treat OtherGlobal like Unknown. [PythonEnvKind.Venv, 'venv'], [PythonEnvKind.VirtualEnv, 'virtualenv'], [PythonEnvKind.VirtualEnvWrapper, 'virtualenv'], - [PythonEnvKind.Pipenv, 'pipenv'], + [PythonEnvKind.Pipenv, 'Pipenv'], [PythonEnvKind.Conda, 'conda'], [PythonEnvKind.ActiveState, 'ActiveState'], // For now we treat OtherVirtual like Unknown. From 66c7db6e369ce02a410deca0913730a87f74df1b Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 29 Sep 2023 15:05:02 -0700 Subject: [PATCH 305/336] check existence of value in header before trim process json prc (#22116) Made extra tests to validate that `parseJsonRPCHeadersAndData` works as expected and uncovered a bug. Added check to see if the value is null before trim. --- .../testing/testController/common/utils.ts | 2 +- .../testing/testController/utils.unit.test.ts | 142 +++++++++++------- 2 files changed, 91 insertions(+), 53 deletions(-) diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index f34d0172abd6..f5f416529c42 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -114,7 +114,7 @@ export function parseJsonRPCHeadersAndData(rawData: string): ParsedRPCHeadersAnd break; } const [key, value] = line.split(':'); - if (value.trim()) { + if (value && value.trim()) { if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { headerMap.set(key.trim(), value.trim()); } diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts index 12100252d1a9..014261a40232 100644 --- a/src/test/testing/testController/utils.unit.test.ts +++ b/src/test/testing/testController/utils.unit.test.ts @@ -65,59 +65,97 @@ suite('Test Controller Utils: JSON RPC', () => { assert.deepStrictEqual(rpcContent.extractedJSON, json); assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString); }); -}); -suite('Test Controller Utils: Other', () => { - interface TestCase { - name: string; - input: string; - expectedParent: string; - expectedSubtest: string; - } - - const testCases: Array = [ - { - name: 'Single parameter, named', - input: 'test_package.ClassName.test_method (param=value)', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '(param=value)', - }, - { - name: 'Single parameter, unnamed', - input: 'test_package.ClassName.test_method [value]', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '[value]', - }, - { - name: 'Multiple parameters, named', - input: 'test_package.ClassName.test_method (param1=value1, param2=value2)', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '(param1=value1, param2=value2)', - }, - { - name: 'Multiple parameters, unnamed', - input: 'test_package.ClassName.test_method [value1, value2]', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '[value1, value2]', - }, - { - name: 'Names with special characters', - input: 'test_package.ClassName.test_method (param1=value/1, param2=value+2)', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '(param1=value/1, param2=value+2)', - }, - { - name: 'Names with spaces', - input: 'test_package.ClassName.test_method ["a b c d"]', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '["a b c d"]', - }, - ]; - - testCases.forEach((testCase) => { - test(`splitTestNameWithRegex: ${testCase.name}`, () => { - const splitResult = splitTestNameWithRegex(testCase.input); - assert.deepStrictEqual(splitResult, [testCase.expectedParent, testCase.expectedSubtest]); + test('Valid constant', async () => { + const data = `{"cwd": "/Users/eleanorboyd/testingFiles/inc_dec_example", "status": "success", "result": {"test_dup_class.test_a.TestSomething.test_a": {"test": "test_dup_class.test_a.TestSomething.test_a", "outcome": "success", "message": "None", "traceback": null, "subtest": null}}}`; + const secondPayload = `Content-Length: 270 +Content-Type: application/json +Request-uuid: 496c86b1-608f-4886-9436-ec00538e144c + +${data}`; + const payload = `Content-Length: 270 +Content-Type: application/json +Request-uuid: 496c86b1-608f-4886-9436-ec00538e144c + +${data}${secondPayload}`; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + assert.deepStrictEqual(rpcHeaders.headers.size, 3); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, data); + assert.deepStrictEqual(rpcContent.remainingRawData, secondPayload); + }); + test('Valid content length as only header with carriage return', async () => { + const payload = `Content-Length: 7 + `; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + assert.deepStrictEqual(rpcHeaders.headers.size, 1); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, ''); + assert.deepStrictEqual(rpcContent.remainingRawData, ''); + }); + test('Valid content length header with no value', async () => { + const payload = `Content-Length:`; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, ''); + assert.deepStrictEqual(rpcContent.remainingRawData, ''); + }); + + suite('Test Controller Utils: Other', () => { + interface TestCase { + name: string; + input: string; + expectedParent: string; + expectedSubtest: string; + } + + const testCases: Array = [ + { + name: 'Single parameter, named', + input: 'test_package.ClassName.test_method (param=value)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param=value)', + }, + { + name: 'Single parameter, unnamed', + input: 'test_package.ClassName.test_method [value]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value]', + }, + { + name: 'Multiple parameters, named', + input: 'test_package.ClassName.test_method (param1=value1, param2=value2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value1, param2=value2)', + }, + { + name: 'Multiple parameters, unnamed', + input: 'test_package.ClassName.test_method [value1, value2]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value1, value2]', + }, + { + name: 'Names with special characters', + input: 'test_package.ClassName.test_method (param1=value/1, param2=value+2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value/1, param2=value+2)', + }, + { + name: 'Names with spaces', + input: 'test_package.ClassName.test_method ["a b c d"]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '["a b c d"]', + }, + ]; + + testCases.forEach((testCase) => { + test(`splitTestNameWithRegex: ${testCase.name}`, () => { + const splitResult = splitTestNameWithRegex(testCase.input); + assert.deepStrictEqual(splitResult, [testCase.expectedParent, testCase.expectedSubtest]); + }); }); }); }); From aeee067b7aef2c9f52d81031e0e7da408a47aa3b Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 2 Oct 2023 11:30:40 -0700 Subject: [PATCH 306/336] Update version and packages for release candidate (#22127) --- package-lock.json | 93 +++++++++++++++++++++------------- package.json | 2 +- pythonFiles/install_debugpy.py | 2 +- 3 files changed, 59 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70205a90f00e..8885d4532477 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.17.0-dev", + "version": "2023.18.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.17.0-dev", + "version": "2023.18.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.82.0-20230830" + "vscode": "^1.82.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -5795,13 +5795,20 @@ } }, "node_modules/eslint/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/eslint/node_modules/doctrine": { @@ -7002,9 +7009,9 @@ "dev": true }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -8735,13 +8742,20 @@ } }, "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/istanbul-reports": { @@ -10193,13 +10207,20 @@ } }, "node_modules/nock/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/nock/node_modules/semver": { @@ -19727,12 +19748,12 @@ } }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "doctrine": { @@ -20941,9 +20962,9 @@ "dev": true }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, "get-intrinsic": { @@ -22251,12 +22272,12 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } } } @@ -23408,12 +23429,12 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "semver": { diff --git a/package.json b/package.json index ef9d0aa6c033..5d7120a74fa2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.17.0-dev", + "version": "2023.18.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, diff --git a/pythonFiles/install_debugpy.py b/pythonFiles/install_debugpy.py index cabb620ea1f2..9377d00237d7 100644 --- a/pythonFiles/install_debugpy.py +++ b/pythonFiles/install_debugpy.py @@ -13,7 +13,7 @@ DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") DEBUGGER_PACKAGE = "debugpy" DEBUGGER_PYTHON_ABI_VERSIONS = ("cp310",) -DEBUGGER_VERSION = "1.6.7" # can also be "latest" +DEBUGGER_VERSION = "1.8.0" # can also be "latest" def _contains(s, parts=()): From 4a3f855c292384fc14c157961b46be8b35db5b7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 11:39:14 -0700 Subject: [PATCH 307/336] Bump packaging from 23.1 to 23.2 (#22124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [packaging](https://github.com/pypa/packaging) from 23.1 to 23.2.
Release notes

Sourced from packaging's releases.

23.2

What's Changed

New Contributors

Full Changelog: https://github.com/pypa/packaging/compare/23.1...23.2

Changelog

Sourced from packaging's changelog.

23.2 - 2023-10-01


* Document calendar-based versioning scheme (:issue:`716`)
* Enforce that the entire marker string is parsed (:issue:`687`)
* Requirement parsing no longer automatically validates the URL
(:issue:`120`)
* Canonicalize names for requirements comparison (:issue:`644`)
* Introduce ``metadata.Metadata`` (along with
``metadata.ExceptionGroup`` and ``metadata.InvalidMetadata``;
:issue:`570`)
* Introduce the ``validate`` keyword parameter to
``utils.validate_name()`` (:issue:`570`)
* Introduce ``utils.is_normalized_name()`` (:issue:`570`)
* Make ``utils.parse_sdist_filename()`` and
``utils.parse_wheel_filename()``
raise ``InvalidSdistFilename`` and ``InvalidWheelFilename``,
respectively,
  when the version component of the name is invalid
Commits
  • b3a5d7d Bump for release
  • d7ce40d Fix code blocks in CHANGELOG.md (#724)
  • 524b701 parse_{sdist,wheel}_filename: don't raise InvalidVersion (#721)
  • b509bef Typing annotations fixed (#723)
  • 0206c39 Bump pip version to avoid known vulnerabilities (#720)
  • 7023537 fix: Update copyright date for docs (#713)
  • 39786bb Document use of calendar-based versioning scheme (#717)
  • c1346df fix: Detect when a platform is 32-bit more accurately (#711)
  • 7e68d82 Correct rST syntax in CHANGELOG.rst (#709)
  • 61e6efb Support enriched metadata in packaging.metadata (#686)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=packaging&package-manager=pip&previous-version=23.1&new-version=23.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8ea311d28cb1..205b9fc4804c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,9 +12,9 @@ microvenv==2023.2.0 \ --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ --hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3 # via -r requirements.in -packaging==23.1 \ - --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ - --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f +packaging==23.2 \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ + --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 # via -r requirements.in tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ @@ -23,9 +23,7 @@ tomli==2.0.1 \ typing-extensions==4.7.1 \ --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 - # via - # -r requirements.in - # importlib-metadata + # via -r requirements.in zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 From 4f82418173be3b989e07fe2cbb076b006ad8fc83 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 2 Oct 2023 11:51:07 -0700 Subject: [PATCH 308/336] Update version for pre-release (#22129) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8885d4532477..2352bcf96c31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.18.0-rc", + "version": "2023.19.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.18.0-rc", + "version": "2023.19.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 5d7120a74fa2..c70a3023c267 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.18.0-rc", + "version": "2023.19.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 590c12a1a5150490d32fbe0b468a11a7df1daec7 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 2 Oct 2023 13:12:41 -0700 Subject: [PATCH 309/336] switch end to end tests to randomized substring (#22114) add in tests which are randomized to provide more testing for the issue that created `https://github.com/microsoft/vscode-python/issues/22104` --- .../testController/payloadTestCases.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/test/testing/testController/payloadTestCases.ts b/src/test/testing/testController/payloadTestCases.ts index 40b2113904dd..f7f94a926f5f 100644 --- a/src/test/testing/testController/payloadTestCases.ts +++ b/src/test/testing/testController/payloadTestCases.ts @@ -50,6 +50,21 @@ const SINGLE_PYTEST_PAYLOAD_TWO = { }, }; +function splitIntoRandomSubstrings(payload: string): string[] { + // split payload at random + const splitPayload = []; + const n = payload.length; + let remaining = n; + while (remaining > 0) { + // Randomly split what remains of the string + const randomSize = Math.floor(Math.random() * remaining) + 1; + splitPayload.push(payload.slice(n - remaining, n - remaining + randomSize)); + + remaining -= randomSize; + } + return splitPayload; +} + export function createPayload(uuid: string, data: unknown): string { return `Content-Length: ${JSON.stringify(data).length} Content-Type: application/json @@ -104,13 +119,7 @@ export function PAYLOAD_ONLY_HEADER_MULTI_CHUNK(uuid: string): DataWithPayloadCh // single payload divided by an arbitrary character and split across payloads export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayloadChunks { const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); - // payload length is know to be >200 - const splitPayload: Array = [ - payload.substring(0, 50), - payload.substring(50, 100), - payload.substring(100, 150), - payload.substring(150), - ]; + const splitPayload = splitIntoRandomSubstrings(payload); const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result); splitPayload.push(EOT_PAYLOAD); return { @@ -121,12 +130,8 @@ export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayload // here a payload is split across the buffer chunks and there are multiple payloads in a single buffer chunk export function PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(uuid: string): DataWithPayloadChunks { - // payload1 length is know to be >200 - const payload1 = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); - const payload2 = createPayload(uuid, SINGLE_PYTEST_PAYLOAD_TWO); - - // chunk 1 is 50 char of payload1, chunk 2 is 50-end of payload1 and all of payload2 - const splitPayload: Array = [payload1.substring(0, 100), payload1.substring(100).concat(payload2)]; + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD).concat(createPayload(uuid, SINGLE_PYTEST_PAYLOAD_TWO)); + const splitPayload = splitIntoRandomSubstrings(payload); const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result).concat( JSON.stringify(SINGLE_PYTEST_PAYLOAD_TWO.result), ); From add82a0a773d5f38e294852d15a3a2eb9f90c1cc Mon Sep 17 00:00:00 2001 From: Peter Law Date: Mon, 2 Oct 2023 21:54:34 +0100 Subject: [PATCH 310/336] Bump Jedi to 0.19.1 for Python 3.12 support (#22132) Follows from https://github.com/microsoft/vscode-python/issues/22011#issuecomment-1742682966 --- pythonFiles/jedilsp_requirements/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pythonFiles/jedilsp_requirements/requirements.txt b/pythonFiles/jedilsp_requirements/requirements.txt index 3759193344a6..889f021670bf 100644 --- a/pythonFiles/jedilsp_requirements/requirements.txt +++ b/pythonFiles/jedilsp_requirements/requirements.txt @@ -28,9 +28,9 @@ importlib-metadata==6.8.0 \ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 # via typeguard -jedi==0.19.0 \ - --hash=sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4 \ - --hash=sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e +jedi==0.19.1 \ + --hash=sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd \ + --hash=sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0 # via jedi-language-server jedi-language-server==0.41.1 \ --hash=sha256:3f15ca5cc28e728564f7d63583e171b418025582447ce023512e3f2b2d71ebae \ From fc62bd8d9a2431eb6199decf58c88653de3f9a37 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 2 Oct 2023 15:21:15 -0700 Subject: [PATCH 311/336] Migrate extension to node 18 (#22135) --- .devcontainer/Dockerfile | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- .nvmrc | 2 +- build/azure-pipeline.pre-release.yml | 2 +- build/azure-pipeline.stable.yml | 2 +- build/azure-pipelines/pipeline.yml | 6 +- gulpfile.js | 21 +- package-lock.json | 352 +++++------------- package.json | 5 +- pythonExtensionApi/package-lock.json | 103 ++++- pythonExtensionApi/package.json | 5 +- .../environmentManagers/conda.unit.test.ts | 1 + src/test/standardTest.ts | 7 +- 14 files changed, 219 insertions(+), 293 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 5fbf068de65f..3e7e9e9cf091 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/typescript-node:16-bookworm +FROM mcr.microsoft.com/devcontainers/typescript-node:18-bookworm RUN apt-get install -y wget bzip2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d121564d385..d1509a7b433e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: - 'release-*' env: - NODE_VERSION: 16.17.1 + NODE_VERSION: 18.17.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index aa223782de62..b7d2ed0c0545 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -8,7 +8,7 @@ on: - release* env: - NODE_VERSION: 16.17.1 + NODE_VERSION: 18.17.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). Also enables a reporter which exits the process running the tests if it haven't already. ARTIFACT_NAME_VSIX: ms-python-insiders-vsix diff --git a/.nvmrc b/.nvmrc index e0325e5adb60..860cc5000ae6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.17.1 +v18.17.1 diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index eed32b70c35d..bb52f983d02e 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -33,7 +33,7 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '16.17.1' + versionSpec: '18.17.1' displayName: Select Node version - task: UsePythonVersion@0 diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index c147f8b55164..02f8bd38cf81 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -28,7 +28,7 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '16.17.1' + versionSpec: '18.17.1' displayName: Select Node version - task: UsePythonVersion@0 diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml index 85b41c16efc0..adb2fa5d1c30 100644 --- a/build/azure-pipelines/pipeline.yml +++ b/build/azure-pipelines/pipeline.yml @@ -37,13 +37,13 @@ extends: testPlatforms: - name: Linux nodeVersions: - - 16.17.1 + - 18.17.1 - name: MacOS nodeVersions: - - 16.17.1 + - 18.17.1 - name: Windows nodeVersions: - - 16.17.1 + - 18.17.1 testSteps: - template: /build/azure-pipelines/templates/test-steps.yml@self parameters: diff --git a/gulpfile.js b/gulpfile.js index 66f96bf48ec0..4dcc03252d16 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,18 +39,19 @@ gulp.task('compileCore', (done) => { .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); }); -const apiTsProject = ts.createProject('./pythonExtensionApi/tsconfig.json', { typescript }); - gulp.task('compileApi', (done) => { - let failed = false; - apiTsProject - .src() - .pipe(apiTsProject()) - .on('error', () => { - failed = true; + spawnAsync('npm', ['run', 'compileApi'], undefined, true) + .then((stdout) => { + if (stdout.includes('error')) { + done(new Error(stdout)); + } else { + done(); + } }) - .js.pipe(gulp.dest('./pythonExtensionApi/out')) - .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); + .catch((ex) => { + console.log(ex); + done(new Error('TypeScript compilation errors', ex)); + }); }); gulp.task('compile', gulp.series('compileCore', 'compileApi')); diff --git a/package-lock.json b/package-lock.json index 2352bcf96c31..5d1ee32bc08b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "@types/md5": "^2.1.32", "@types/mocha": "^9.1.0", "@types/nock": "^10.0.3", - "@types/node": "^16.17.0", + "@types/node": "^18.17.1", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", @@ -73,7 +73,7 @@ "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", - "@vscode/test-electron": "^2.1.3", + "@vscode/test-electron": "^2.3.4", "@vscode/vsce": "^2.18.0", "bent": "^7.3.12", "chai": "^4.1.2", @@ -1553,9 +1553,9 @@ } }, "node_modules/@types/node": { - "version": "16.18.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.25.tgz", - "integrity": "sha512-rUDO6s9Q/El1R1I21HG4qw/LstTHCPO/oQNAwI/4b2f9EWvMnqt4d3HJwPMawfZ3UvodB8516Yg+VAq54YM+eA==", + "version": "18.17.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.14.tgz", + "integrity": "sha512-ZE/5aB73CyGqgQULkLG87N9GnyGe5TcQjv34pwS8tfBs1IkCh0ASM69mydb2znqd6v0eX+9Ytvk6oQRqu8T1Vw==", "dev": true }, "node_modules/@types/semver": { @@ -1854,18 +1854,18 @@ } }, "node_modules/@vscode/test-electron": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.3.tgz", - "integrity": "sha512-ps/yJ/9ToUZtR1dHfWi1mDXtep1VoyyrmGKC3UnIbScToRQvbUjyy1VMqnMEW3EpMmC3g7+pyThIPtPyCLHyow==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.4.tgz", + "integrity": "sha512-eWzIqXMhvlcoXfEFNWrVu/yYT5w6De+WZXR/bafUQhAp8+8GkQo95Oe14phwiRUPv8L+geAKl/QM2+PoT3YW3g==", "dev": true, "dependencies": { "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" + "jszip": "^3.10.1", + "semver": "^7.5.2" }, "engines": { - "node": ">=8.9.3" + "node": ">=16" } }, "node_modules/@vscode/vsce": { @@ -3023,15 +3023,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/big-integer": { - "version": "1.6.49", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.49.tgz", - "integrity": "sha512-KJ7VhqH+f/BOt9a3yMwJNmcZjG53ijWMTjSAGMveQWyLwqIiwkjNP5PFgDob3Snnx86SjDj6I89fIbv0dkQeNw==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -3041,19 +3032,6 @@ "node": "*" } }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3220,12 +3198,6 @@ "pako": "~1.0.5" } }, - "node_modules/browserify-zlib/node_modules/pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true - }, "node_modules/browserslist": { "version": "4.21.9", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", @@ -3319,30 +3291,12 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true, - "engines": { - "node": ">=0.2.0" - } - }, "node_modules/builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -3501,18 +3455,6 @@ "chai": ">= 2.1.2 < 5" } }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5010,15 +4952,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, "node_modules/duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -6955,33 +6888,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -7902,6 +7808,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -8926,6 +8838,18 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/just-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", @@ -9054,6 +8978,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -9082,12 +9015,6 @@ "uc.micro": "^1.0.1" } }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, "node_modules/load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -11144,6 +11071,12 @@ "node": ">=8" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -13685,15 +13618,6 @@ "node": ">=6" } }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", @@ -14342,30 +14266,6 @@ "node": ">=8" } }, - "node_modules/unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "node_modules/unzipper/node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, "node_modules/upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", @@ -16647,9 +16547,9 @@ } }, "@types/node": { - "version": "16.18.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.25.tgz", - "integrity": "sha512-rUDO6s9Q/El1R1I21HG4qw/LstTHCPO/oQNAwI/4b2f9EWvMnqt4d3HJwPMawfZ3UvodB8516Yg+VAq54YM+eA==", + "version": "18.17.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.14.tgz", + "integrity": "sha512-ZE/5aB73CyGqgQULkLG87N9GnyGe5TcQjv34pwS8tfBs1IkCh0ASM69mydb2znqd6v0eX+9Ytvk6oQRqu8T1Vw==", "dev": true }, "@types/semver": { @@ -16863,15 +16763,15 @@ } }, "@vscode/test-electron": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.3.tgz", - "integrity": "sha512-ps/yJ/9ToUZtR1dHfWi1mDXtep1VoyyrmGKC3UnIbScToRQvbUjyy1VMqnMEW3EpMmC3g7+pyThIPtPyCLHyow==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.4.tgz", + "integrity": "sha512-eWzIqXMhvlcoXfEFNWrVu/yYT5w6De+WZXR/bafUQhAp8+8GkQo95Oe14phwiRUPv8L+geAKl/QM2+PoT3YW3g==", "dev": true, "requires": { "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" + "jszip": "^3.10.1", + "semver": "^7.5.2" } }, "@vscode/vsce": { @@ -17786,28 +17686,12 @@ } } }, - "big-integer": { - "version": "1.6.49", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.49.tgz", - "integrity": "sha512-KJ7VhqH+f/BOt9a3yMwJNmcZjG53ijWMTjSAGMveQWyLwqIiwkjNP5PFgDob3Snnx86SjDj6I89fIbv0dkQeNw==", - "dev": true - }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -17965,14 +17849,6 @@ "dev": true, "requires": { "pako": "~1.0.5" - }, - "dependencies": { - "pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true - } } }, "browserslist": { @@ -18031,24 +17907,12 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true - }, "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true - }, "builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -18171,15 +18035,6 @@ "check-error": "^1.0.2" } }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -19389,15 +19244,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -20915,29 +20761,6 @@ "dev": true, "optional": true }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -21650,6 +21473,12 @@ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -22411,6 +22240,18 @@ "object.assign": "^4.1.2" } }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "just-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", @@ -22517,6 +22358,15 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, "liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -22542,12 +22392,6 @@ "uc.micro": "^1.0.1" } }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -24156,6 +24000,12 @@ "release-zalgo": "^1.0.0" } }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -26137,12 +25987,6 @@ "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", "dev": true }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true - }, "trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", @@ -26636,32 +26480,6 @@ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==" }, - "unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - }, - "dependencies": { - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - } - } - }, "upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", diff --git a/package.json b/package.json index c70a3023c267..47faec663015 100644 --- a/package.json +++ b/package.json @@ -2032,6 +2032,7 @@ "package": "gulp clean && gulp prePublishBundle && vsce package -o ms-python-insiders.vsix", "prePublish": "gulp clean && gulp prePublishNonBundle", "compile": "tsc -watch -p ./", + "compileApi": "node ./node_modules/typescript/lib/tsc.js -b ./pythonExtensionApi/tsconfig.json", "compiled": "deemon npm run compile", "kill-compiled": "deemon --kill npm run compile", "checkDependencies": "gulp checkDependencies", @@ -2116,7 +2117,7 @@ "@types/md5": "^2.1.32", "@types/mocha": "^9.1.0", "@types/nock": "^10.0.3", - "@types/node": "^16.17.0", + "@types/node": "^18.17.1", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", @@ -2129,7 +2130,7 @@ "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", - "@vscode/test-electron": "^2.1.3", + "@vscode/test-electron": "^2.3.4", "@vscode/vsce": "^2.18.0", "bent": "^7.3.12", "chai": "^4.1.2", diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index 1f4098e1b0de..ef6914e0e786 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -10,10 +10,11 @@ "license": "MIT", "devDependencies": { "@types/vscode": "^1.78.0", + "source-map": "^0.8.0-beta.0", "typescript": "5.0.4" }, "engines": { - "node": ">=16.17.1", + "node": ">=18.17.1", "vscode": "^1.78.0" } }, @@ -23,6 +24,42 @@ "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", "dev": true }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/typescript": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", @@ -35,6 +72,23 @@ "engines": { "node": ">=12.20" } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } } }, "dependencies": { @@ -44,11 +98,58 @@ "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "requires": { + "whatwg-url": "^7.0.0" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "typescript": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } } } } diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index baabf85d6549..9e58f1a2400c 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -13,7 +13,7 @@ "main": "./out/main.js", "types": "./out/main.d.ts", "engines": { - "node": ">=16.17.1", + "node": ">=18.17.1", "vscode": "^1.78.0" }, "license": "MIT", @@ -27,7 +27,8 @@ }, "devDependencies": { "typescript": "5.0.4", - "@types/vscode": "^1.78.0" + "@types/vscode": "^1.78.0", + "source-map": "^0.8.0-beta.0" }, "scripts": { "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", diff --git a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts index ca0e24d5f3d3..1e9de68ad77a 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts @@ -156,6 +156,7 @@ suite('Conda and its environments are located correctly', () => { const isFile = typeof dir[name] === 'string'; return { name, + path: dir.name?.toString() ?? '', isFile: () => isFile, isDirectory: () => !isFile, isBlockDevice: () => false, diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts index 0562d1adf431..0fe53437cf3d 100644 --- a/src/test/standardTest.ts +++ b/src/test/standardTest.ts @@ -6,6 +6,7 @@ import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTest import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../client/common/constants'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; import { getChannel } from './utils/vscode'; +import { TestOptions } from '@vscode/test-electron/out/runTest'; // If running smoke tests, we don't have access to this. if (process.env.TEST_FILES_SUFFIX !== 'smoke.test') { @@ -85,18 +86,20 @@ async function start() { : ['--disable-extensions']; await installJupyterExtension(vscodeExecutablePath); await installPylanceExtension(vscodeExecutablePath); + console.log('VS Code executable', vscodeExecutablePath); const launchArgs = baseLaunchArgs .concat([workspacePath]) .concat(channel === 'insiders' ? ['--enable-proposed-api'] : []) .concat(['--timeout', '5000']); console.log(`Starting vscode ${channel} with args ${launchArgs.join(' ')}`); - await runTests({ + const options: TestOptions = { extensionDevelopmentPath: extensionDevelopmentPath, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test'), launchArgs, version: channel, extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, - }); + }; + await runTests(options); } start().catch((ex) => { console.error('End Standard tests (with errors)', ex); From a3633810b5647008c1b89ea3c6f2d466139909ba Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 3 Oct 2023 12:55:07 -0700 Subject: [PATCH 312/336] switch to using envvars for port and uuid in unittest (#22131) closes https://github.com/microsoft/vscode-python/issues/22130 --- .../tests/unittestadapter/test_discovery.py | 30 +------------ .../tests/unittestadapter/test_execution.py | 40 +---------------- pythonFiles/unittestadapter/discovery.py | 36 ++++----------- pythonFiles/unittestadapter/execution.py | 45 +++++-------------- pythonFiles/vscode_pytest/__init__.py | 4 +- .../testing/testController/common/server.ts | 13 +++--- .../testController/server.unit.test.ts | 18 ++++++-- 7 files changed, 45 insertions(+), 141 deletions(-) diff --git a/pythonFiles/tests/unittestadapter/test_discovery.py b/pythonFiles/tests/unittestadapter/test_discovery.py index c4778aa85852..67e52f43b70c 100644 --- a/pythonFiles/tests/unittestadapter/test_discovery.py +++ b/pythonFiles/tests/unittestadapter/test_discovery.py @@ -6,39 +6,13 @@ from typing import List import pytest -from unittestadapter.discovery import ( - DEFAULT_PORT, - discover_tests, - parse_discovery_cli_args, -) +from unittestadapter.discovery import discover_tests from unittestadapter.utils import TestNodeTypeEnum, parse_unittest_args + from . import expected_discovery_test_output from .helpers import TEST_DATA_PATH, is_same_tree -@pytest.mark.parametrize( - "args, expected", - [ - (["--port", "6767", "--uuid", "some-uuid"], (6767, "some-uuid")), - (["--foo", "something", "--bar", "another"], (int(DEFAULT_PORT), None)), - (["--port", "4444", "--foo", "something", "--port", "9999"], (9999, None)), - ( - ["--uuid", "first-uuid", "--bar", "other", "--uuid", "second-uuid"], - (int(DEFAULT_PORT), "second-uuid"), - ), - ], -) -def test_parse_cli_args(args: List[str], expected: List[str]) -> None: - """The parse_cli_args function should parse and return the port and uuid passed as command-line options. - - If there were no --port or --uuid command-line option, it should return default values). - If there are multiple options, the last one wins. - """ - actual = parse_discovery_cli_args(args) - - assert expected == actual - - @pytest.mark.parametrize( "args, expected", [ diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index 057f64d7396a..f7306e37662e 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -4,55 +4,17 @@ import os import pathlib import sys -from typing import List import pytest script_dir = pathlib.Path(__file__).parent.parent sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from unittestadapter.execution import parse_execution_cli_args, run_tests +from unittestadapter.execution import run_tests TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" -@pytest.mark.parametrize( - "args, expected", - [ - ( - [ - "--port", - "111", - "--uuid", - "fake-uuid", - ], - (111, "fake-uuid"), - ), - ( - ["--port", "111", "--uuid", "fake-uuid"], - (111, "fake-uuid"), - ), - ( - [ - "--port", - "111", - "--uuid", - "fake-uuid", - "-v", - "-s", - ], - (111, "fake-uuid"), - ), - ], -) -def test_parse_execution_cli_args(args: List[str], expected: List[str]) -> None: - """The parse_execution_cli_args function should return values for the port, uuid, and testids arguments - when passed as command-line options, and ignore unrecognized arguments. - """ - actual = parse_execution_cli_args(args) - assert actual == expected - - def test_no_ids_run() -> None: """This test runs on an empty array of test_ids, therefore it should return an empty dict for the result. diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index 69c14cae34e6..7e07e45d1202 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -1,46 +1,27 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import argparse import json import os import pathlib import sys import traceback import unittest -from typing import List, Optional, Tuple, Union +from typing import List, Optional, Union script_dir = pathlib.Path(__file__).parent.parent sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) from testing_tools import socket_manager +from typing_extensions import Literal, NotRequired, TypedDict # If I use from utils then there will be an import error in test_discovery.py. from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args -from typing_extensions import NotRequired, TypedDict, Literal - DEFAULT_PORT = "45454" -def parse_discovery_cli_args(args: List[str]) -> Tuple[int, Union[str, None]]: - """Parse command-line arguments that should be processed by the script. - - So far this includes the port number that it needs to connect to, and the uuid passed by the TS side. - The port is passed to the discovery.py script when it is executed, and - defaults to DEFAULT_PORT if it can't be parsed. - The uuid should be passed to the discovery.py script when it is executed, and defaults to None if it can't be parsed. - If the arguments appear several times, the value returned by parse_cli_args will be the value of the last argument. - """ - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument("--port", default=DEFAULT_PORT) - arg_parser.add_argument("--uuid") - parsed_args, _ = arg_parser.parse_known_args(args) - - return int(parsed_args.port), parsed_args.uuid - - class PayloadDict(TypedDict): cwd: str status: Literal["success", "error"] @@ -141,15 +122,16 @@ def post_response( start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) # Perform test discovery. - port, uuid = parse_discovery_cli_args(argv[:index]) + testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) + testUuid = os.environ.get("TEST_UUID") # Post this discovery payload. - if uuid is not None: - payload = discover_tests(start_dir, pattern, top_level_dir, uuid) - post_response(payload, port, uuid) + if testUuid is not None: + payload = discover_tests(start_dir, pattern, top_level_dir, testUuid) + post_response(payload, testPort, testUuid) # Post EOT token. eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} - post_response(eot_payload, port, uuid) + post_response(eot_payload, testPort, testUuid) else: print("Error: no uuid provided or parsed.") eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} - post_response(eot_payload, port, "") + post_response(eot_payload, testPort, "") diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index a208056c6682..0684ada8e44b 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import argparse import atexit import enum import json @@ -18,40 +17,17 @@ sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict - from testing_tools import process_json_util, socket_manager +from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args DEFAULT_PORT = "45454" - -def parse_execution_cli_args( - args: List[str], -) -> Tuple[int, Union[str, None]]: - """Parse command-line arguments that should be processed by the script. - - So far this includes the port number that it needs to connect to, the uuid passed by the TS side, - and the list of test ids to report. - The port is passed to the execution.py script when it is executed, and - defaults to DEFAULT_PORT if it can't be parsed. - The list of test ids is passed to the execution.py script when it is executed, and defaults to an empty list if it can't be parsed. - The uuid should be passed to the execution.py script when it is executed, and defaults to None if it can't be parsed. - If the arguments appear several times, the value returned by parse_cli_args will be the value of the last argument. - """ - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument("--port", default=DEFAULT_PORT) - arg_parser.add_argument("--uuid") - parsed_args, _ = arg_parser.parse_known_args(args) - - return (int(parsed_args.port), parsed_args.uuid) - - ErrorType = Union[ Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] ] -PORT = 0 -UUID = 0 +testPort = 0 +testUuid = 0 START_DIR = "" @@ -148,9 +124,9 @@ def formatResult( "subtest": subtest.id() if subtest else None, } self.formatted[test_id] = result - if PORT == 0 or UUID == 0: + if testPort == 0 or testUuid == 0: print("Error sending response, port or uuid unknown to python server.") - send_run_data(result, PORT, UUID) + send_run_data(result, testPort, testUuid) class TestExecutionStatus(str, enum.Enum): @@ -322,11 +298,12 @@ def post_response( print(f"Error: Could not connect to runTestIdsPort: {e}") print("Error: Could not connect to runTestIdsPort") - PORT, UUID = parse_execution_cli_args(argv[:index]) + testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) + testUuid = os.environ.get("TEST_UUID") if test_ids_from_buffer: # Perform test execution. payload = run_tests( - start_dir, test_ids_from_buffer, pattern, top_level_dir, UUID + start_dir, test_ids_from_buffer, pattern, top_level_dir, testUuid ) else: cwd = os.path.abspath(start_dir) @@ -338,8 +315,8 @@ def post_response( "result": None, } eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True} - if UUID is None: + if testUuid is None: print("Error sending response, uuid unknown to python server.") - post_response(eot_payload, PORT, "unknown") + post_response(eot_payload, testPort, "unknown") else: - post_response(eot_payload, PORT, UUID) + post_response(eot_payload, testPort, testUuid) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index f5827f87e1b4..e870136e3dc1 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -683,7 +683,7 @@ def send_post_request( cls_encoder -- a custom encoder if needed. """ testPort = os.getenv("TEST_PORT", 45454) - testuuid = os.getenv("TEST_UUID") + testUuid = os.getenv("TEST_UUID") addr = ("localhost", int(testPort)) global __socket @@ -698,7 +698,7 @@ def send_post_request( data = json.dumps(payload, cls=cls_encoder) request = f"""Content-Length: {len(data)} Content-Type: application/json -Request-uuid: {testuuid} +Request-uuid: {testUuid} {data}""" diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 1f9c0223d3fd..46217eab0459 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -179,7 +179,11 @@ export class PythonTestServer implements ITestServer, Disposable { cwd: options.cwd, throwOnStdErr: true, outputChannel: options.outChannel, - extraVariables: { PYTHONPATH: pythonPathCommand }, + extraVariables: { + PYTHONPATH: pythonPathCommand, + TEST_UUID: uuid.toString(), + TEST_PORT: this.getPort().toString(), + }, }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; @@ -191,12 +195,7 @@ export class PythonTestServer implements ITestServer, Disposable { }; const execService = await this.executionFactory.createActivatedEnvironment(creationOptions); - // Add the generated UUID to the data to be sent (expecting to receive it back). - // first check if we have testIds passed in (in case of execution) and - // insert appropriate flag and test id array - const args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat( - options.command.args, - ); + const args = [options.command.script].concat(options.command.args); if (options.outChannel) { options.outChannel.appendLine(`python ${args.join(' ')}`); diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 92a9a1135f55..02c35e806156 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -190,6 +190,16 @@ suite('Python Test Server, Send command etc', () => { RUN_TEST_IDS_PORT_CONST, 'Expect test id port to be in extra variables and set correctly', ); + assert.strictEqual( + options2.extraVariables.TEST_UUID, + FAKE_UUID, + 'Expect test uuid to be in extra variables and set correctly', + ); + assert.strictEqual( + options2.extraVariables.TEST_PORT, + 12345, + 'Expect server port to be set correctly as a env var', + ); } catch (e) { assert(false, 'Error parsing data, extra variables do not match'); } @@ -203,6 +213,8 @@ suite('Python Test Server, Send command etc', () => { return Promise.resolve(execService.object); }); server = new PythonTestServer(execFactory.object, debugLauncher); + sinon.stub(server, 'getPort').returns(12345); + // const portServer = server.getPort(); await server.serverReady(); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, @@ -215,8 +227,7 @@ suite('Python Test Server, Send command etc', () => { await deferred2.promise; mockProc.trigger('close'); - const port = server.getPort(); - const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', FAKE_UUID, '-foo', 'foo']; + const expectedArgs = ['myscript', '-foo', 'foo']; execService.verify((x) => x.execObservable(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); }); @@ -254,8 +265,7 @@ suite('Python Test Server, Send command etc', () => { await deferred.promise; mockProc.trigger('close'); - const port = server.getPort(); - const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', FAKE_UUID, '-foo', 'foo'].join(' '); + const expected = ['python', 'myscript', '-foo', 'foo'].join(' '); assert.deepStrictEqual(output2, [expected]); }); From ff0d4df88c9aa612853b5bd43cce65440a7ce0ec Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 4 Oct 2023 10:34:14 -0700 Subject: [PATCH 313/336] handle key error pytest (#22151) fixes https://github.com/microsoft/vscode-python/issues/22149 --- pythonFiles/vscode_pytest/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index e870136e3dc1..2fab4d77c2f8 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -200,8 +200,9 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - node_path = map_id_to_path[report.nodeid] - if not node_path: + try: + node_path = map_id_to_path[report.nodeid] + except KeyError: node_path = cwd # Calculate the absolute test id and use this as the ID moving forward. absolute_node_id = get_absolute_test_id(report.nodeid, node_path) From ae427391c9058f49fecfd5b8a20511624d2bb262 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 4 Oct 2023 11:29:53 -0700 Subject: [PATCH 314/336] Remove unsupported command from readme (#22153) --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8a5df6720717..0a8766f086af 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Extensions installed through the marketplace are subject to the [Marketplace Ter ## Jupyter Notebook quick start -The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. +The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. - Install the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter). @@ -60,7 +60,6 @@ Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/L | `Python: Select Interpreter` | Switch between Python interpreters, versions, and environments. | | `Python: Start REPL` | Start an interactive Python REPL using the selected interpreter in the VS Code terminal. | | `Python: Run Python File in Terminal` | Runs the active Python file in the VS Code terminal. You can also run a Python file by right-clicking on the file and selecting `Run Python File in Terminal`. | -| `Python: Select Linter` | Switch from Pylint to Flake8 or other supported linters. | | `Format Document` | Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/editing#_formatting) in the `settings.json` file. | | `Python: Configure Tests` | Select a test framework and configure it to display the Test Explorer. | @@ -82,7 +81,7 @@ Learn more about the rich features of the Python extension: - [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments -- [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). +- [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). From ab6ab06e60b26109fe22843ea1aa46e918864e10 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 4 Oct 2023 12:29:57 -0700 Subject: [PATCH 315/336] Use python 3.12-dev (#22043) --- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1509a7b433e..56d9c04f0cd1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -120,7 +120,7 @@ jobs: # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. - python: ['3.8', '3.x'] + python: ['3.8', '3.x', '3.12-dev'] steps: - name: Checkout diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index b7d2ed0c0545..9229393ce5cc 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -94,7 +94,7 @@ jobs: # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. - python: ['3.8', '3.x'] + python: ['3.8', '3.x', '3.12-dev'] steps: - name: Checkout From 66cea2169ef4c46a96e42725c4793ccce3b5c5df Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 4 Oct 2023 16:29:51 -0700 Subject: [PATCH 316/336] Show notification when deactivate command is run in terminal (#22133) Closes https://github.com/microsoft/vscode-python/issues/22121 --- package.json | 3 +- .../common/application/applicationShell.ts | 12 +- src/client/common/application/types.ts | 18 ++ src/client/common/utils/localize.ts | 4 + src/client/interpreter/activation/types.ts | 8 - src/client/interpreter/serviceRegistry.ts | 13 +- src/client/telemetry/constants.ts | 1 + src/client/telemetry/index.ts | 18 ++ .../deactivatePrompt.ts | 91 +++++++ .../indicatorPrompt.ts} | 6 +- .../envCollectionActivation/service.ts} | 7 +- src/client/terminals/serviceRegistry.ts | 38 ++- src/client/terminals/types.ts | 8 + ...erminalEnvVarCollectionPrompt.unit.test.ts | 8 +- ...rminalEnvVarCollectionService.unit.test.ts | 2 +- .../deactivatePrompt.unit.test.ts | 251 ++++++++++++++++++ .../terminals/serviceRegistry.unit.test.ts | 16 ++ ...scode.proposed.terminalDataWriteEvent.d.ts | 31 +++ 18 files changed, 490 insertions(+), 45 deletions(-) create mode 100644 src/client/terminals/envCollectionActivation/deactivatePrompt.ts rename src/client/{interpreter/activation/terminalEnvVarCollectionPrompt.ts => terminals/envCollectionActivation/indicatorPrompt.ts} (95%) rename src/client/{interpreter/activation/terminalEnvVarCollectionService.ts => terminals/envCollectionActivation/service.ts} (98%) create mode 100644 src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts create mode 100644 typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts diff --git a/package.json b/package.json index 47faec663015..df2d2546e4d8 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "quickPickSortByLabel", "testObserver", "quickPickItemTooltip", - "saveEditor" + "saveEditor", + "terminalDataWriteEvent" ], "author": { "name": "Microsoft Corporation" diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index 454662472010..aadf80186900 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -10,6 +10,7 @@ import { DocumentSelector, env, Event, + EventEmitter, InputBox, InputBoxOptions, languages, @@ -37,7 +38,8 @@ import { WorkspaceFolder, WorkspaceFolderPickOptions, } from 'vscode'; -import { IApplicationShell } from './types'; +import { traceError } from '../../logging'; +import { IApplicationShell, TerminalDataWriteEvent } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { @@ -172,4 +174,12 @@ export class ApplicationShell implements IApplicationShell { public createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem { return languages.createLanguageStatusItem(id, selector); } + public get onDidWriteTerminalData(): Event { + try { + return window.onDidWriteTerminalData; + } catch (ex) { + traceError('Failed to get proposed API onDidWriteTerminalData', ex); + return new EventEmitter().event; + } + } } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index fa2ced6c45da..863f5e4651b2 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -67,6 +67,17 @@ import { Resource } from '../types'; import { ICommandNameArgumentTypeMapping } from './commands'; import { ExtensionContextKey } from './contextKeys'; +export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; +} + export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { /** @@ -75,6 +86,13 @@ export interface IApplicationShell { */ readonly onDidChangeWindowState: Event; + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + readonly onDidWriteTerminalData: Event; + showInformationMessage(message: string, ...items: string[]): Thenable; /** diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index bc32c1078cad..fc118699d2c7 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -201,6 +201,10 @@ export namespace Interpreters { export const terminalEnvVarCollectionPrompt = l10n.t( 'The Python extension automatically activates all terminals using the selected environment, even when the name of the environment{0} is not present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', ); + export const terminalDeactivatePrompt = l10n.t( + 'Deactivating virtual environments may not work by default due to a technical limitation in our activation approach, but it can be resolved with a few simple steps.', + ); + export const deactivateDoneButton = l10n.t('Done, it works'); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', ); diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts index 2b364cbeb862..e00ef9b62b3f 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -21,11 +21,3 @@ export interface IEnvironmentActivationService { interpreter?: PythonEnvironment, ): Promise; } - -export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); -export interface ITerminalEnvVarCollectionService { - /** - * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. - */ - isTerminalPromptSetCorrectly(resource?: Resource): boolean; -} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 018e7abfdc46..422776bd5e43 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -6,9 +6,7 @@ import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { EnvironmentActivationService } from './activation/service'; -import { TerminalEnvVarCollectionPrompt } from './activation/terminalEnvVarCollectionPrompt'; -import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService'; -import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './activation/types'; +import { IEnvironmentActivationService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types'; @@ -110,13 +108,4 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); - serviceManager.addSingleton( - ITerminalEnvVarCollectionService, - TerminalEnvVarCollectionService, - ); - serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TerminalEnvVarCollectionPrompt, - ); } diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index c680b91094cb..4b4dc302dc3f 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -29,6 +29,7 @@ export enum EventName { TERMINAL_SHELL_IDENTIFICATION = 'TERMINAL_SHELL_IDENTIFICATION', PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT', PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT', + TERMINAL_DEACTIVATE_PROMPT = 'TERMINAL_DEACTIVATE_PROMPT', CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT', REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT', ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 600f9a2d48ff..bd60b9281a93 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1328,6 +1328,24 @@ export interface IEventNamePropertyMapping { */ selection: 'Allow' | 'Close' | undefined; }; + /** + * Telemetry event sent with details when user clicks the prompt with the following message: + * + * 'Deactivating virtual environments may not work by default due to a technical limitation in our activation approach, but it can be resolved with a few simple steps.' + */ + /* __GDPR__ + "terminal_deactivate_prompt" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" } + } + */ + [EventName.TERMINAL_DEACTIVATE_PROMPT]: { + /** + * `See Instructions` When 'See Instructions' option is selected + * `Done, it works` When 'Done, it works' option is selected + * `Don't show again` When 'Don't show again' option is selected + */ + selection: 'See Instructions' | 'Done, it works' | "Don't show again" | undefined; + }; /** * Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed. */ diff --git a/src/client/terminals/envCollectionActivation/deactivatePrompt.ts b/src/client/terminals/envCollectionActivation/deactivatePrompt.ts new file mode 100644 index 000000000000..460144303f18 --- /dev/null +++ b/src/client/terminals/envCollectionActivation/deactivatePrompt.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationEnvironment, IApplicationShell } from '../../common/application/types'; +import { IBrowserService, IDisposableRegistry, IExperimentService, IPersistentStateFactory } from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { TerminalShellType } from '../../common/terminal/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +export const terminalDeactivationPromptKey = 'TERMINAL_DEACTIVATION_PROMPT_KEY'; + +@injectable() +export class TerminalDeactivateLimitationPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IBrowserService) private readonly browserService: IBrowserService, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) {} + + public async activate(): Promise { + if (!inTerminalEnvVarExperiment(this.experimentService)) { + return; + } + this.disposableRegistry.push( + this.appShell.onDidWriteTerminalData(async (e) => { + if (!e.data.includes('deactivate')) { + return; + } + const shellType = identifyShellFromShellPath(this.appEnvironment.shell); + if (shellType === TerminalShellType.commandPrompt) { + return; + } + const { terminal } = e; + const cwd = + 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd + ? terminal.creationOptions.cwd + : undefined; + const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.type !== PythonEnvType.Virtual) { + return; + } + await this.notifyUsers(); + }), + ); + } + + private async notifyUsers(): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + terminalDeactivationPromptKey, + true, + ); + if (!notificationPromptEnabled.value) { + return; + } + const prompts = [Common.seeInstructions, Interpreters.deactivateDoneButton, Common.doNotShowAgain]; + const telemetrySelections: ['See Instructions', 'Done, it works', "Don't show again"] = [ + 'See Instructions', + 'Done, it works', + "Don't show again", + ]; + const selection = await this.appShell.showWarningMessage(Interpreters.terminalDeactivatePrompt, ...prompts); + if (!selection) { + return; + } + sendTelemetryEvent(EventName.TERMINAL_DEACTIVATE_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, + }); + if (selection === prompts[0]) { + const url = `https://aka.ms/AAmx2ft`; + this.browserService.launch(url); + } + if (selection === prompts[1] || selection === prompts[2]) { + await notificationPromptEnabled.updateValue(false); + } + } +} diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts similarity index 95% rename from src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts rename to src/client/terminals/envCollectionActivation/indicatorPrompt.ts index c8aea205a32a..bf648eefe8e9 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts +++ b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts @@ -14,15 +14,15 @@ import { } from '../../common/types'; import { Common, Interpreters } from '../../common/utils/localize'; import { IExtensionSingleActivationService } from '../../activation/types'; -import { ITerminalEnvVarCollectionService } from './types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; -import { IInterpreterService } from '../contracts'; +import { IInterpreterService } from '../../interpreter/contracts'; import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../types'; export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; @injectable() -export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivationService { +export class TerminalIndicatorPrompt implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; constructor( diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/terminals/envCollectionActivation/service.ts similarity index 98% rename from src/client/interpreter/activation/terminalEnvVarCollectionService.ts rename to src/client/terminals/envCollectionActivation/service.ts index c11ec221d4d7..ae346d264eeb 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -28,9 +28,9 @@ import { import { Deferred, createDeferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; import { traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging'; -import { IInterpreterService } from '../contracts'; -import { defaultShells } from './service'; -import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { defaultShells } from '../../interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../interpreter/activation/types'; import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; @@ -38,6 +38,7 @@ import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; import { normCase } from '../../common/platform/fs-paths'; import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { ITerminalEnvVarCollectionService } from '../types'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts index a39ef31a8fe4..a9da776d011a 100644 --- a/src/client/terminals/serviceRegistry.ts +++ b/src/client/terminals/serviceRegistry.ts @@ -1,25 +1,26 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { interfaces } from 'inversify'; -import { ClassType } from '../ioc/types'; +import { IServiceManager } from '../ioc/types'; import { TerminalAutoActivation } from './activation'; import { CodeExecutionManager } from './codeExecution/codeExecutionManager'; import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution'; import { CodeExecutionHelper } from './codeExecution/helper'; import { ReplProvider } from './codeExecution/repl'; import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution'; -import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation } from './types'; +import { + ICodeExecutionHelper, + ICodeExecutionManager, + ICodeExecutionService, + ITerminalAutoActivation, + ITerminalEnvVarCollectionService, +} from './types'; +import { TerminalEnvVarCollectionService } from './envCollectionActivation/service'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; +import { TerminalDeactivateLimitationPrompt } from './envCollectionActivation/deactivatePrompt'; +import { TerminalIndicatorPrompt } from './envCollectionActivation/indicatorPrompt'; -interface IServiceRegistry { - addSingleton( - serviceIdentifier: interfaces.ServiceIdentifier, - constructor: ClassType, - name?: string | number | symbol, - ): void; -} - -export function registerTypes(serviceManager: IServiceRegistry): void { +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); @@ -37,4 +38,17 @@ export function registerTypes(serviceManager: IServiceRegistry): void { serviceManager.addSingleton(ICodeExecutionService, ReplProvider, 'repl'); serviceManager.addSingleton(ITerminalAutoActivation, TerminalAutoActivation); + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, + TerminalEnvVarCollectionService, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalIndicatorPrompt, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalDeactivateLimitationPrompt, + ); + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 47ac16d9e08b..48d60adf3f39 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -33,3 +33,11 @@ export interface ITerminalAutoActivation extends IDisposable { register(): void; disableAutoActivation(terminal: Terminal): void; } + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts index baa83c8b11c5..5d4da49ebb45 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -13,13 +13,13 @@ import { IPersistentStateFactory, IPythonSettings, } from '../../../client/common/types'; -import { TerminalEnvVarCollectionPrompt } from '../../../client/interpreter/activation/terminalEnvVarCollectionPrompt'; -import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/activation/types'; +import { TerminalIndicatorPrompt } from '../../../client/terminals/envCollectionActivation/indicatorPrompt'; import { Common, Interpreters } from '../../../client/common/utils/localize'; import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; import { sleep } from '../../core'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../../../client/terminals/types'; suite('Terminal Environment Variable Collection Prompt', () => { let shell: IApplicationShell; @@ -28,7 +28,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { let activeResourceService: IActiveResourceService; let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; let persistentStateFactory: IPersistentStateFactory; - let terminalEnvVarCollectionPrompt: TerminalEnvVarCollectionPrompt; + let terminalEnvVarCollectionPrompt: TerminalIndicatorPrompt; let terminalEventEmitter: EventEmitter; let notificationEnabled: IPersistentState; let configurationService: IConfigurationService; @@ -61,7 +61,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { ); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); - terminalEnvVarCollectionPrompt = new TerminalEnvVarCollectionPrompt( + terminalEnvVarCollectionPrompt = new TerminalIndicatorPrompt( instance(shell), instance(persistentStateFactory), instance(terminalManager), diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index e41d6ce4d53c..5e572e7ad06f 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -32,7 +32,7 @@ import { import { Interpreters } from '../../../client/common/utils/localize'; import { OSType, getOSType } from '../../../client/common/utils/platform'; import { defaultShells } from '../../../client/interpreter/activation/service'; -import { TerminalEnvVarCollectionService } from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; +import { TerminalEnvVarCollectionService } from '../../../client/terminals/envCollectionActivation/service'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; diff --git a/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts b/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts new file mode 100644 index 000000000000..acd8ee99e5d7 --- /dev/null +++ b/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; +import { EventEmitter, Terminal, TerminalDataWriteEvent, Uri } from 'vscode'; +import { IApplicationEnvironment, IApplicationShell } from '../../../client/common/application/types'; +import { + IBrowserService, + IExperimentService, + IPersistentState, + IPersistentStateFactory, +} from '../../../client/common/types'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; +import { sleep } from '../../core'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { TerminalDeactivateLimitationPrompt } from '../../../client/terminals/envCollectionActivation/deactivatePrompt'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import { TerminalShellType } from '../../../client/common/terminal/types'; + +suite('Terminal Deactivation Limitation Prompt', () => { + let shell: IApplicationShell; + let experimentService: IExperimentService; + let persistentStateFactory: IPersistentStateFactory; + let appEnvironment: IApplicationEnvironment; + let deactivatePrompt: TerminalDeactivateLimitationPrompt; + let terminalWriteEvent: EventEmitter; + let notificationEnabled: IPersistentState; + let browserService: IBrowserService; + let interpreterService: IInterpreterService; + const prompts = [Common.seeInstructions, Interpreters.deactivateDoneButton, Common.doNotShowAgain]; + const expectedMessage = Interpreters.terminalDeactivatePrompt; + + setup(async () => { + shell = mock(); + interpreterService = mock(); + experimentService = mock(); + persistentStateFactory = mock(); + appEnvironment = mock(); + when(appEnvironment.shell).thenReturn('bash'); + browserService = mock(); + notificationEnabled = mock>(); + terminalWriteEvent = new EventEmitter(); + when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( + instance(notificationEnabled), + ); + when(shell.onDidWriteTerminalData).thenReturn(terminalWriteEvent.event); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + deactivatePrompt = new TerminalDeactivateLimitationPrompt( + instance(shell), + instance(persistentStateFactory), + [], + instance(interpreterService), + instance(browserService), + instance(appEnvironment), + instance(experimentService), + ); + }); + + test('Show notification when "deactivate" command is run when a virtual env is selected', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); + }); + + test('When using cmd, do not show notification for the same', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + reset(appEnvironment); + when(appEnvironment.shell).thenReturn(TerminalShellType.commandPrompt); + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); + }); + + test('When not in experiment, do not show notification for the same', async () => { + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification if notification is disabled', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(false); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification when virtual env is not activated for terminal', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Conda, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test("Disable notification if `Don't show again` is clicked", async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Disable notification if `Done, it works` is clicked', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn( + Promise.resolve(Interpreters.deactivateDoneButton), + ); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Open link to workaround if `See instructions` is clicked', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.seeInstructions)); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); + verify(browserService.launch(anything())).once(); + }); + + test('Do not perform any action if prompt is closed', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); + verify(notificationEnabled.updateValue(false)).never(); + verify(browserService.launch(anything())).never(); + }); +}); diff --git a/src/test/terminals/serviceRegistry.unit.test.ts b/src/test/terminals/serviceRegistry.unit.test.ts index 38a9a9744e91..816afa17cf88 100644 --- a/src/test/terminals/serviceRegistry.unit.test.ts +++ b/src/test/terminals/serviceRegistry.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as typemoq from 'typemoq'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../../client/activation/types'; import { IServiceManager } from '../../client/ioc/types'; import { TerminalAutoActivation } from '../../client/terminals/activation'; import { CodeExecutionManager } from '../../client/terminals/codeExecution/codeExecutionManager'; @@ -9,12 +10,16 @@ import { DjangoShellCodeExecutionProvider } from '../../client/terminals/codeExe import { CodeExecutionHelper } from '../../client/terminals/codeExecution/helper'; import { ReplProvider } from '../../client/terminals/codeExecution/repl'; import { TerminalCodeExecutionProvider } from '../../client/terminals/codeExecution/terminalCodeExecution'; +import { TerminalDeactivateLimitationPrompt } from '../../client/terminals/envCollectionActivation/deactivatePrompt'; +import { TerminalIndicatorPrompt } from '../../client/terminals/envCollectionActivation/indicatorPrompt'; +import { TerminalEnvVarCollectionService } from '../../client/terminals/envCollectionActivation/service'; import { registerTypes } from '../../client/terminals/serviceRegistry'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation, + ITerminalEnvVarCollectionService, } from '../../client/terminals/types'; suite('Terminal - Service Registry', () => { @@ -27,6 +32,9 @@ suite('Terminal - Service Registry', () => { [ICodeExecutionService, ReplProvider, 'repl'], [ITerminalAutoActivation, TerminalAutoActivation], [ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'], + [ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService], + [IExtensionSingleActivationService, TerminalIndicatorPrompt], + [IExtensionSingleActivationService, TerminalDeactivateLimitationPrompt], ].forEach((args) => { if (args.length === 2) { services @@ -50,6 +58,14 @@ suite('Terminal - Service Registry', () => { .verifiable(typemoq.Times.once()); } }); + services + .setup((s) => + s.addBinding( + typemoq.It.is((v) => ITerminalEnvVarCollectionService === v), + typemoq.It.is((value) => IExtensionActivationService === value), + ), + ) + .verifiable(typemoq.Times.once()); registerTypes(services.object); diff --git a/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts new file mode 100644 index 000000000000..6913b862c70f --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/78502 + // + // This API is still proposed but we don't intent on promoting it to stable due to problems + // around performance. See #145234 for a more likely API to get stabilized. + + export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; + } + + namespace window { + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + export const onDidWriteTerminalData: Event; + } +} From 514bce666d3388dc47d115da44cb2d127560757d Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 5 Oct 2023 11:11:34 -0700 Subject: [PATCH 317/336] Revert "Show notification when deactivate command is run in terminal" (#22158) Reverts microsoft/vscode-python#22133 --- package.json | 3 +- .../common/application/applicationShell.ts | 12 +- src/client/common/application/types.ts | 18 -- src/client/common/utils/localize.ts | 4 - .../terminalEnvVarCollectionPrompt.ts} | 6 +- .../terminalEnvVarCollectionService.ts} | 7 +- src/client/interpreter/activation/types.ts | 8 + src/client/interpreter/serviceRegistry.ts | 13 +- src/client/telemetry/constants.ts | 1 - src/client/telemetry/index.ts | 18 -- .../deactivatePrompt.ts | 91 ------- src/client/terminals/serviceRegistry.ts | 38 +-- src/client/terminals/types.ts | 8 - ...erminalEnvVarCollectionPrompt.unit.test.ts | 8 +- ...rminalEnvVarCollectionService.unit.test.ts | 2 +- .../deactivatePrompt.unit.test.ts | 251 ------------------ .../terminals/serviceRegistry.unit.test.ts | 16 -- ...scode.proposed.terminalDataWriteEvent.d.ts | 31 --- 18 files changed, 45 insertions(+), 490 deletions(-) rename src/client/{terminals/envCollectionActivation/indicatorPrompt.ts => interpreter/activation/terminalEnvVarCollectionPrompt.ts} (95%) rename src/client/{terminals/envCollectionActivation/service.ts => interpreter/activation/terminalEnvVarCollectionService.ts} (98%) delete mode 100644 src/client/terminals/envCollectionActivation/deactivatePrompt.ts delete mode 100644 src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts delete mode 100644 typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts diff --git a/package.json b/package.json index df2d2546e4d8..47faec663015 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,7 @@ "quickPickSortByLabel", "testObserver", "quickPickItemTooltip", - "saveEditor", - "terminalDataWriteEvent" + "saveEditor" ], "author": { "name": "Microsoft Corporation" diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index aadf80186900..454662472010 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -10,7 +10,6 @@ import { DocumentSelector, env, Event, - EventEmitter, InputBox, InputBoxOptions, languages, @@ -38,8 +37,7 @@ import { WorkspaceFolder, WorkspaceFolderPickOptions, } from 'vscode'; -import { traceError } from '../../logging'; -import { IApplicationShell, TerminalDataWriteEvent } from './types'; +import { IApplicationShell } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { @@ -174,12 +172,4 @@ export class ApplicationShell implements IApplicationShell { public createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem { return languages.createLanguageStatusItem(id, selector); } - public get onDidWriteTerminalData(): Event { - try { - return window.onDidWriteTerminalData; - } catch (ex) { - traceError('Failed to get proposed API onDidWriteTerminalData', ex); - return new EventEmitter().event; - } - } } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 863f5e4651b2..fa2ced6c45da 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -67,17 +67,6 @@ import { Resource } from '../types'; import { ICommandNameArgumentTypeMapping } from './commands'; import { ExtensionContextKey } from './contextKeys'; -export interface TerminalDataWriteEvent { - /** - * The {@link Terminal} for which the data was written. - */ - readonly terminal: Terminal; - /** - * The data being written. - */ - readonly data: string; -} - export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { /** @@ -86,13 +75,6 @@ export interface IApplicationShell { */ readonly onDidChangeWindowState: Event; - /** - * An event which fires when the terminal's child pseudo-device is written to (the shell). - * In other words, this provides access to the raw data stream from the process running - * within the terminal, including VT sequences. - */ - readonly onDidWriteTerminalData: Event; - showInformationMessage(message: string, ...items: string[]): Thenable; /** diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index fc118699d2c7..bc32c1078cad 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -201,10 +201,6 @@ export namespace Interpreters { export const terminalEnvVarCollectionPrompt = l10n.t( 'The Python extension automatically activates all terminals using the selected environment, even when the name of the environment{0} is not present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', ); - export const terminalDeactivatePrompt = l10n.t( - 'Deactivating virtual environments may not work by default due to a technical limitation in our activation approach, but it can be resolved with a few simple steps.', - ); - export const deactivateDoneButton = l10n.t('Done, it works'); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', ); diff --git a/src/client/terminals/envCollectionActivation/indicatorPrompt.ts b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts similarity index 95% rename from src/client/terminals/envCollectionActivation/indicatorPrompt.ts rename to src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts index bf648eefe8e9..c8aea205a32a 100644 --- a/src/client/terminals/envCollectionActivation/indicatorPrompt.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts @@ -14,15 +14,15 @@ import { } from '../../common/types'; import { Common, Interpreters } from '../../common/utils/localize'; import { IExtensionSingleActivationService } from '../../activation/types'; +import { ITerminalEnvVarCollectionService } from './types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; -import { IInterpreterService } from '../../interpreter/contracts'; +import { IInterpreterService } from '../contracts'; import { PythonEnvironment } from '../../pythonEnvironments/info'; -import { ITerminalEnvVarCollectionService } from '../types'; export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; @injectable() -export class TerminalIndicatorPrompt implements IExtensionSingleActivationService { +export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; constructor( diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts similarity index 98% rename from src/client/terminals/envCollectionActivation/service.ts rename to src/client/interpreter/activation/terminalEnvVarCollectionService.ts index ae346d264eeb..c11ec221d4d7 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -28,9 +28,9 @@ import { import { Deferred, createDeferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; import { traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging'; -import { IInterpreterService } from '../../interpreter/contracts'; -import { defaultShells } from '../../interpreter/activation/service'; -import { IEnvironmentActivationService } from '../../interpreter/activation/types'; +import { IInterpreterService } from '../contracts'; +import { defaultShells } from './service'; +import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; @@ -38,7 +38,6 @@ import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; import { normCase } from '../../common/platform/fs-paths'; import { PythonEnvType } from '../../pythonEnvironments/base/info'; -import { ITerminalEnvVarCollectionService } from '../types'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts index e00ef9b62b3f..2b364cbeb862 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -21,3 +21,11 @@ export interface IEnvironmentActivationService { interpreter?: PythonEnvironment, ): Promise; } + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 422776bd5e43..018e7abfdc46 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -6,7 +6,9 @@ import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { EnvironmentActivationService } from './activation/service'; -import { IEnvironmentActivationService } from './activation/types'; +import { TerminalEnvVarCollectionPrompt } from './activation/terminalEnvVarCollectionPrompt'; +import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService'; +import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types'; @@ -108,4 +110,13 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, + TerminalEnvVarCollectionService, + ); + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalEnvVarCollectionPrompt, + ); } diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 4b4dc302dc3f..c680b91094cb 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -29,7 +29,6 @@ export enum EventName { TERMINAL_SHELL_IDENTIFICATION = 'TERMINAL_SHELL_IDENTIFICATION', PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT', PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT', - TERMINAL_DEACTIVATE_PROMPT = 'TERMINAL_DEACTIVATE_PROMPT', CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT', REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT', ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index bd60b9281a93..600f9a2d48ff 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1328,24 +1328,6 @@ export interface IEventNamePropertyMapping { */ selection: 'Allow' | 'Close' | undefined; }; - /** - * Telemetry event sent with details when user clicks the prompt with the following message: - * - * 'Deactivating virtual environments may not work by default due to a technical limitation in our activation approach, but it can be resolved with a few simple steps.' - */ - /* __GDPR__ - "terminal_deactivate_prompt" : { - "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" } - } - */ - [EventName.TERMINAL_DEACTIVATE_PROMPT]: { - /** - * `See Instructions` When 'See Instructions' option is selected - * `Done, it works` When 'Done, it works' option is selected - * `Don't show again` When 'Don't show again' option is selected - */ - selection: 'See Instructions' | 'Done, it works' | "Don't show again" | undefined; - }; /** * Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed. */ diff --git a/src/client/terminals/envCollectionActivation/deactivatePrompt.ts b/src/client/terminals/envCollectionActivation/deactivatePrompt.ts deleted file mode 100644 index 460144303f18..000000000000 --- a/src/client/terminals/envCollectionActivation/deactivatePrompt.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { IApplicationEnvironment, IApplicationShell } from '../../common/application/types'; -import { IBrowserService, IDisposableRegistry, IExperimentService, IPersistentStateFactory } from '../../common/types'; -import { Common, Interpreters } from '../../common/utils/localize'; -import { IExtensionSingleActivationService } from '../../activation/types'; -import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; -import { IInterpreterService } from '../../interpreter/contracts'; -import { PythonEnvType } from '../../pythonEnvironments/base/info'; -import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; -import { TerminalShellType } from '../../common/terminal/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; - -export const terminalDeactivationPromptKey = 'TERMINAL_DEACTIVATION_PROMPT_KEY'; - -@injectable() -export class TerminalDeactivateLimitationPrompt implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IBrowserService) private readonly browserService: IBrowserService, - @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, - @inject(IExperimentService) private readonly experimentService: IExperimentService, - ) {} - - public async activate(): Promise { - if (!inTerminalEnvVarExperiment(this.experimentService)) { - return; - } - this.disposableRegistry.push( - this.appShell.onDidWriteTerminalData(async (e) => { - if (!e.data.includes('deactivate')) { - return; - } - const shellType = identifyShellFromShellPath(this.appEnvironment.shell); - if (shellType === TerminalShellType.commandPrompt) { - return; - } - const { terminal } = e; - const cwd = - 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd - ? terminal.creationOptions.cwd - : undefined; - const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; - const interpreter = await this.interpreterService.getActiveInterpreter(resource); - if (interpreter?.type !== PythonEnvType.Virtual) { - return; - } - await this.notifyUsers(); - }), - ); - } - - private async notifyUsers(): Promise { - const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( - terminalDeactivationPromptKey, - true, - ); - if (!notificationPromptEnabled.value) { - return; - } - const prompts = [Common.seeInstructions, Interpreters.deactivateDoneButton, Common.doNotShowAgain]; - const telemetrySelections: ['See Instructions', 'Done, it works', "Don't show again"] = [ - 'See Instructions', - 'Done, it works', - "Don't show again", - ]; - const selection = await this.appShell.showWarningMessage(Interpreters.terminalDeactivatePrompt, ...prompts); - if (!selection) { - return; - } - sendTelemetryEvent(EventName.TERMINAL_DEACTIVATE_PROMPT, undefined, { - selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, - }); - if (selection === prompts[0]) { - const url = `https://aka.ms/AAmx2ft`; - this.browserService.launch(url); - } - if (selection === prompts[1] || selection === prompts[2]) { - await notificationPromptEnabled.updateValue(false); - } - } -} diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts index a9da776d011a..a39ef31a8fe4 100644 --- a/src/client/terminals/serviceRegistry.ts +++ b/src/client/terminals/serviceRegistry.ts @@ -1,26 +1,25 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IServiceManager } from '../ioc/types'; +import { interfaces } from 'inversify'; +import { ClassType } from '../ioc/types'; import { TerminalAutoActivation } from './activation'; import { CodeExecutionManager } from './codeExecution/codeExecutionManager'; import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution'; import { CodeExecutionHelper } from './codeExecution/helper'; import { ReplProvider } from './codeExecution/repl'; import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution'; -import { - ICodeExecutionHelper, - ICodeExecutionManager, - ICodeExecutionService, - ITerminalAutoActivation, - ITerminalEnvVarCollectionService, -} from './types'; -import { TerminalEnvVarCollectionService } from './envCollectionActivation/service'; -import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; -import { TerminalDeactivateLimitationPrompt } from './envCollectionActivation/deactivatePrompt'; -import { TerminalIndicatorPrompt } from './envCollectionActivation/indicatorPrompt'; +import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation } from './types'; -export function registerTypes(serviceManager: IServiceManager): void { +interface IServiceRegistry { + addSingleton( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol, + ): void; +} + +export function registerTypes(serviceManager: IServiceRegistry): void { serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); @@ -38,17 +37,4 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ICodeExecutionService, ReplProvider, 'repl'); serviceManager.addSingleton(ITerminalAutoActivation, TerminalAutoActivation); - serviceManager.addSingleton( - ITerminalEnvVarCollectionService, - TerminalEnvVarCollectionService, - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TerminalIndicatorPrompt, - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TerminalDeactivateLimitationPrompt, - ); - serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 48d60adf3f39..47ac16d9e08b 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -33,11 +33,3 @@ export interface ITerminalAutoActivation extends IDisposable { register(): void; disableAutoActivation(terminal: Terminal): void; } - -export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); -export interface ITerminalEnvVarCollectionService { - /** - * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. - */ - isTerminalPromptSetCorrectly(resource?: Resource): boolean; -} diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts index 5d4da49ebb45..baa83c8b11c5 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -13,13 +13,13 @@ import { IPersistentStateFactory, IPythonSettings, } from '../../../client/common/types'; -import { TerminalIndicatorPrompt } from '../../../client/terminals/envCollectionActivation/indicatorPrompt'; +import { TerminalEnvVarCollectionPrompt } from '../../../client/interpreter/activation/terminalEnvVarCollectionPrompt'; +import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/activation/types'; import { Common, Interpreters } from '../../../client/common/utils/localize'; import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; import { sleep } from '../../core'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { ITerminalEnvVarCollectionService } from '../../../client/terminals/types'; suite('Terminal Environment Variable Collection Prompt', () => { let shell: IApplicationShell; @@ -28,7 +28,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { let activeResourceService: IActiveResourceService; let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; let persistentStateFactory: IPersistentStateFactory; - let terminalEnvVarCollectionPrompt: TerminalIndicatorPrompt; + let terminalEnvVarCollectionPrompt: TerminalEnvVarCollectionPrompt; let terminalEventEmitter: EventEmitter; let notificationEnabled: IPersistentState; let configurationService: IConfigurationService; @@ -61,7 +61,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { ); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); - terminalEnvVarCollectionPrompt = new TerminalIndicatorPrompt( + terminalEnvVarCollectionPrompt = new TerminalEnvVarCollectionPrompt( instance(shell), instance(persistentStateFactory), instance(terminalManager), diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 5e572e7ad06f..e41d6ce4d53c 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -32,7 +32,7 @@ import { import { Interpreters } from '../../../client/common/utils/localize'; import { OSType, getOSType } from '../../../client/common/utils/platform'; import { defaultShells } from '../../../client/interpreter/activation/service'; -import { TerminalEnvVarCollectionService } from '../../../client/terminals/envCollectionActivation/service'; +import { TerminalEnvVarCollectionService } from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; diff --git a/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts b/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts deleted file mode 100644 index acd8ee99e5d7..000000000000 --- a/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; -import { EventEmitter, Terminal, TerminalDataWriteEvent, Uri } from 'vscode'; -import { IApplicationEnvironment, IApplicationShell } from '../../../client/common/application/types'; -import { - IBrowserService, - IExperimentService, - IPersistentState, - IPersistentStateFactory, -} from '../../../client/common/types'; -import { Common, Interpreters } from '../../../client/common/utils/localize'; -import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; -import { sleep } from '../../core'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { TerminalDeactivateLimitationPrompt } from '../../../client/terminals/envCollectionActivation/deactivatePrompt'; -import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; -import { TerminalShellType } from '../../../client/common/terminal/types'; - -suite('Terminal Deactivation Limitation Prompt', () => { - let shell: IApplicationShell; - let experimentService: IExperimentService; - let persistentStateFactory: IPersistentStateFactory; - let appEnvironment: IApplicationEnvironment; - let deactivatePrompt: TerminalDeactivateLimitationPrompt; - let terminalWriteEvent: EventEmitter; - let notificationEnabled: IPersistentState; - let browserService: IBrowserService; - let interpreterService: IInterpreterService; - const prompts = [Common.seeInstructions, Interpreters.deactivateDoneButton, Common.doNotShowAgain]; - const expectedMessage = Interpreters.terminalDeactivatePrompt; - - setup(async () => { - shell = mock(); - interpreterService = mock(); - experimentService = mock(); - persistentStateFactory = mock(); - appEnvironment = mock(); - when(appEnvironment.shell).thenReturn('bash'); - browserService = mock(); - notificationEnabled = mock>(); - terminalWriteEvent = new EventEmitter(); - when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( - instance(notificationEnabled), - ); - when(shell.onDidWriteTerminalData).thenReturn(terminalWriteEvent.event); - when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); - deactivatePrompt = new TerminalDeactivateLimitationPrompt( - instance(shell), - instance(persistentStateFactory), - [], - instance(interpreterService), - instance(browserService), - instance(appEnvironment), - instance(experimentService), - ); - }); - - test('Show notification when "deactivate" command is run when a virtual env is selected', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); - }); - - test('When using cmd, do not show notification for the same', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - reset(appEnvironment); - when(appEnvironment.shell).thenReturn(TerminalShellType.commandPrompt); - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); - }); - - test('When not in experiment, do not show notification for the same', async () => { - reset(experimentService); - when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test('Do not show notification if notification is disabled', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(false); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test('Do not show notification when virtual env is not activated for terminal', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Conda, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test("Disable notification if `Don't show again` is clicked", async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(notificationEnabled.updateValue(false)).once(); - }); - - test('Disable notification if `Done, it works` is clicked', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn( - Promise.resolve(Interpreters.deactivateDoneButton), - ); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(notificationEnabled.updateValue(false)).once(); - }); - - test('Open link to workaround if `See instructions` is clicked', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.seeInstructions)); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); - verify(browserService.launch(anything())).once(); - }); - - test('Do not perform any action if prompt is closed', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); - verify(notificationEnabled.updateValue(false)).never(); - verify(browserService.launch(anything())).never(); - }); -}); diff --git a/src/test/terminals/serviceRegistry.unit.test.ts b/src/test/terminals/serviceRegistry.unit.test.ts index 816afa17cf88..38a9a9744e91 100644 --- a/src/test/terminals/serviceRegistry.unit.test.ts +++ b/src/test/terminals/serviceRegistry.unit.test.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import * as typemoq from 'typemoq'; -import { IExtensionActivationService, IExtensionSingleActivationService } from '../../client/activation/types'; import { IServiceManager } from '../../client/ioc/types'; import { TerminalAutoActivation } from '../../client/terminals/activation'; import { CodeExecutionManager } from '../../client/terminals/codeExecution/codeExecutionManager'; @@ -10,16 +9,12 @@ import { DjangoShellCodeExecutionProvider } from '../../client/terminals/codeExe import { CodeExecutionHelper } from '../../client/terminals/codeExecution/helper'; import { ReplProvider } from '../../client/terminals/codeExecution/repl'; import { TerminalCodeExecutionProvider } from '../../client/terminals/codeExecution/terminalCodeExecution'; -import { TerminalDeactivateLimitationPrompt } from '../../client/terminals/envCollectionActivation/deactivatePrompt'; -import { TerminalIndicatorPrompt } from '../../client/terminals/envCollectionActivation/indicatorPrompt'; -import { TerminalEnvVarCollectionService } from '../../client/terminals/envCollectionActivation/service'; import { registerTypes } from '../../client/terminals/serviceRegistry'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation, - ITerminalEnvVarCollectionService, } from '../../client/terminals/types'; suite('Terminal - Service Registry', () => { @@ -32,9 +27,6 @@ suite('Terminal - Service Registry', () => { [ICodeExecutionService, ReplProvider, 'repl'], [ITerminalAutoActivation, TerminalAutoActivation], [ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'], - [ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService], - [IExtensionSingleActivationService, TerminalIndicatorPrompt], - [IExtensionSingleActivationService, TerminalDeactivateLimitationPrompt], ].forEach((args) => { if (args.length === 2) { services @@ -58,14 +50,6 @@ suite('Terminal - Service Registry', () => { .verifiable(typemoq.Times.once()); } }); - services - .setup((s) => - s.addBinding( - typemoq.It.is((v) => ITerminalEnvVarCollectionService === v), - typemoq.It.is((value) => IExtensionActivationService === value), - ), - ) - .verifiable(typemoq.Times.once()); registerTypes(services.object); diff --git a/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts deleted file mode 100644 index 6913b862c70f..000000000000 --- a/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - // https://github.com/microsoft/vscode/issues/78502 - // - // This API is still proposed but we don't intent on promoting it to stable due to problems - // around performance. See #145234 for a more likely API to get stabilized. - - export interface TerminalDataWriteEvent { - /** - * The {@link Terminal} for which the data was written. - */ - readonly terminal: Terminal; - /** - * The data being written. - */ - readonly data: string; - } - - namespace window { - /** - * An event which fires when the terminal's child pseudo-device is written to (the shell). - * In other words, this provides access to the raw data stream from the process running - * within the terminal, including VT sequences. - */ - export const onDidWriteTerminalData: Event; - } -} From e7dfef8f5ed9f9b59b75fd9d0d64118734224bdd Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 6 Oct 2023 11:57:18 -0700 Subject: [PATCH 318/336] fix to get user defined env and use it to set up testing subprocess (#22165) fixes https://github.com/microsoft/vscode-python/issues/21642 and https://github.com/microsoft/vscode-python/issues/22166 --- .../testing/testController/common/server.ts | 18 ++++---- .../testing/testController/common/types.ts | 2 + .../testing/testController/controller.ts | 6 +++ .../pytest/pytestDiscoveryAdapter.ts | 15 ++++--- .../pytest/pytestExecutionAdapter.ts | 32 +++++++------ .../unittest/testDiscoveryAdapter.ts | 17 +++++-- .../unittest/testExecutionAdapter.ts | 8 +++- .../testing/common/testingAdapter.test.ts | 15 +++++++ .../pytestDiscoveryAdapter.unit.test.ts | 45 ++++++++++++------- .../pytestExecutionAdapter.unit.test.ts | 37 ++++++++------- .../testController/server.unit.test.ts | 32 ++++++++----- .../testCancellationRunAdapters.unit.test.ts | 2 + .../testDiscoveryAdapter.unit.test.ts | 36 +++++++-------- 13 files changed, 171 insertions(+), 94 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 46217eab0459..7437a44d6080 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -23,6 +23,7 @@ import { extractJsonPayload, } from './utils'; import { createDeferred } from '../../../common/utils/async'; +import { EnvironmentVariables } from '../../../api/types'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); @@ -165,28 +166,29 @@ export class PythonTestServer implements ITestServer, Disposable { async sendCommand( options: TestCommandOptions, + env: EnvironmentVariables, runTestIdPort?: string, runInstance?: TestRun, testIds?: string[], callback?: () => void, ): Promise { const { uuid } = options; - + // get and edit env vars + const mutableEnv = { ...env }; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [options.cwd, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_UUID = uuid.toString(); + mutableEnv.TEST_PORT = this.getPort().toString(); + mutableEnv.RUN_TEST_IDS_PORT = runTestIdPort; + const spawnOptions: SpawnOptions = { token: options.token, cwd: options.cwd, throwOnStdErr: true, outputChannel: options.outChannel, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.getPort().toString(), - }, + env: mutableEnv, }; - - if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; const isRun = runTestIdPort !== undefined; // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 32e0c4ba8cc6..e51270eb4f9e 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -15,6 +15,7 @@ import { import { ITestDebugLauncher, TestDiscoveryOptions } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; import { Deferred } from '../../../common/utils/async'; +import { EnvironmentVariables } from '../../../common/variables/types'; export type TestRunInstanceOptions = TestRunOptions & { exclude?: readonly TestItem[]; @@ -177,6 +178,7 @@ export interface ITestServer { readonly onDiscoveryDataReceived: Event; sendCommand( options: TestCommandOptions, + env: EnvironmentVariables, runTestIdsPort?: string, runInstance?: TestRun, testIds?: string[], diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index af77ab2b2525..a87017a26a51 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -50,6 +50,7 @@ import { ITestDebugLauncher } from '../common/types'; import { IServiceContainer } from '../../ioc/types'; import { PythonResultResolver } from './common/resultResolver'; import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; @@ -100,6 +101,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, @inject(ITestOutputChannel) private readonly testOutputChannel: ITestOutputChannel, @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, ) { this.refreshCancellation = new CancellationTokenSource(); @@ -174,12 +176,14 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); executionAdapter = new UnittestTestExecutionAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); } else { testProvider = PYTEST_PROVIDER; @@ -189,12 +193,14 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); executionAdapter = new PytestTestExecutionAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); } diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index c0e1a310ee4a..4ed2570ba7cc 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -19,6 +19,7 @@ import { ITestServer, } from '../common/types'; import { createDiscoveryErrorPayload, createEOTPayload, createTestingDeferred } from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied @@ -29,6 +30,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { @@ -61,18 +63,21 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + // get and edit env vars + const mutableEnv = { + ...(await this.envVarsService?.getEnvironmentVariables(uri)), + }; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_UUID = uuid.toString(); + mutableEnv.TEST_PORT = this.testServer.getPort().toString(); const spawnOptions: SpawnOptions = { cwd, throwOnStdErr: true, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.testServer.getPort().toString(), - }, outputChannel: this.outputChannel, + env: mutableEnv, }; // Create the Python environment in which to execute the command. diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 8020be17cf90..eb8e9b6f935a 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -24,6 +24,7 @@ import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; import * as utils from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( @@ -31,6 +32,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} async runTests( @@ -46,6 +48,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const deferredTillEOT: Deferred = utils.createTestingDeferred(); const dataReceivedDisposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + runInstance?.token.isCancellationRequested; if (runInstance) { const eParsed = JSON.parse(e.data); this.resultResolver?.resolveExecution(eParsed, runInstance, deferredTillEOT); @@ -105,20 +108,13 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - + // get and edit env vars + const mutableEnv = { ...(await this.envVarsService?.getEnvironmentVariables(uri)) }; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - const spawnOptions: SpawnOptions = { - cwd, - throwOnStdErr: true, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.testServer.getPort().toString(), - }, - outputChannel: this.outputChannel, - stdinStr: testIds.toString(), - }; + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_UUID = uuid.toString(); + mutableEnv.TEST_PORT = this.testServer.getPort().toString(); // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { @@ -141,9 +137,17 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testArgs.push('--capture', 'no'); } + // add port with run test ids to env vars const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); - if (spawnOptions.extraVariables) - spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); + mutableEnv.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + outputChannel: this.outputChannel, + stdinStr: testIds.toString(), + env: mutableEnv, + }; if (debugBool) { const pytestPort = this.testServer.getPort().toString(); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 440df4f94dc6..75e29afc9712 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -15,6 +15,7 @@ import { TestDiscoveryCommand, } from '../common/types'; import { Deferred, createDeferred } from '../../../common/utils/async'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. @@ -25,13 +26,17 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} public async discoverTests(uri: Uri): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - + let env: EnvironmentVariables | undefined = await this.envVarsService?.getEnvironmentVariables(uri); + if (env === undefined) { + env = {} as EnvironmentVariables; + } const command = buildDiscoveryCommand(unittestArgs); const uuid = this.testServer.createUUID(uri.fsPath); @@ -52,7 +57,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { dataReceivedDisposable.dispose(); }; - await this.callSendCommand(options, () => { + await this.callSendCommand(options, env, () => { disposeDataReceiver?.(this.testServer); }); await deferredTillEOT.promise; @@ -66,8 +71,12 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { - await this.testServer.sendCommand(options, undefined, undefined, [], callback); + private async callSendCommand( + options: TestCommandOptions, + env: EnvironmentVariables, + callback: () => void, + ): Promise { + await this.testServer.sendCommand(options, env, undefined, undefined, [], callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 9da0872ef601..d90581a93110 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -17,6 +17,7 @@ import { } from '../common/types'; import { traceError, traceInfo, traceLog } from '../../../logging'; import { startTestIdServer } from '../common/utils'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; /** * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? @@ -28,6 +29,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} public async runTests( @@ -78,6 +80,10 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const command = buildExecutionCommand(unittestArgs); + let env: EnvironmentVariables | undefined = await this.envVarsService?.getEnvironmentVariables(uri); + if (env === undefined) { + env = {} as EnvironmentVariables; + } const options: TestCommandOptions = { workspaceFolder: uri, @@ -92,7 +98,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, testIds, () => { + await this.testServer.sendCommand(options, env, runTestIdsPort.toString(), runInstance, testIds, () => { deferredTillEOT?.resolve(); }); // placeholder until after the rewrite is adopted diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 4f46f1cf738c..3b5ef0062a98 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -20,6 +20,7 @@ import { UnittestTestExecutionAdapter } from '../../../client/testing/testContro import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; import { TestProvider } from '../../../client/testing/types'; import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; suite('End to End Tests: test adapters', () => { let resultResolver: ITestResultResolver; @@ -28,6 +29,7 @@ suite('End to End Tests: test adapters', () => { let debugLauncher: ITestDebugLauncher; let configService: IConfigurationService; let serviceContainer: IServiceContainer; + let envVarsService: IEnvironmentVariablesProvider; let workspaceUri: Uri; let testOutputChannel: typeMoq.IMock; let testController: TestController; @@ -67,6 +69,7 @@ suite('End to End Tests: test adapters', () => { pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); debugLauncher = serviceContainer.get(ITestDebugLauncher); testController = serviceContainer.get(ITestController); + envVarsService = serviceContainer.get(IEnvironmentVariablesProvider); // create objects that were not injected pythonTestServer = new PythonTestServer(pythonExecFactory, debugLauncher); @@ -121,6 +124,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { @@ -167,6 +171,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { @@ -206,6 +211,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); // set workspace to test workspace folder @@ -248,6 +254,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); // set workspace to test workspace folder @@ -301,6 +308,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -353,6 +361,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -403,6 +412,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -467,6 +477,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -524,6 +535,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -579,6 +591,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); // set workspace to test workspace folder @@ -641,6 +654,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -701,6 +715,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 8ba7dd9a6f00..7badb5a0350d 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -33,6 +33,7 @@ suite('pytest test discovery adapter', () => { let uri: Uri; let expectedExtraVariables: Record; let mockProc: MockChildProcess; + let deferred2: Deferred; setup(() => { const mockExtensionRootDir = typeMoq.Mock.ofType(); @@ -73,20 +74,25 @@ suite('pytest test discovery adapter', () => { // set up exec service with child process mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + outputChannel = typeMoq.Mock.ofType(); + const output = new Observable>(() => { /* no op */ }); + deferred2 = createDeferred(); execService .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => ({ - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - })); - execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); - outputChannel = typeMoq.Mock.ofType(); + .returns(() => { + deferred2.resolve(); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); }); test('Discovery should call exec with correct basic args', async () => { // set up exec mock @@ -98,24 +104,28 @@ suite('pytest test discovery adapter', () => { deferred.resolve(); return Promise.resolve(execService.object); }); - adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); adapter.discoverTests(uri, execFactory.object); // add in await and trigger await deferred.promise; + await deferred2.promise; mockProc.trigger('close'); // verification - const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; execService.verify( (x) => x.execObservable( - expectedArgs, + typeMoq.It.isAny(), typeMoq.It.is((options) => { - assert.deepEqual(options.extraVariables, expectedExtraVariables); - assert.equal(options.cwd, expectedPath); - assert.equal(options.throwOnStdErr, true); - return true; + try { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } }), ), typeMoq.Times.once(), @@ -147,6 +157,7 @@ suite('pytest test discovery adapter', () => { adapter.discoverTests(uri, execFactory.object); // add in await and trigger await deferred.promise; + await deferred2.promise; mockProc.trigger('close'); // verification @@ -156,7 +167,7 @@ suite('pytest test discovery adapter', () => { x.execObservable( expectedArgs, typeMoq.It.is((options) => { - assert.deepEqual(options.extraVariables, expectedExtraVariables); + assert.deepEqual(options.env, expectedExtraVariables); assert.equal(options.cwd, expectedPathNew); assert.equal(options.throwOnStdErr, true); return true; diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index a2e5c810dc86..a097df654360 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -30,6 +30,7 @@ suite('pytest test execution adapter', () => { let adapter: PytestTestExecutionAdapter; let execService: typeMoq.IMock; let deferred: Deferred; + let deferred4: Deferred; let debugLauncher: typeMoq.IMock; (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; @@ -59,16 +60,20 @@ suite('pytest test execution adapter', () => { const output = new Observable>(() => { /* no op */ }); + deferred4 = createDeferred(); execService = typeMoq.Mock.ofType(); execService .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => ({ - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - })); + .returns(() => { + deferred4.resolve(); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); execFactory = typeMoq.Mock.ofType(); utilsStartServerStub = sinon.stub(util, 'startTestIdServer'); debugLauncher = typeMoq.Mock.ofType(); @@ -161,6 +166,7 @@ suite('pytest test execution adapter', () => { await deferred2.promise; await deferred3.promise; + await deferred4.promise; mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); @@ -176,10 +182,10 @@ suite('pytest test execution adapter', () => { x.execObservable( expectedArgs, typeMoq.It.is((options) => { - assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); - assert.equal(options.extraVariables?.TEST_UUID, expectedExtraVariables.TEST_UUID); - assert.equal(options.extraVariables?.TEST_PORT, expectedExtraVariables.TEST_PORT); - assert.equal(options.extraVariables?.RUN_TEST_IDS_PORT, '54321'); + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_UUID, expectedExtraVariables.TEST_UUID); + assert.equal(options.env?.TEST_PORT, expectedExtraVariables.TEST_PORT); + assert.equal(options.env?.RUN_TEST_IDS_PORT, '54321'); assert.equal(options.cwd, uri.fsPath); assert.equal(options.throwOnStdErr, true); return true; @@ -227,6 +233,7 @@ suite('pytest test execution adapter', () => { await deferred2.promise; await deferred3.promise; + await deferred4.promise; mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); @@ -243,10 +250,10 @@ suite('pytest test execution adapter', () => { x.execObservable( expectedArgs, typeMoq.It.is((options) => { - assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); - assert.equal(options.extraVariables?.TEST_UUID, expectedExtraVariables.TEST_UUID); - assert.equal(options.extraVariables?.TEST_PORT, expectedExtraVariables.TEST_PORT); - assert.equal(options.extraVariables?.RUN_TEST_IDS_PORT, '54321'); + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_UUID, expectedExtraVariables.TEST_UUID); + assert.equal(options.env?.TEST_PORT, expectedExtraVariables.TEST_PORT); + assert.equal(options.env?.RUN_TEST_IDS_PORT, '54321'); assert.equal(options.cwd, newCwd); assert.equal(options.throwOnStdErr, true); return true; diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 02c35e806156..eaf94eca5189 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -139,7 +139,7 @@ suite('Python Test Server, DataWithPayloadChunks', () => { traceLog('Socket connection error:', error); }); - server.sendCommand(options); + server.sendCommand(options, {}); await deferred.promise; const expectedResult = dataWithPayloadChunks.data; assert.deepStrictEqual(eventData, expectedResult); @@ -176,32 +176,35 @@ suite('Python Test Server, Send command etc', () => { test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { const deferred2 = createDeferred(); const RUN_TEST_IDS_PORT_CONST = '5678'; + let error = false; + let errorMessage = ''; execService .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((_args, options2) => { try { assert.strictEqual( - options2.extraVariables.PYTHONPATH, + options2.env.PYTHONPATH, '/foo/bar', 'Expect python path to exist as extra variable and be set correctly', ); assert.strictEqual( - options2.extraVariables.RUN_TEST_IDS_PORT, + options2.env.RUN_TEST_IDS_PORT, RUN_TEST_IDS_PORT_CONST, 'Expect test id port to be in extra variables and set correctly', ); assert.strictEqual( - options2.extraVariables.TEST_UUID, + options2.env.TEST_UUID, FAKE_UUID, 'Expect test uuid to be in extra variables and set correctly', ); assert.strictEqual( - options2.extraVariables.TEST_PORT, - 12345, + options2.env.TEST_PORT, + '12345', 'Expect server port to be set correctly as a env var', ); } catch (e) { - assert(false, 'Error parsing data, extra variables do not match'); + error = true; + errorMessage = `error occurred, assertion was incorrect, ${e}`; } return typeMoq.Mock.ofType>().object; }); @@ -222,13 +225,20 @@ suite('Python Test Server, Send command etc', () => { cwd: '/foo/bar', uuid: FAKE_UUID, }; - server.sendCommand(options, RUN_TEST_IDS_PORT_CONST); + try { + server.sendCommand(options, {}, RUN_TEST_IDS_PORT_CONST); + } catch (e) { + assert(false, `Error sending command, ${e}`); + } // add in await and trigger await deferred2.promise; mockProc.trigger('close'); const expectedArgs = ['myscript', '-foo', 'foo']; execService.verify((x) => x.execObservable(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); + if (error) { + assert(false, errorMessage); + } }); test('sendCommand should write to an output channel if it is provided as an option', async () => { @@ -260,13 +270,13 @@ suite('Python Test Server, Send command etc', () => { server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - server.sendCommand(options); + server.sendCommand(options, {}); // add in await and trigger await deferred.promise; mockProc.trigger('close'); const expected = ['python', 'myscript', '-foo', 'foo'].join(' '); - + assert.equal(output2.length, 1); assert.deepStrictEqual(output2, [expected]); }); @@ -303,7 +313,7 @@ suite('Python Test Server, Send command etc', () => { eventData = JSON.parse(data); }); - server.sendCommand(options); + server.sendCommand(options, {}); await deferred2.promise; await deferred3.promise; assert.notEqual(eventData, undefined); diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index e85cd2b62834..a0fb4eea8589 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -239,6 +239,7 @@ suite('Execution Flow Run Adapters', () => { typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), + typeMoq.It.isAny(), ), ) .returns(() => { @@ -319,6 +320,7 @@ suite('Execution Flow Run Adapters', () => { typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), + typeMoq.It.isAny(), ), ) .returns(() => { diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index dc883afdf441..0eee88120f6a 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -9,6 +9,7 @@ import { IConfigurationService, ITestOutputChannel } from '../../../../client/co import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; import { UnittestTestDiscoveryAdapter } from '../../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { createDeferred } from '../../../../client/common/utils/async'; suite('Unittest test discovery adapter', () => { let stubConfigSettings: IConfigurationService; @@ -26,10 +27,12 @@ suite('Unittest test discovery adapter', () => { test('DiscoverTests should send the discovery command to the test server with the correct args', async () => { let options: TestCommandOptions | undefined; + const deferred = createDeferred(); const stubTestServer = ({ sendCommand(opt: TestCommandOptions): Promise { delete opt.outChannel; options = opt; + deferred.resolve(); return Promise.resolve(); }, onDiscoveryDataReceived: () => { @@ -44,15 +47,12 @@ suite('Unittest test discovery adapter', () => { const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); adapter.discoverTests(uri); - assert.deepStrictEqual(options, { - workspaceFolder: uri, - cwd: uri.fsPath, - command: { - script, - args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'], - }, - uuid: '123456789', - }); + await deferred.promise; + assert.deepStrictEqual(options?.command?.args, ['--udiscovery', '-v', '-s', '.', '-p', 'test*']); + assert.deepStrictEqual(options.workspaceFolder, uri); + assert.deepStrictEqual(options.cwd, uri.fsPath); + assert.deepStrictEqual(options.command.script, script); + assert.deepStrictEqual(options.uuid, '123456789'); }); test('DiscoverTests should respect settings.testings.cwd when present', async () => { let options: TestCommandOptions | undefined; @@ -62,10 +62,12 @@ suite('Unittest test discovery adapter', () => { }), } as unknown) as IConfigurationService; + const deferred = createDeferred(); const stubTestServer = ({ sendCommand(opt: TestCommandOptions): Promise { delete opt.outChannel; options = opt; + deferred.resolve(); return Promise.resolve(); }, onDiscoveryDataReceived: () => { @@ -80,15 +82,11 @@ suite('Unittest test discovery adapter', () => { const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); adapter.discoverTests(uri); - - assert.deepStrictEqual(options, { - workspaceFolder: uri, - cwd: newCwd, - command: { - script, - args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'], - }, - uuid: '123456789', - }); + await deferred.promise; + assert.deepStrictEqual(options?.command?.args, ['--udiscovery', '-v', '-s', '.', '-p', 'test*']); + assert.deepStrictEqual(options.workspaceFolder, uri); + assert.deepStrictEqual(options.cwd, newCwd); + assert.deepStrictEqual(options.command.script, script); + assert.deepStrictEqual(options.uuid, '123456789'); }); }); From 091e121df020ee7eaafd5e310535245477d40d99 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 6 Oct 2023 18:34:48 -0700 Subject: [PATCH 319/336] fix bug with unittest debug not having args (#22169) --- src/client/testing/common/debugLauncher.ts | 12 +--- .../testing/testController/common/server.ts | 2 + .../testController/server.unit.test.ts | 60 ++++++++++++++++++- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 63e2a4543beb..c76557699ff2 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -205,19 +205,13 @@ export class DebugLauncher implements ITestDebugLauncher { } launchArgs.request = 'launch'; - // Both types of tests need to have the port for the test result server. - if (options.runTestIdsPort) { - launchArgs.env = { - ...launchArgs.env, - RUN_TEST_IDS_PORT: options.runTestIdsPort, - }; - } - if (options.testProvider === 'pytest' && pythonTestAdapterRewriteExperiment) { - if (options.pytestPort && options.pytestUUID) { + if (pythonTestAdapterRewriteExperiment) { + if (options.pytestPort && options.pytestUUID && options.runTestIdsPort) { launchArgs.env = { ...launchArgs.env, TEST_PORT: options.pytestPort, TEST_UUID: options.pytestUUID, + RUN_TEST_IDS_PORT: options.runTestIdsPort, }; } else { throw Error( diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 7437a44d6080..50ae1f3f7536 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -211,6 +211,8 @@ export class PythonTestServer implements ITestServer, Disposable { token: options.token, testProvider: UNITTEST_PROVIDER, runTestIdsPort: runTestIdPort, + pytestUUID: uuid.toString(), + pytestPort: this.getPort().toString(), }; traceInfo(`Running DEBUG unittest with arguments: ${args}\r\n`); diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index eaf94eca5189..742492b33ba8 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -16,7 +16,7 @@ import { Output, } from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; -import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { ITestDebugLauncher, LaunchOptions } from '../../../client/testing/common/types'; import { Deferred, createDeferred } from '../../../client/common/utils/async'; import { MockChildProcess } from '../../mocks/mockChildProcess'; import { @@ -240,6 +240,64 @@ suite('Python Test Server, Send command etc', () => { assert(false, errorMessage); } }); + test('sendCommand should add right extra variables to command during debug', async () => { + const deferred2 = createDeferred(); + const RUN_TEST_IDS_PORT_CONST = '5678'; + const error = false; + const errorMessage = ''; + const debugLauncherMock = typeMoq.Mock.ofType(); + let actualLaunchOptions: LaunchOptions = {} as LaunchOptions; + const deferred4 = createDeferred(); + debugLauncherMock + .setup((x) => x.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((options, _) => { + actualLaunchOptions = options; + deferred4.resolve(); + return Promise.resolve(); + }); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => typeMoq.Mock.ofType>().object); + const execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + server = new PythonTestServer(execFactory.object, debugLauncherMock.object); + sinon.stub(server, 'getPort').returns(12345); + // const portServer = server.getPort(); + await server.serverReady(); + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: FAKE_UUID, + debugBool: true, + }; + try { + server.sendCommand(options, {}, RUN_TEST_IDS_PORT_CONST); + } catch (e) { + assert(false, `Error sending command, ${e}`); + } + // add in await and trigger + await deferred2.promise; + await deferred4.promise; + mockProc.trigger('close'); + + assert.notDeepEqual(actualLaunchOptions, {}, 'launch options should be set'); + assert.strictEqual(actualLaunchOptions.cwd, '/foo/bar'); + assert.strictEqual(actualLaunchOptions.testProvider, 'unittest'); + assert.strictEqual(actualLaunchOptions.pytestPort, '12345'); + assert.strictEqual(actualLaunchOptions.pytestUUID, 'fake-uuid'); + assert.strictEqual(actualLaunchOptions.runTestIdsPort, '5678'); + + debugLauncherMock.verify((x) => x.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny()), typeMoq.Times.once()); + if (error) { + assert(false, errorMessage); + } + }); test('sendCommand should write to an output channel if it is provided as an option', async () => { const output2: string[] = []; From 92c2a2f0b6fd4fbc6f6f845b5dc2bf36f032b1ce Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 10 Oct 2023 01:13:28 -0700 Subject: [PATCH 320/336] Remove `isort` support (#22187) This feature is now moved to `ms-python.isort` extension. For https://github.com/microsoft/vscode-python/issues/22183 Closes https://github.com/microsoft/vscode-python/issues/22147 --- .github/dependabot.yml | 1 - .vscode/settings.json | 1 - build/test-requirements.txt | 1 - package.json | 17 ---- package.nls.json | 5 -- resources/report_issue_user_settings.json | 4 - src/client/common/application/commands.ts | 1 - src/client/common/configSettings.ts | 11 --- src/client/common/constants.ts | 1 - src/client/common/types.ts | 6 -- src/client/common/utils/localize.ts | 7 -- .../codeActionProvider/isortPrompt.ts | 89 ------------------- .../providers/codeActionProvider/main.ts | 25 +----- src/client/telemetry/index.ts | 8 +- src/test/.vscode/settings.json | 1 - src/test/common.ts | 1 - .../configSettings.unit.test.ts | 2 - src/test/common/productsToTest.ts | 1 - .../codeActionProvider/main.unit.test.ts | 6 +- src/testMultiRootWkspc/multi.code-workspace | 4 - 20 files changed, 6 insertions(+), 186 deletions(-) delete mode 100644 src/client/providers/codeActionProvider/isortPrompt.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index de5ebfe9158b..14c8e18d475d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -37,7 +37,6 @@ updates: - dependency-name: prospector # Due to Python 2.7 and #14477. - dependency-name: pytest # Due to Python 2.7 and #13776. - dependency-name: py # Due to Python 2.7. - - dependency-name: isort - dependency-name: jedi-language-server labels: - 'no-changelog' diff --git a/.vscode/settings.json b/.vscode/settings.json index a5dbb4869fd9..6a9c299aa72b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -47,7 +47,6 @@ "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version "python.linting.enabled": false, "python.formatting.provider": "black", - "python.sortImports.args": ["--profile", "black"], "typescript.preferences.quoteStyle": "single", "javascript.preferences.quoteStyle": "single", "typescriptHero.imports.stringQuoteStyle": "'", diff --git a/build/test-requirements.txt b/build/test-requirements.txt index c732b3bcb228..433bd0f86682 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -17,7 +17,6 @@ flask fastapi uvicorn django -isort # Integrated TensorBoard tests tensorboard diff --git a/package.json b/package.json index 47faec663015..074ae69bca5e 100644 --- a/package.json +++ b/package.json @@ -1161,23 +1161,6 @@ "scope": "machine-overridable", "type": "string" }, - "python.sortImports.args": { - "default": [], - "description": "%python.sortImports.args.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "deprecationMessage": "%python.sortImports.args.deprecationMessage%" - }, - "python.sortImports.path": { - "default": "", - "description": "%python.sortImports.path.description%", - "scope": "machine-overridable", - "type": "string", - "deprecationMessage": "%python.sortImports.path.deprecationMessage%" - }, "python.tensorBoard.logDirectory": { "default": "", "description": "%python.tensorBoard.logDirectory.description%", diff --git a/package.nls.json b/package.nls.json index 7d14baf40d64..7a6f789fdf2d 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,5 +1,4 @@ { - "python.command.python.sortImports.title": "Sort Imports", "python.command.python.startREPL.title": "Start REPL", "python.command.python.createEnvironment.title": "Create Environment...", "python.command.python.createNewFile.title": "New Python File", @@ -201,8 +200,6 @@ "python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", - "python.sortImports.args.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.sortImports.path.description": "Path to isort script, default using inner version", "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.", @@ -220,8 +217,6 @@ "python.testing.unittestEnabled.description": "Enable testing using unittest.", "python.venvFolders.description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).", "python.venvPath.description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", - "python.sortImports.args.deprecationMessage": "This setting will be removed soon. Use 'isort.args' instead.", - "python.sortImports.path.deprecationMessage": "This setting will be removed soon. Use 'isort.path' instead.", "walkthrough.pythonWelcome.title": "Get Started with Python Development", "walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", "walkthrough.step.python.createPythonFile.title": "Create a Python file", diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index 778434c5cf0d..677e58d83f21 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -69,10 +69,6 @@ "memory": true, "symbolsHierarchyDepthLimit": false }, - "sortImports": { - "args": "placeholder", - "path": "placeholder" - }, "formatting": { "autopep8Args": "placeholder", "autopep8Path": "placeholder", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index d8944fe2b057..763fa4dde79d 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -93,7 +93,6 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu ['workbench.action.openIssueReporter']: [{ extensionId: string; issueBody: string }]; [Commands.GetSelectedInterpreterPath]: [{ workspaceFolder: string } | string[]]; [Commands.TriggerEnvironmentSelection]: [undefined | Uri]; - [Commands.Sort_Imports]: [undefined, Uri]; [Commands.Exec_In_Terminal]: [undefined, Uri]; [Commands.Exec_In_Terminal_Icon]: [undefined, Uri]; [Commands.Debug_In_Terminal]: [Uri]; diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index db5944cc794b..3e4b75b8b087 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -32,7 +32,6 @@ import { IInterpreterSettings, ILintingSettings, IPythonSettings, - ISortImportSettings, ITensorBoardSettings, ITerminalSettings, Resource, @@ -120,8 +119,6 @@ export class PythonSettings implements IPythonSettings { public terminal!: ITerminalSettings; - public sortImports!: ISortImportSettings; - public globalModuleInstallation = false; public experiments!: IExperiments; @@ -319,14 +316,6 @@ export class PythonSettings implements IPythonSettings { this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; - const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; - if (this.sortImports) { - Object.assign(this.sortImports, sortImportSettings); - } else { - this.sortImports = sortImportSettings; - } - // Support for travis. - this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; // Support for travis. this.linting = this.linting ? this.linting diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index c3705a3c6504..cd6d305f624a 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -58,7 +58,6 @@ export namespace Commands { export const ReportIssue = 'python.reportIssue'; export const Set_Interpreter = 'python.setInterpreter'; export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; - export const Sort_Imports = 'python.sortImports'; export const Start_REPL = 'python.startREPL'; export const Tests_Configure = 'python.configureTests'; export const TriggerEnvironmentSelection = 'python.triggerEnvSelection'; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index b48a2daadaa6..07f1fea6b86b 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -106,7 +106,6 @@ export enum Product { autopep8 = 10, mypy = 11, unittest = 12, - isort = 15, black = 16, bandit = 17, tensorboard = 24, @@ -190,7 +189,6 @@ export interface IPythonSettings { readonly testing: ITestingSettings; readonly autoComplete: IAutoCompleteSettings; readonly terminal: ITerminalSettings; - readonly sortImports: ISortImportSettings; readonly envFile: string; readonly globalModuleInstallation: boolean; readonly experiments: IExperiments; @@ -204,10 +202,6 @@ export interface IPythonSettings { export interface ITensorBoardSettings { logDirectory: string | undefined; } -export interface ISortImportSettings { - readonly path: string; - readonly args: string[]; -} export interface IPylintCategorySeverity { readonly convention: DiagnosticSeverity; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index bc32c1078cad..c6086071363f 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -39,9 +39,6 @@ export namespace Diagnostics { 'Your settings needs to be updated to change the setting "python.unitTest." to "python.testing.", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?', ); export const updateSettings = l10n.t('Yes, update settings'); - export const checkIsort5UpgradeGuide = l10n.t( - 'We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.', - ); export const pylanceDefaultMessage = l10n.t( "The Python extension now includes Pylance to improve completions, code navigation, overall performance and much more! You can learn more about the update and learn how to change your language server [here](https://aka.ms/new-python-bundle).\n\nRead Pylance's license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).", ); @@ -517,12 +514,8 @@ export namespace ToolsExtensions { export const pylintPromptMessage = l10n.t( 'Use the Pylint extension to enable easier configuration and new features such as quick fixes.', ); - export const isortPromptMessage = l10n.t( - 'To use sort imports, install the isort extension. It provides easier configuration and new features such as code actions.', - ); export const installPylintExtension = l10n.t('Install Pylint extension'); export const installFlake8Extension = l10n.t('Install Flake8 extension'); - export const installISortExtension = l10n.t('Install isort extension'); export const selectBlackFormatterPrompt = l10n.t( 'You have the Black formatter extension installed, would you like to use that as the default formatter?', diff --git a/src/client/providers/codeActionProvider/isortPrompt.ts b/src/client/providers/codeActionProvider/isortPrompt.ts deleted file mode 100644 index ffef481b498d..000000000000 --- a/src/client/providers/codeActionProvider/isortPrompt.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IApplicationEnvironment } from '../../common/application/types'; -import { IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { isExtensionDisabled, isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; - -export const ISORT_EXTENSION = 'ms-python.isort'; -const ISORT_PROMPT_DONOTSHOW_KEY = 'showISortExtensionPrompt'; - -function doNotShowPromptState(serviceContainer: IServiceContainer, promptKey: string): IPersistentState { - const persistFactory: IPersistentStateFactory = serviceContainer.get( - IPersistentStateFactory, - ); - return persistFactory.createWorkspacePersistentState(promptKey, false); -} - -export class ISortExtensionPrompt { - private shownThisSession = false; - - public constructor(private readonly serviceContainer: IServiceContainer) {} - - public async showPrompt(): Promise { - const isEnabled = isExtensionEnabled(ISORT_EXTENSION); - if (isEnabled || isExtensionDisabled(ISORT_EXTENSION)) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED, undefined, { - extensionId: ISORT_EXTENSION, - isEnabled, - }); - return true; - } - - const doNotShow = doNotShowPromptState(this.serviceContainer, ISORT_PROMPT_DONOTSHOW_KEY); - if (this.shownThisSession || doNotShow.value) { - return false; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN, undefined, { extensionId: ISORT_EXTENSION }); - this.shownThisSession = true; - const response = await showInformationMessage( - ToolsExtensions.isortPromptMessage, - ToolsExtensions.installISortExtension, - Common.doNotShowAgain, - ); - - if (response === Common.doNotShowAgain) { - await doNotShow.updateValue(true); - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: ISORT_EXTENSION, - dismissType: 'doNotShow', - }); - return false; - } - - if (response === ToolsExtensions.installISortExtension) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED, undefined, { - extensionId: ISORT_EXTENSION, - }); - const appEnv: IApplicationEnvironment = this.serviceContainer.get( - IApplicationEnvironment, - ); - await executeCommand('workbench.extensions.installExtension', ISORT_EXTENSION, { - installPreReleaseVersion: appEnv.extensionChannel === 'insiders', - }); - return true; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: ISORT_EXTENSION, - dismissType: 'close', - }); - - return false; - } -} - -let _prompt: ISortExtensionPrompt | undefined; -export function getOrCreateISortPrompt(serviceContainer: IServiceContainer): ISortExtensionPrompt { - if (!_prompt) { - _prompt = new ISortExtensionPrompt(serviceContainer); - } - return _prompt; -} diff --git a/src/client/providers/codeActionProvider/main.ts b/src/client/providers/codeActionProvider/main.ts index 40afd4dbb2b2..259f42848606 100644 --- a/src/client/providers/codeActionProvider/main.ts +++ b/src/client/providers/codeActionProvider/main.ts @@ -4,23 +4,14 @@ import { inject, injectable } from 'inversify'; import * as vscodeTypes from 'vscode'; import { IExtensionSingleActivationService } from '../../activation/types'; -import { Commands } from '../../common/constants'; import { IDisposableRegistry } from '../../common/types'; -import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; -import { isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { IServiceContainer } from '../../ioc/types'; -import { traceLog } from '../../logging'; -import { getOrCreateISortPrompt, ISORT_EXTENSION } from './isortPrompt'; import { LaunchJsonCodeActionProvider } from './launchJsonCodeActionProvider'; @injectable() export class CodeActionProviderService implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - constructor( - @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IServiceContainer) private serviceContainer: IServiceContainer, - ) {} + constructor(@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry) {} public async activate(): Promise { // eslint-disable-next-line global-require @@ -35,19 +26,5 @@ export class CodeActionProviderService implements IExtensionSingleActivationServ providedCodeActionKinds: [vscode.CodeActionKind.QuickFix], }), ); - this.disposableRegistry.push( - registerCommand(Commands.Sort_Imports, async () => { - const prompt = getOrCreateISortPrompt(this.serviceContainer); - await prompt.showPrompt(); - if (!isExtensionEnabled(ISORT_EXTENSION)) { - traceLog( - 'Sort Imports: Please install and enable `ms-python.isort` extension to use this feature.', - ); - return; - } - - executeCommand('editor.action.organizeImports'); - }), - ); } } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 600f9a2d48ff..7883e0cd7555 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2181,7 +2181,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; + extensionId: 'ms-python.pylint' | 'ms-python.flake8'; isEnabled: boolean; }; /** @@ -2193,7 +2193,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; + extensionId: 'ms-python.pylint' | 'ms-python.flake8'; }; /** * Telemetry event sent when clicking to install linter or formatter extension from the suggestion prompt. @@ -2204,7 +2204,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; + extensionId: 'ms-python.pylint' | 'ms-python.flake8'; }; /** * Telemetry event sent when dismissing prompt suggesting to install the linter or formatter extension. @@ -2216,7 +2216,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; + extensionId: 'ms-python.pylint' | 'ms-python.flake8'; dismissType: 'close' | 'doNotShow'; }; /* __GDPR__ diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index ef9292849a9d..771962b5a909 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -3,7 +3,6 @@ "python.linting.flake8Enabled": false, "python.testing.pytestArgs": [], "python.testing.unittestArgs": ["-s=./tests", "-p=test_*.py", "-v", "-s", ".", "-p", "*test*.py"], - "python.sortImports.args": [], "python.linting.lintOnSave": false, "python.linting.enabled": true, "python.linting.pycodestyleEnabled": false, diff --git a/src/test/common.ts b/src/test/common.ts index 95345f91e5e0..4cc985c795b6 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -53,7 +53,6 @@ export type PythonSettingKeys = | 'testing.pytestArgs' | 'testing.unittestArgs' | 'formatting.provider' - | 'sortImports.args' | 'testing.pytestEnabled' | 'testing.unittestEnabled' | 'envFile' diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index eeaed6aa996b..113770122fbc 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -22,7 +22,6 @@ import { IFormattingSettings, IInterpreterSettings, ILintingSettings, - ISortImportSettings, ITerminalSettings, } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; @@ -118,7 +117,6 @@ suite('Python Settings', async () => { // complex settings config.setup((c) => c.get('interpreter')).returns(() => sourceSettings.interpreter); config.setup((c) => c.get('linting')).returns(() => sourceSettings.linting); - config.setup((c) => c.get('sortImports')).returns(() => sourceSettings.sortImports); config.setup((c) => c.get('formatting')).returns(() => sourceSettings.formatting); config.setup((c) => c.get('autoComplete')).returns(() => sourceSettings.autoComplete); config.setup((c) => c.get('testing')).returns(() => sourceSettings.testing); diff --git a/src/test/common/productsToTest.ts b/src/test/common/productsToTest.ts index 7fc06863f67c..861bab898509 100644 --- a/src/test/common/productsToTest.ts +++ b/src/test/common/productsToTest.ts @@ -17,7 +17,6 @@ export function getProductsForInstallerTests(): { name: string; value: Product } 'yapf', 'autopep8', 'mypy', - 'isort', 'black', 'bandit', ].includes(p.name), diff --git a/src/test/providers/codeActionProvider/main.unit.test.ts b/src/test/providers/codeActionProvider/main.unit.test.ts index 501c3c7eca2b..55644d80ae54 100644 --- a/src/test/providers/codeActionProvider/main.unit.test.ts +++ b/src/test/providers/codeActionProvider/main.unit.test.ts @@ -8,7 +8,6 @@ import rewiremock from 'rewiremock'; import * as typemoq from 'typemoq'; import { CodeActionKind, CodeActionProvider, CodeActionProviderMetadata, DocumentSelector } from 'vscode'; import { IDisposableRegistry } from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; import { CodeActionProviderService } from '../../../client/providers/codeActionProvider/main'; @@ -38,10 +37,7 @@ suite('Code Action Provider service', async () => { }; rewiremock.enable(); rewiremock('vscode').with(vscodeMock); - const quickFixService = new CodeActionProviderService( - typemoq.Mock.ofType().object, - typemoq.Mock.ofType().object, - ); + const quickFixService = new CodeActionProviderService(typemoq.Mock.ofType().object); await quickFixService.activate(); diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace index 1daf409a0836..9d5c8ac77475 100644 --- a/src/testMultiRootWkspc/multi.code-workspace +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -38,10 +38,6 @@ "python.linting.pycodestyleEnabled": false, "python.linting.prospectorEnabled": false, "python.formatting.provider": "yapf", - "python.sortImports.args": [ - "-sp", - "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/sorting/withconfig" - ], "python.linting.lintOnSave": false, "python.linting.enabled": true, "python.pythonPath": "python" From 066550632f06a6a6b5d0c78ea315e131439fba46 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 10 Oct 2023 09:00:05 -0700 Subject: [PATCH 321/336] fix unittest output to remove print of only object reference (#22180) the traceback object was incorrectly printed as just the reference to the object in the error message. Ended up just removing it since it is correctly printed in the traceback object which is where it should ultimately belong. closes: https://github.com/microsoft/vscode-python/issues/22181 --------- Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> --- .../tests/unittestadapter/test_execution.py | 3 +++ pythonFiles/unittestadapter/execution.py | 15 ++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index f7306e37662e..ccf13d983c60 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -202,6 +202,9 @@ def test_failed_tests(): assert "outcome" in id_result assert id_result["outcome"] == "failure" assert "message" and "traceback" in id_result + assert "2 not greater than 3" in str(id_result["message"]) or "1 == 1" in str( + id_result["traceback"] + ) assert True diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 0684ada8e44b..5f46bda95328 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -104,13 +104,18 @@ def formatResult( subtest: Union[unittest.TestCase, None] = None, ): tb = None - if error and error[2] is not None: - # Format traceback + + message = "" + # error is a tuple of the form returned by sys.exc_info(): (type, value, traceback). + if error is not None: + try: + message = f"{error[0]} {error[1]}" + except Exception: + message = "Error occurred, unknown type or value" formatted = traceback.format_exception(*error) + tb = "".join(formatted) # Remove the 'Traceback (most recent call last)' formatted = formatted[1:] - tb = "".join(formatted) - if subtest: test_id = subtest.id() else: @@ -119,7 +124,7 @@ def formatResult( result = { "test": test.id(), "outcome": outcome, - "message": str(error), + "message": message, "traceback": tb, "subtest": subtest.id() if subtest else None, } From 56661a1576b93430953f249cda582eeef30ff543 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 10 Oct 2023 10:49:05 -0700 Subject: [PATCH 322/336] REPL Smart Shift+Enter and Dynamic Smart Cursor (#21779) There are two Feature Requests from: #18105 #21838 They are grouped together to provide the smoothest experience: when user wants to press shift+enter and smoothly move between each executable Python code block without having to manually move their cursor. #19955 (For Execute line/selection and advance to next line, referred to as dynamic smart cursor hereby) Open Issue: #21778 #21838 Steps in implementing REPL Smart Send (smart shift+enter to the REPL) and dynamic cursor move aka. Move to Next Line (next executable line of code to be more precise): 1. Figure out the workflow of where things start and run when user clicks on run selection/line 2. Send the content of selection & document to the Python Side from Typescript side. 3. Respect and follow previous logic/code for EXPLICIT selection (user has highlighting particular text they want to send to REPL), but otherwise, use newly created smart send code. 4. Receive content of document & selection in Python Side 5. Use AST (From Python standard library) to figure out if selection if selection is part of, for example, dictionary, but look for nodes and how each relates to the top level. If some selection is, for example part of a dictionary, we should run the whole dictionary. Look at how to do this for all top level, so that we run the Minimum Viable Block possible. (For example, if user selects part of a dictionary to run in REPL, it will select and send only the dictionary not the whole class or file, etc) 6. Receive the commands to run in typescript side and send it to the REPL 7. After the user has ran shift+enter(non highlight, meaning there was no explicit highlight of text), thus the incurring of smart send, and we have processed the smart selection, figure out the "next" executable line of code in the currently opened Python file. 8. After figuring out the "next" line number, we will move user's cursor to that line number. - [x] Additional scope for telemetry EventName.EXECUTION_CODE with the scope of 'line' in addition to differentiate the explicit selection usage compared to line or executable block. - [x] Drop 3.7 support before merging since end_line attribute of the AST module is only supported for Python version 3.8 and above. - [x] Python tests for both smart selection, dynamic cursor move. - [x] TypeScript tests for smart selection, dynamic cursor move. Notes: * To be shipped after dropping Python3.7 support, since end_lineno, which is critical in smart shift+enter logic, is only for Python version GREATER than 3.7 Update (9/14/23: Python 3.7 support is dropped from the VS Code Python extension: #21962) * Code in regards to this feature(s) should be wrapped in standard experiment (not setting based experiment) * Respective Telemetry should also be attached * EXPLICIT (highlight) selection of the text, and shift+enter/run selection should respect user's selection and send AS IT IS. (When the user selects/highlight specifically what they want to send, we should respect user's selection and send the selection as they are selected) * Smart Shift+Enter should be shipped together with dynamic smart cursor movement for smoothest experience. This way user could shift+enter line by line (or more accurately top block after another top block) as they shift+enter their code. * Be careful with line_no usage between vscode and python as vscode counts line number starting from 0 and python ast start as normal (starts from line 1)) So vscode_lineno + 1 = python_ast_lineno --------- Co-authored-by: Karthik Nadig --- package.json | 12 +- package.nls.json | 1 + pythonFiles/normalizeSelection.py | 149 ++++++- pythonFiles/tests/test_dynamic_cursor.py | 203 +++++++++ pythonFiles/tests/test_normalize_selection.py | 53 +++ pythonFiles/tests/test_smart_selection.py | 388 ++++++++++++++++++ src/client/common/application/commands.ts | 8 + src/client/common/experiments/groups.ts | 4 + src/client/telemetry/index.ts | 4 +- .../codeExecution/codeExecutionManager.ts | 6 +- src/client/terminals/codeExecution/helper.ts | 78 +++- src/client/terminals/types.ts | 2 +- .../terminalExec/sample_smart_selection.py | 21 + src/test/smoke/smartSend.smoke.test.ts | 0 .../terminals/codeExecution/smartSend.test.ts | 229 +++++++++++ .../smokeTests/create_delete_file.py | 5 + 16 files changed, 1148 insertions(+), 15 deletions(-) create mode 100644 pythonFiles/tests/test_dynamic_cursor.py create mode 100644 pythonFiles/tests/test_smart_selection.py create mode 100644 src/test/pythonFiles/terminalExec/sample_smart_selection.py create mode 100644 src/test/smoke/smartSend.smoke.test.ts create mode 100644 src/test/terminals/codeExecution/smartSend.test.ts create mode 100644 src/testMultiRootWkspc/smokeTests/create_delete_file.py diff --git a/package.json b/package.json index 074ae69bca5e..0f93441c6113 100644 --- a/package.json +++ b/package.json @@ -536,14 +536,16 @@ "pythonSurveyNotification", "pythonPromptNewToolsExt", "pythonTerminalEnvVarActivation", - "pythonTestAdapter" + "pythonTestAdapter", + "pythonREPLSmartSend" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", "%python.experiments.pythonTerminalEnvVarActivation.description%", - "%python.experiments.pythonTestAdapter.description%" + "%python.experiments.pythonTestAdapter.description%", + "%python.experiments.pythonREPLSmartSend.description%" ] }, "scope": "machine", @@ -559,14 +561,16 @@ "pythonSurveyNotification", "pythonPromptNewToolsExt", "pythonTerminalEnvVarActivation", - "pythonTestAdapter" + "pythonTestAdapter", + "pythonREPLSmartSend" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", "%python.experiments.pythonTerminalEnvVarActivation.description%", - "%python.experiments.pythonTestAdapter.description%" + "%python.experiments.pythonTestAdapter.description%", + "%python.experiments.pythonREPLSmartSend.description%" ] }, "scope": "machine", diff --git a/package.nls.json b/package.nls.json index 7a6f789fdf2d..5687e51ab9df 100644 --- a/package.nls.json +++ b/package.nls.json @@ -41,6 +41,7 @@ "python.experiments.pythonPromptNewToolsExt.description": "Denotes the Python Prompt New Tools Extension experiment.", "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", + "python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.", "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", "python.formatting.autopep8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", "python.formatting.autopep8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", diff --git a/pythonFiles/normalizeSelection.py b/pythonFiles/normalizeSelection.py index 0363702717ab..0ac47ab5dc3b 100644 --- a/pythonFiles/normalizeSelection.py +++ b/pythonFiles/normalizeSelection.py @@ -6,6 +6,7 @@ import re import sys import textwrap +from typing import Iterable def split_lines(source): @@ -118,6 +119,8 @@ def normalize_lines(selection): # Insert a newline between each top-level statement, and append a newline to the selection. source = "\n".join(statements) + "\n" + if selection[-2] == "}": + source = source[:-1] except Exception: # If there's a problem when parsing statements, # append a blank line to end the block and send it as-is. @@ -126,17 +129,159 @@ def normalize_lines(selection): return source +top_level_nodes = [] +min_key = None + + +def check_exact_exist(top_level_nodes, start_line, end_line): + exact_nodes = [] + for node in top_level_nodes: + if node.lineno == start_line and node.end_lineno == end_line: + exact_nodes.append(node) + + return exact_nodes + + +def traverse_file(wholeFileContent, start_line, end_line, was_highlighted): + """ + Intended to traverse through a user's given file content and find, collect all appropriate lines + that should be sent to the REPL in case of smart selection. + This could be exact statement such as just a single line print statement, + or a multiline dictionary, or differently styled multi-line list comprehension, etc. + Then call the normalize_lines function to normalize our smartly selected code block. + """ + + parsed_file_content = ast.parse(wholeFileContent) + smart_code = "" + should_run_top_blocks = [] + + # Purpose of this loop is to fetch and collect all the + # AST top level nodes, and its node.body as child nodes. + # Individual nodes will contain information like + # the start line, end line and get source segment information + # that will be used to smartly select, and send normalized code. + for node in ast.iter_child_nodes(parsed_file_content): + top_level_nodes.append(node) + + ast_types_with_nodebody = ( + ast.Module, + ast.Interactive, + ast.Expression, + ast.FunctionDef, + ast.AsyncFunctionDef, + ast.ClassDef, + ast.For, + ast.AsyncFor, + ast.While, + ast.If, + ast.With, + ast.AsyncWith, + ast.Try, + ast.Lambda, + ast.IfExp, + ast.ExceptHandler, + ) + if isinstance(node, ast_types_with_nodebody) and isinstance( + node.body, Iterable + ): + for child_nodes in node.body: + top_level_nodes.append(child_nodes) + + exact_nodes = check_exact_exist(top_level_nodes, start_line, end_line) + + # Just return the exact top level line, if present. + if len(exact_nodes) > 0: + which_line_next = 0 + for same_line_node in exact_nodes: + should_run_top_blocks.append(same_line_node) + smart_code += ( + f"{ast.get_source_segment(wholeFileContent, same_line_node)}\n" + ) + which_line_next = get_next_block_lineno(should_run_top_blocks) + return { + "normalized_smart_result": smart_code, + "which_line_next": which_line_next, + } + + # For each of the nodes in the parsed file content, + # add the appropriate source code line(s) to be sent to the REPL, dependent on + # user is trying to send and execute single line/statement or multiple with smart selection. + for top_node in ast.iter_child_nodes(parsed_file_content): + if start_line == top_node.lineno and end_line == top_node.end_lineno: + should_run_top_blocks.append(top_node) + + smart_code += f"{ast.get_source_segment(wholeFileContent, top_node)}\n" + break # If we found exact match, don't waste computation in parsing extra nodes. + elif start_line >= top_node.lineno and end_line <= top_node.end_lineno: + # Case to apply smart selection for multiple line. + # This is the case for when we have to add multiple lines that should be included in the smart send. + # For example: + # 'my_dictionary': { + # 'Audi': 'Germany', + # 'BMW': 'Germany', + # 'Genesis': 'Korea', + # } + # with the mouse cursor at 'BMW': 'Germany', should send all of the lines that pertains to my_dictionary. + + should_run_top_blocks.append(top_node) + + smart_code += str(ast.get_source_segment(wholeFileContent, top_node)) + smart_code += "\n" + + normalized_smart_result = normalize_lines(smart_code) + which_line_next = get_next_block_lineno(should_run_top_blocks) + return { + "normalized_smart_result": normalized_smart_result, + "which_line_next": which_line_next, + } + + +# Look at the last top block added, find lineno for the next upcoming block, +# This will be used in calculating lineOffset to move cursor in VS Code. +def get_next_block_lineno(which_line_next): + last_ran_lineno = int(which_line_next[-1].end_lineno) + next_lineno = int(which_line_next[-1].end_lineno) + + for reverse_node in top_level_nodes: + if reverse_node.lineno > last_ran_lineno: + next_lineno = reverse_node.lineno + break + return next_lineno + + if __name__ == "__main__": # Content is being sent from the extension as a JSON object. # Decode the data from the raw bytes. stdin = sys.stdin if sys.version_info < (3,) else sys.stdin.buffer raw = stdin.read() contents = json.loads(raw.decode("utf-8")) + # Empty highlight means user has not explicitly selected specific text. + empty_Highlight = contents.get("emptyHighlight", False) - normalized = normalize_lines(contents["code"]) + # We also get the activeEditor selection start line and end line from the typescript VS Code side. + # Remember to add 1 to each of the received since vscode starts line counting from 0 . + vscode_start_line = contents["startLine"] + 1 + vscode_end_line = contents["endLine"] + 1 # Send the normalized code back to the extension in a JSON object. - data = json.dumps({"normalized": normalized}) + data = None + which_line_next = 0 + + if empty_Highlight and contents.get("smartSendExperimentEnabled"): + result = traverse_file( + contents["wholeFileContent"], + vscode_start_line, + vscode_end_line, + not empty_Highlight, + ) + normalized = result["normalized_smart_result"] + which_line_next = result["which_line_next"] + data = json.dumps( + {"normalized": normalized, "nextBlockLineno": result["which_line_next"]} + ) + else: + normalized = normalize_lines(contents["code"]) + data = json.dumps({"normalized": normalized}) stdout = sys.stdout if sys.version_info < (3,) else sys.stdout.buffer stdout.write(data.encode("utf-8")) diff --git a/pythonFiles/tests/test_dynamic_cursor.py b/pythonFiles/tests/test_dynamic_cursor.py new file mode 100644 index 000000000000..7aea59427aa6 --- /dev/null +++ b/pythonFiles/tests/test_dynamic_cursor.py @@ -0,0 +1,203 @@ +import importlib +import textwrap + +import normalizeSelection + + +def test_dictionary_mouse_mover(): + """ + Having the mouse cursor on second line, + 'my_dict = {' + and pressing shift+enter should bring the + mouse cursor to line 6, on and to be able to run + 'print('only send the dictionary')' + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + not_dictionary = 'hi' + my_dict = { + "key1": "value1", + "key2": "value2" + } + print('only send the dictionary') + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["which_line_next"] == 6 + + +def test_beginning_func(): + """ + Pressing shift+enter on the very first line, + of function definition, such as 'my_func():' + It should properly skip the comment and assert the + next executable line to be executed is line 5 at + 'my_dict = {' + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def my_func(): + print("line 2") + print("line 3") + # Skip line 4 because it is a comment + my_dict = { + "key1": "value1", + "key2": "value2" + } + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 5 + + +def test_cursor_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + lucid_dream = ["Corgi", "Husky", "Pomsky"] + for dogs in lucid_dream: # initial starting position + print(dogs) + print("I wish I had a dog!") + + print("This should be the next block that should be ran") + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["which_line_next"] == 6 + + +def test_inside_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for food in lucid_dream: + print("We are starting") # initial starting position + print("Next cursor should be here!") + + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["which_line_next"] == 3 + + +def test_skip_sameline_statements(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("Audi");print("BMW");print("Mercedes") + print("Next line to be run is here!") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 2 + + +def test_skip_multi_comp_lambda(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + + my_lambda = lambda x: ( + x + 1 + ) + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + # Shift enter from the very first ( should make + # next executable statement as the lambda expression + assert result["which_line_next"] == 7 + + +def test_move_whole_class(): + """ + Shift+enter on a class definition + should move the cursor after running whole class. + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + print("We should be here after running whole class") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 7 + + +def test_def_to_def(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + + # Skip here + def next_func(): + print("Not here but above") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 9 + + +def test_try_catch_move(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + print("Should be here afterwards") + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["which_line_next"] == 6 + + +def test_skip_nested(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + + print("Cursor should be here after running line 1") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["which_line_next"] == 8 diff --git a/pythonFiles/tests/test_normalize_selection.py b/pythonFiles/tests/test_normalize_selection.py index 138c5ad2f522..5f4d6d7d4a1f 100644 --- a/pythonFiles/tests/test_normalize_selection.py +++ b/pythonFiles/tests/test_normalize_selection.py @@ -1,8 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + +import importlib import textwrap +# __file__ = "/Users/anthonykim/Desktop/vscode-python/pythonFiles/normalizeSelection.py" +# sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__)))) import normalizeSelection @@ -215,3 +219,52 @@ def show_something(): ) result = normalizeSelection.normalize_lines(src) assert result == expected + + def test_fstring(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + name = "Ahri" + age = 10 + + print(f'My name is {name}') + """ + ) + + expected = textwrap.dedent( + """\ + name = "Ahri" + age = 10 + print(f'My name is {name}') + """ + ) + result = normalizeSelection.normalize_lines(src) + + assert result == expected + + def test_list_comp(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + names = ['Ahri', 'Bobby', 'Charlie'] + breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky'] + dogs = [(name, breed) for name, breed in zip(names, breed)] + + print(dogs) + my_family_dog = 'Corgi' + """ + ) + + expected = textwrap.dedent( + """\ + names = ['Ahri', 'Bobby', 'Charlie'] + breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky'] + dogs = [(name, breed) for name, breed in zip(names, breed)] + print(dogs) + my_family_dog = 'Corgi' + """ + ) + + result = normalizeSelection.normalize_lines(src) + + assert result == expected diff --git a/pythonFiles/tests/test_smart_selection.py b/pythonFiles/tests/test_smart_selection.py new file mode 100644 index 000000000000..b86e6f9dc82e --- /dev/null +++ b/pythonFiles/tests/test_smart_selection.py @@ -0,0 +1,388 @@ +import importlib +import textwrap + +import normalizeSelection + + +def test_part_dictionary(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + not_dictionary = 'hi' + my_dict = { + "key1": "value1", + "key2": "value2" + } + print('only send the dictionary') + """ + ) + + expected = textwrap.dedent( + """\ + my_dict = { + "key1": "value1", + "key2": "value2" + } + """ + ) + + result = normalizeSelection.traverse_file(src, 3, 3, False) + assert result["normalized_smart_result"] == expected + + +def test_nested_loop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + """ + ) + expected = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected + + +def test_smart_shift_enter_multiple_statements(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + import textwrap + import ast + + print("Porsche") + print("Genesis") + + + print("Audi");print("BMW");print("Mercedes") + + print("dont print me") + + """ + ) + # Expected to printing statement line by line, + # for when multiple print statements are ran + # from the same line. + expected = textwrap.dedent( + """\ + print("Audi") + print("BMW") + print("Mercedes") + """ + ) + result = normalizeSelection.traverse_file(src, 8, 8, False) + assert result["normalized_smart_result"] == expected + + +def test_two_layer_dictionary(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("dont print me") + + two_layered_dictionary = { + 'inner_dict_one': { + 'Audi': 'Germany', + 'BMW': 'Germnay', + 'Genesis': 'Korea', + }, + 'inner_dict_two': { + 'Mercedes': 'Germany', + 'Porsche': 'Germany', + 'Lamborghini': 'Italy', + 'Ferrari': 'Italy', + 'Maserati': 'Italy' + } + } + """ + ) + expected = textwrap.dedent( + """\ + two_layered_dictionary = { + 'inner_dict_one': { + 'Audi': 'Germany', + 'BMW': 'Germnay', + 'Genesis': 'Korea', + }, + 'inner_dict_two': { + 'Mercedes': 'Germany', + 'Porsche': 'Germany', + 'Lamborghini': 'Italy', + 'Ferrari': 'Italy', + 'Maserati': 'Italy' + } + } + """ + ) + result = normalizeSelection.traverse_file(src, 6, 7, False) + + assert result["normalized_smart_result"] == expected + + +def test_run_whole_func(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("Decide which dog you will choose") + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + """ + ) + + expected = textwrap.dedent( + """\ + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + + """ + ) + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["normalized_smart_result"] == expected + + +def test_small_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + """ + ) + expected = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + + """ + ) + + # Cover the whole for loop block with multiple inner statements + # Make sure to contain all of the print statements included. + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["normalized_smart_result"] == expected + + +def inner_for_loop_component(): + """ + Pressing shift+enter inside a for loop, + specifically on a viable expression + by itself, such as print(i) + should only return that exact expression + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + """ + ) + result = normalizeSelection.traverse_file(src, 2, 2, False) + expected = textwrap.dedent( + """\ + print(i) + """ + ) + + assert result["normalized_smart_result"] == expected + + +def test_dict_comprehension(): + """ + Having the mouse cursor on the first line, + and pressing shift+enter should return the + whole dictionary comp, respecting user's code style. + """ + + importlib.reload + src = textwrap.dedent( + """\ + my_dict_comp = {temp_mover: + temp_mover for temp_mover in range(1, 7)} + """ + ) + + expected = textwrap.dedent( + """\ + my_dict_comp = {temp_mover: + temp_mover for temp_mover in range(1, 7)} + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["normalized_smart_result"] == expected + + +def test_send_whole_generator(): + """ + Pressing shift+enter on the first line, which is the '(' + should be returning the whole generator expression instead of just the '(' + """ + + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + """ + ) + + expected = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["normalized_smart_result"] == expected + + +def test_multiline_lambda(): + """ + Shift+enter on part of the lambda expression + should return the whole lambda expression, + regardless of whether all the component of + lambda expression is on the same or not. + """ + + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + my_lambda = lambda x: ( + x + 1 + ) + """ + ) + expected = textwrap.dedent( + """\ + my_lambda = lambda x: ( + x + 1 + ) + + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected + + +def test_send_whole_class(): + """ + Shift+enter on a class definition + should send the whole class definition + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + print("We should be here after running whole class") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + expected = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + + """ + ) + assert result["normalized_smart_result"] == expected + + +def test_send_whole_if_statement(): + """ + Shift+enter on an if statement + should send the whole if statement + including statements inside and else. + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + if True: + print('send this') + else: + print('also send this') + + print('cursor here afterwards') + """ + ) + expected = textwrap.dedent( + """\ + if True: + print('send this') + else: + print('also send this') + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected + + +def test_send_try(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + print("Not running this") + """ + ) + expected = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 763fa4dde79d..ba29f0dcd956 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -99,4 +99,12 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [Commands.Tests_Configure]: [undefined, undefined | CommandSource, undefined | Uri]; [Commands.LaunchTensorBoard]: [TensorBoardEntrypoint, TensorBoardEntrypointTrigger]; ['workbench.view.testing.focus']: []; + ['cursorMove']: [ + { + to: string; + by: string; + value: number; + }, + ]; + ['cursorEnd']: []; } diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts index 1ee06469095c..b7a598e0a08a 100644 --- a/src/client/common/experiments/groups.ts +++ b/src/client/common/experiments/groups.ts @@ -18,3 +18,7 @@ export enum ShowFormatterExtensionPrompt { export enum EnableTestAdapterRewrite { experiment = 'pythonTestAdapter', } +// Experiment to enable smart shift+enter, advance cursor. +export enum EnableREPLSmartSend { + experiment = 'pythonREPLSmartSend', +} diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 7883e0cd7555..95496c828018 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -821,11 +821,11 @@ export interface IEventNamePropertyMapping { */ [EventName.EXECUTION_CODE]: { /** - * Whether the user executed a file in the terminal or just the selected text. + * Whether the user executed a file in the terminal or just the selected text or line by shift+enter. * * @type {('file' | 'selection')} */ - scope: 'file' | 'selection'; + scope: 'file' | 'selection' | 'line'; /** * How was the code executed (through the command or by clicking the `Run File` icon). * diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 2dd619a1816a..ed31e194b2d2 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -159,7 +159,11 @@ export class CodeExecutionManager implements ICodeExecutionManager { } const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!); - const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!); + let wholeFileContent = ''; + if (activeEditor && activeEditor.document) { + wholeFileContent = activeEditor.document.getText(); + } + const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!, wholeFileContent); if (!normalizedCode || normalizedCode.trim().length === 0) { return; } diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index 0d5694b4a28d..c560de9c17b7 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -5,7 +5,12 @@ import '../../common/extensions'; import { inject, injectable } from 'inversify'; import { l10n, Position, Range, TextEditor, Uri } from 'vscode'; -import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} from '../../common/application/types'; import { PYTHON_LANGUAGE } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; import { IProcessServiceFactory } from '../../common/process/types'; @@ -14,7 +19,10 @@ import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { ICodeExecutionHelper } from '../types'; import { traceError } from '../../logging'; -import { Resource } from '../../common/types'; +import { IConfigurationService, IExperimentService, Resource } from '../../common/types'; +import { EnableREPLSmartSend } from '../../common/experiments/groups'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; @injectable() export class CodeExecutionHelper implements ICodeExecutionHelper { @@ -26,14 +34,22 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { private readonly interpreterService: IInterpreterService; + private readonly commandManager: ICommandManager; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error TS6133: 'configSettings' is declared but its value is never read. + private readonly configSettings: IConfigurationService; + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { this.documentManager = serviceContainer.get(IDocumentManager); this.applicationShell = serviceContainer.get(IApplicationShell); this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); this.interpreterService = serviceContainer.get(IInterpreterService); + this.configSettings = serviceContainer.get(IConfigurationService); + this.commandManager = serviceContainer.get(ICommandManager); } - public async normalizeLines(code: string, resource?: Uri): Promise { + public async normalizeLines(code: string, wholeFileContent?: string, resource?: Uri): Promise { try { if (code.trim().length === 0) { return ''; @@ -42,6 +58,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { // So just remove cr from the input. code = code.replace(new RegExp('\\r', 'g'), ''); + const activeEditor = this.documentManager.activeTextEditor; const interpreter = await this.interpreterService.getActiveInterpreter(resource); const processService = await this.processServiceFactory.create(resource); @@ -63,10 +80,24 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { normalizeOutput.resolve(normalized); }, }); - + // If there is no explicit selection, we are exeucting 'line' or 'block'. + if (activeEditor?.selection?.isEmpty) { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'line' }); + } // The normalization script expects a serialized JSON object, with the selection under the "code" key. // We're using a JSON object so that we don't have to worry about encoding, or escaping non-ASCII characters. - const input = JSON.stringify({ code }); + const startLineVal = activeEditor?.selection?.start.line ?? 0; + const endLineVal = activeEditor?.selection?.end.line ?? 0; + const emptyHighlightVal = activeEditor?.selection?.isEmpty ?? true; + const smartSendExperimentEnabledVal = pythonSmartSendEnabled(this.serviceContainer); + const input = JSON.stringify({ + code, + wholeFileContent, + startLine: startLineVal, + endLine: endLineVal, + emptyHighlight: emptyHighlightVal, + smartSendExperimentEnabled: smartSendExperimentEnabledVal, + }); observable.proc?.stdin?.write(input); observable.proc?.stdin?.end(); @@ -74,6 +105,11 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const result = await normalizeOutput.promise; const object = JSON.parse(result); + if (activeEditor?.selection) { + const lineOffset = object.nextBlockLineno - activeEditor!.selection.start.line - 1; + await this.moveToNextBlock(lineOffset, activeEditor); + } + return parse(object.normalized); } catch (ex) { traceError(ex, 'Python: Failed to normalize code for execution in terminal'); @@ -81,6 +117,30 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { } } + /** + * Depending on whether or not user is in experiment for smart send, + * dynamically move the cursor to the next block of code. + * The cursor movement is not moved by one everytime, + * since with the smart selection, the next executable code block + * can be multiple lines away. + * Intended to provide smooth shift+enter user experience + * bringing user's cursor to the next executable block of code when used with smart selection. + */ + // eslint-disable-next-line class-methods-use-this + private async moveToNextBlock(lineOffset: number, activeEditor?: TextEditor): Promise { + if (pythonSmartSendEnabled(this.serviceContainer)) { + if (activeEditor?.selection?.isEmpty) { + await this.commandManager.executeCommand('cursorMove', { + to: 'down', + by: 'line', + value: Number(lineOffset), + }); + await this.commandManager.executeCommand('cursorEnd'); + } + } + return Promise.resolve(); + } + public async getFileToExecute(): Promise { const activeEditor = this.documentManager.activeTextEditor; if (!activeEditor) { @@ -110,6 +170,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const { selection } = textEditor; let code: string; + if (selection.isEmpty) { code = textEditor.document.lineAt(selection.start.line).text; } else if (selection.isSingleLine) { @@ -117,6 +178,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { } else { code = getMultiLineSelectionText(textEditor); } + return code; } @@ -235,3 +297,9 @@ function getMultiLineSelectionText(textEditor: TextEditor): string { // ↑<---------------- To here return selectionText; } + +function pythonSmartSendEnabled(serviceContainer: IServiceContainer): boolean { + const experiment = serviceContainer.get(IExperimentService); + + return experiment ? experiment.inExperimentSync(EnableREPLSmartSend.experiment) : false; +} diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 47ac16d9e08b..48e39d4e1c81 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -15,7 +15,7 @@ export interface ICodeExecutionService { export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); export interface ICodeExecutionHelper { - normalizeLines(code: string): Promise; + normalizeLines(code: string, wholeFileContent?: string): Promise; getFileToExecute(): Promise; saveFileIfDirty(file: Uri): Promise; getSelectedTextToExecute(textEditor: TextEditor): Promise; diff --git a/src/test/pythonFiles/terminalExec/sample_smart_selection.py b/src/test/pythonFiles/terminalExec/sample_smart_selection.py new file mode 100644 index 000000000000..3933f06b5d65 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample_smart_selection.py @@ -0,0 +1,21 @@ +my_dict = { + "key1": "value1", + "key2": "value2" +} +#Sample + +print("Audi");print("BMW");print("Mercedes") + +# print("dont print me") + +def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + +# Skip me to prove that you did a good job +def next_func(): + print("You") + diff --git a/src/test/smoke/smartSend.smoke.test.ts b/src/test/smoke/smartSend.smoke.test.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/terminals/codeExecution/smartSend.test.ts b/src/test/terminals/codeExecution/smartSend.test.ts new file mode 100644 index 000000000000..8d70ab6e01e0 --- /dev/null +++ b/src/test/terminals/codeExecution/smartSend.test.ts @@ -0,0 +1,229 @@ +import * as TypeMoq from 'typemoq'; +import * as path from 'path'; +import { TextEditor, Selection, Position, TextDocument } from 'vscode'; +import * as fs from 'fs-extra'; +import { SemVer } from 'semver'; +import { assert, expect } from 'chai'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../../../client/common/application/types'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IConfigurationService, IExperimentService } from '../../../client/common/types'; +import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ICodeExecutionHelper } from '../../../client/terminals/types'; +import { EnableREPLSmartSend } from '../../../client/common/experiments/groups'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { PYTHON_PATH } from '../../common'; +import { Architecture } from '../../../client/common/utils/platform'; +import { ProcessService } from '../../../client/common/process/proc'; + +const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'terminalExec'); + +suite('REPL - Smart Send', () => { + let documentManager: TypeMoq.IMock; + let applicationShell: TypeMoq.IMock; + + let interpreterService: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; + + let processServiceFactory: TypeMoq.IMock; + let configurationService: TypeMoq.IMock; + + let serviceContainer: TypeMoq.IMock; + let codeExecutionHelper: ICodeExecutionHelper; + let experimentService: TypeMoq.IMock; + + let processService: TypeMoq.IMock; + + let document: TypeMoq.IMock; + const workingPython: PythonEnvironment = { + path: PYTHON_PATH, + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + displayName: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, + }; + + // suite set up only run once for each suite. Very start + // set up --- before each test + // tests -- actual tests + // tear down -- run after each test + // suite tear down only run once at the very end. + + // all object that is common to every test. What each test needs + setup(() => { + documentManager = TypeMoq.Mock.ofType(); + applicationShell = TypeMoq.Mock.ofType(); + processServiceFactory = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + configurationService = TypeMoq.Mock.ofType(); + serviceContainer = TypeMoq.Mock.ofType(); + experimentService = TypeMoq.Mock.ofType(); + processService = TypeMoq.Mock.ofType(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processService.setup((x: any) => x.then).returns(() => undefined); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) + .returns(() => documentManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => applicationShell.object); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory))) + .returns(() => processServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => commandManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IExperimentService))) + .returns(() => experimentService.object); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(workingPython)); + + codeExecutionHelper = new CodeExecutionHelper(serviceContainer.object); + document = TypeMoq.Mock.ofType(); + }); + + test('Cursor is not moved when explicit selection is present', async () => { + experimentService + .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment))) + .returns(() => true); + + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent); + + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.never()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + commandManager.verifyAll(); + }); + + test('Smart send should perform smart selection and move cursor', async () => { + experimentService + .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment))) + .returns(() => true); + + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => true); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualSmartOutput = await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent); + + // my_dict = { <----- smart shift+enter here + // "key1": "value1", + // "key2": "value2" + // } <---- cursor should be here afterwards, hence offset 3 + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.once()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const expectedSmartOutput = 'my_dict = {\n "key1": "value1",\n "key2": "value2"\n}\n'; + expect(actualSmartOutput).to.be.equal(expectedSmartOutput); + commandManager.verifyAll(); + }); + + // Do not perform smart selection when there is explicit selection + test('Smart send should not perform smart selection when there is explicit selection', async () => { + experimentService + .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment))) + .returns(() => true); + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualNonSmartResult = await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent); + const expectedNonSmartResult = 'my_dict = {\n\n'; // Standard for previous normalization logic + expect(actualNonSmartResult).to.be.equal(expectedNonSmartResult); + }); +}); diff --git a/src/testMultiRootWkspc/smokeTests/create_delete_file.py b/src/testMultiRootWkspc/smokeTests/create_delete_file.py new file mode 100644 index 000000000000..399bc4863c15 --- /dev/null +++ b/src/testMultiRootWkspc/smokeTests/create_delete_file.py @@ -0,0 +1,5 @@ +with open('smart_send_smoke.txt', 'w') as f: + f.write('This is for smart send smoke test') +import os + +os.remove('smart_send_smoke.txt') From bc0c7144d586d5a7514921ddfc8cd495f1838ba1 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 11 Oct 2023 10:18:16 -0700 Subject: [PATCH 323/336] add clickable show logs on discovery error (#22199) fixes https://github.com/microsoft/vscode-python/issues/22175 --- .../testController/common/resultResolver.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 79cee6452a8c..cf757d77243d 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -1,7 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode'; +import { + CancellationToken, + TestController, + TestItem, + Uri, + TestMessage, + Location, + TestRun, + MarkdownString, +} from 'vscode'; import * as util from 'util'; import { DiscoveredTestPayload, EOTTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; import { TestProvider } from '../../types'; @@ -78,7 +87,11 @@ export class PythonResultResolver implements ITestResultResolver { errorNode = createErrorTestItem(this.testController, options); this.testController.items.add(errorNode); } - errorNode.error = message; + const errorNodeLabel: MarkdownString = new MarkdownString( + `[Show output](command:python.viewOutput) to view error logs`, + ); + errorNodeLabel.isTrusted = true; + errorNode.error = errorNodeLabel; } else { // remove error node only if no errors exist. this.testController.items.delete(`DiscoveryError:${workspacePath}`); From 75e707be42bf67aac316c900f7d095c1e21bae28 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Wed, 11 Oct 2023 11:11:57 -0700 Subject: [PATCH 324/336] Update LSP to latest version to support completion itemDefaults (#22200) Dirk added this feature here: https://github.com/microsoft/vscode-languageserver-node/commit/0b7acc15abd7132c9154d94140f478ccf5ba5769 We want to use this in Pylance in order to speedup completions. For the degenerate case, this can speedup completion results by 30%. See https://github.com/microsoft/pyrx/issues/4113 and https://github.com/microsoft/pylance-release/issues/4919 --- package-lock.json | 167 ++++++++++++++++++++++++++++++---------------- package.json | 8 +-- 2 files changed, 113 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5d1ee32bc08b..c4e177468706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,10 +37,10 @@ "untildify": "^4.0.0", "vscode-debugadapter": "^1.28.0", "vscode-debugprotocol": "^1.28.0", - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageclient": "^8.1.0", - "vscode-languageserver": "^8.1.0", - "vscode-languageserver-protocol": "^3.17.3", + "vscode-jsonrpc": "^8.2.0", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", @@ -1843,6 +1843,41 @@ "vscode": "^1.67.0-insider" } }, + "node_modules/@vscode/jupyter-lsp-middleware/node_modules/vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vscode/jupyter-lsp-middleware/node_modules/vscode-languageclient": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", + "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.3" + }, + "engines": { + "vscode": "^1.67.0" + } + }, + "node_modules/@vscode/jupyter-lsp-middleware/node_modules/vscode-languageserver-protocol": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "dependencies": { + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" + } + }, + "node_modules/@vscode/jupyter-lsp-middleware/node_modules/vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + }, "node_modules/@vscode/lsp-notebook-concat": { "version": "0.1.16", "resolved": "https://registry.npmjs.org/@vscode/lsp-notebook-concat/-/lsp-notebook-concat-0.1.16.tgz", @@ -14554,58 +14589,50 @@ "deprecated": "This package has been renamed to @vscode/debugprotocol, please update to the new name" }, "node_modules/vscode-jsonrpc": { - "version": "8.0.2-next.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2-next.1.tgz", - "integrity": "sha512-sbbvGSWja7NVBLHPGawtgezc8DHYJaP4qfr/AaJiyDapWcSFtHyPtm18+LnYMLTmB7bhOUW/lf5PeeuLpP6bKA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", - "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", "dependencies": { "minimatch": "^5.1.0", "semver": "^7.3.7", - "vscode-languageserver-protocol": "3.17.3" + "vscode-languageserver-protocol": "3.17.5" }, "engines": { - "vscode": "^1.67.0" + "vscode": "^1.82.0" } }, "node_modules/vscode-languageserver": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", - "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", "dependencies": { - "vscode-languageserver-protocol": "3.17.3" + "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", - "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "dependencies": { - "vscode-jsonrpc": "8.1.0", - "vscode-languageserver-types": "3.17.3" - } - }, - "node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", - "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", - "engines": { - "node": ">=14.0.0" + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" } }, "node_modules/vscode-languageserver-types": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", - "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "node_modules/vscode-tas-client": { "version": "0.1.63", @@ -16750,6 +16777,37 @@ "vscode-languageclient": "^8.0.2-next.4", "vscode-languageserver-protocol": "^3.17.2-next.5", "vscode-uri": "^3.0.2" + }, + "dependencies": { + "vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==" + }, + "vscode-languageclient": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", + "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", + "requires": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.3" + } + }, + "vscode-languageserver-protocol": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "requires": { + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" + } + }, + "vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + } } }, "@vscode/lsp-notebook-concat": { @@ -26716,48 +26774,41 @@ "integrity": "sha512-+OMm11R1bGYbpIJ5eQIkwoDGFF4GvBz3Ztl6/VM+/RNNb2Gjk2c0Ku+oMmfhlTmTlPCpgHBsH4JqVCbUYhu5bA==" }, "vscode-jsonrpc": { - "version": "8.0.2-next.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2-next.1.tgz", - "integrity": "sha512-sbbvGSWja7NVBLHPGawtgezc8DHYJaP4qfr/AaJiyDapWcSFtHyPtm18+LnYMLTmB7bhOUW/lf5PeeuLpP6bKA==" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==" }, "vscode-languageclient": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", - "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", "requires": { "minimatch": "^5.1.0", "semver": "^7.3.7", - "vscode-languageserver-protocol": "3.17.3" + "vscode-languageserver-protocol": "3.17.5" } }, "vscode-languageserver": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", - "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", "requires": { - "vscode-languageserver-protocol": "3.17.3" + "vscode-languageserver-protocol": "3.17.5" } }, "vscode-languageserver-protocol": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", - "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "requires": { - "vscode-jsonrpc": "8.1.0", - "vscode-languageserver-types": "3.17.3" - }, - "dependencies": { - "vscode-jsonrpc": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", - "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==" - } + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" } }, "vscode-languageserver-types": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", - "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "vscode-tas-client": { "version": "0.1.63", diff --git a/package.json b/package.json index 0f93441c6113..490e615f26f2 100644 --- a/package.json +++ b/package.json @@ -2081,10 +2081,10 @@ "untildify": "^4.0.0", "vscode-debugadapter": "^1.28.0", "vscode-debugprotocol": "^1.28.0", - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageclient": "^8.1.0", - "vscode-languageserver": "^8.1.0", - "vscode-languageserver-protocol": "^3.17.3", + "vscode-jsonrpc": "^8.2.0", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", From 055a352285db83158be4374a2e57bdc48b28fda8 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 11 Oct 2023 12:05:39 -0700 Subject: [PATCH 325/336] Remove formatting support (#22196) --- .../common/installer/moduleInstaller.ts | 6 - src/client/common/installer/productNames.ts | 3 - src/client/common/installer/productPath.ts | 14 - src/client/common/installer/productService.ts | 3 - .../common/installer/serviceRegistry.ts | 6 - src/client/common/types.ts | 5 - src/client/extensionActivation.ts | 23 +- src/client/formatters/autoPep8Formatter.ts | 43 --- src/client/formatters/baseFormatter.ts | 149 -------- src/client/formatters/blackFormatter.ts | 55 --- src/client/formatters/dummyFormatter.ts | 19 - src/client/formatters/helper.ts | 53 --- src/client/formatters/serviceRegistry.ts | 10 - src/client/formatters/types.ts | 20 -- src/client/formatters/yapfFormatter.ts | 38 -- src/client/logging/settingLogs.ts | 42 +++ src/client/providers/formatProvider.ts | 135 ------- .../prompts/installFormatterPrompt.ts | 145 -------- src/client/providers/prompts/promptUtils.ts | 38 -- src/client/providers/prompts/types.ts | 12 - src/client/providers/serviceRegistry.ts | 3 - src/test/common/installer.test.ts | 12 +- .../installer/channelManager.unit.test.ts | 6 +- .../common/installer/productPath.unit.test.ts | 45 +-- .../installer/serviceRegistry.unit.test.ts | 8 - src/test/common/moduleInstaller.test.ts | 1 - src/test/format/extension.format.test.ts | 205 ----------- src/test/format/format.helper.test.ts | 117 ------ src/test/format/formatter.unit.test.ts | 171 --------- src/test/linters/lint.multiroot.test.ts | 11 +- src/test/linters/lint.test.ts | 11 +- .../installFormatterPrompt.unit.test.ts | 335 ------------------ src/test/serviceRegistry.ts | 5 - 33 files changed, 53 insertions(+), 1696 deletions(-) delete mode 100644 src/client/formatters/autoPep8Formatter.ts delete mode 100644 src/client/formatters/baseFormatter.ts delete mode 100644 src/client/formatters/blackFormatter.ts delete mode 100644 src/client/formatters/dummyFormatter.ts delete mode 100644 src/client/formatters/helper.ts delete mode 100644 src/client/formatters/serviceRegistry.ts delete mode 100644 src/client/formatters/types.ts delete mode 100644 src/client/formatters/yapfFormatter.ts create mode 100644 src/client/logging/settingLogs.ts delete mode 100644 src/client/providers/formatProvider.ts delete mode 100644 src/client/providers/prompts/installFormatterPrompt.ts delete mode 100644 src/client/providers/prompts/promptUtils.ts delete mode 100644 src/client/providers/prompts/types.ts delete mode 100644 src/test/format/extension.format.test.ts delete mode 100644 src/test/format/format.helper.test.ts delete mode 100644 src/test/format/formatter.unit.test.ts delete mode 100644 src/test/providers/prompt/installFormatterPrompt.unit.test.ts diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index f70dd937aba9..5a4f245900ea 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -248,16 +248,10 @@ export function translateProductToModule(product: Product): string { return 'pylint'; case Product.pytest: return 'pytest'; - case Product.autopep8: - return 'autopep8'; - case Product.black: - return 'black'; case Product.pycodestyle: return 'pycodestyle'; case Product.pydocstyle: return 'pydocstyle'; - case Product.yapf: - return 'yapf'; case Product.flake8: return 'flake8'; case Product.unittest: diff --git a/src/client/common/installer/productNames.ts b/src/client/common/installer/productNames.ts index 378fd5a38dba..9b917d2f1d76 100644 --- a/src/client/common/installer/productNames.ts +++ b/src/client/common/installer/productNames.ts @@ -4,9 +4,7 @@ import { Product } from '../types'; export const ProductNames = new Map(); -ProductNames.set(Product.autopep8, 'autopep8'); ProductNames.set(Product.bandit, 'bandit'); -ProductNames.set(Product.black, 'black'); ProductNames.set(Product.flake8, 'flake8'); ProductNames.set(Product.mypy, 'mypy'); ProductNames.set(Product.pycodestyle, 'pycodestyle'); @@ -15,7 +13,6 @@ ProductNames.set(Product.prospector, 'prospector'); ProductNames.set(Product.pydocstyle, 'pydocstyle'); ProductNames.set(Product.pylint, 'pylint'); ProductNames.set(Product.pytest, 'pytest'); -ProductNames.set(Product.yapf, 'yapf'); ProductNames.set(Product.tensorboard, 'tensorboard'); ProductNames.set(Product.torchProfilerInstallName, 'torch-tb-profiler'); ProductNames.set(Product.torchProfilerImportName, 'torch_tb_profiler'); diff --git a/src/client/common/installer/productPath.ts b/src/client/common/installer/productPath.ts index 5c36a6bbd3bd..3b3f1d7c1794 100644 --- a/src/client/common/installer/productPath.ts +++ b/src/client/common/installer/productPath.ts @@ -6,7 +6,6 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; -import { IFormatterHelper } from '../../formatters/types'; import { IServiceContainer } from '../../ioc/types'; import { ILinterManager } from '../../linters/types'; import { ITestingService } from '../../testing/types'; @@ -37,19 +36,6 @@ export abstract class BaseProductPathsService implements IProductPathService { } } -@injectable() -export class FormatterProductPathService extends BaseProductPathsService { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer); - } - public getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const settings = this.configService.getSettings(resource); - const formatHelper = this.serviceContainer.get(IFormatterHelper); - const settingsPropNames = formatHelper.getSettingsPropertyNames(product); - return settings.formatting[settingsPropNames.pathName] as string; - } -} - @injectable() export class LinterProductPathService extends BaseProductPathsService { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts index 26a01e37c3ba..af2192755fe8 100644 --- a/src/client/common/installer/productService.ts +++ b/src/client/common/installer/productService.ts @@ -22,9 +22,6 @@ export class ProductService implements IProductService { this.ProductTypes.set(Product.pylint, ProductType.Linter); this.ProductTypes.set(Product.pytest, ProductType.TestFramework); this.ProductTypes.set(Product.unittest, ProductType.TestFramework); - this.ProductTypes.set(Product.autopep8, ProductType.Formatter); - this.ProductTypes.set(Product.black, ProductType.Formatter); - this.ProductTypes.set(Product.yapf, ProductType.Formatter); this.ProductTypes.set(Product.tensorboard, ProductType.DataScience); this.ProductTypes.set(Product.torchProfilerInstallName, ProductType.DataScience); this.ProductTypes.set(Product.torchProfilerImportName, ProductType.DataScience); diff --git a/src/client/common/installer/serviceRegistry.ts b/src/client/common/installer/serviceRegistry.ts index c262c7571711..c4e7c1a089c6 100644 --- a/src/client/common/installer/serviceRegistry.ts +++ b/src/client/common/installer/serviceRegistry.ts @@ -11,7 +11,6 @@ import { PipInstaller } from './pipInstaller'; import { PoetryInstaller } from './poetryInstaller'; import { DataScienceProductPathService, - FormatterProductPathService, LinterProductPathService, TestFrameworkProductPathService, } from './productPath'; @@ -25,11 +24,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IModuleInstaller, PoetryInstaller); serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); serviceManager.addSingleton(IProductService, ProductService); - serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); serviceManager.addSingleton( IProductPathService, diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 07f1fea6b86b..8b90443703c6 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -87,7 +87,6 @@ export enum ProductInstallStatus { export enum ProductType { Linter = 'Linter', - Formatter = 'Formatter', TestFramework = 'TestFramework', RefactoringLibrary = 'RefactoringLibrary', DataScience = 'DataScience', @@ -102,11 +101,8 @@ export enum Product { pylama = 6, prospector = 7, pydocstyle = 8, - yapf = 9, - autopep8 = 10, mypy = 11, unittest = 12, - black = 16, bandit = 17, tensorboard = 24, torchProfilerInstallName = 25, @@ -185,7 +181,6 @@ export interface IPythonSettings { readonly poetryPath: string; readonly devOptions: string[]; readonly linting: ILintingSettings; - readonly formatting: IFormattingSettings; readonly testing: ITestingSettings; readonly autoComplete: IAutoCompleteSettings; readonly terminal: ITerminalSettings; diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 807698f3ec29..0d3b04d9bb8c 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -10,7 +10,7 @@ import { IExtensionActivationManager } from './activation/types'; import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; import { IApplicationDiagnostics } from './application/types'; import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from './common/application/types'; -import { Commands, PYTHON, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; +import { Commands, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; import { IFileSystem } from './common/platform/types'; import { @@ -25,11 +25,9 @@ import { noop } from './common/utils/misc'; import { DebuggerTypeName } from './debugger/constants'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; import { IDebugConfigurationService, IDynamicDebugConfigurationService } from './debugger/extension/types'; -import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; -import { PythonFormattingEditProvider } from './providers/formatProvider'; import { ReplProvider } from './providers/replProvider'; import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; import { TerminalProvider } from './providers/terminalProvider'; @@ -51,10 +49,10 @@ import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { WorkspaceService } from './common/application/workspace'; import { DynamicPythonDebugConfigurationService } from './debugger/extension/configuration/dynamicdebugConfigurationService'; import { IInterpreterQuickPick } from './interpreter/configuration/types'; -import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; import { initializePersistentStateForTriggers } from './common/persistentState'; +import { logAndNotifyOnFormatterSetting } from './logging/settingLogs'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -110,7 +108,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): // See https://github.com/microsoft/vscode-python/issues/10454. async function activateLegacy(ext: ExtensionState): Promise { - const { context, legacyIOC } = ext; + const { legacyIOC } = ext; const { serviceManager, serviceContainer } = legacyIOC; // register "services" @@ -125,7 +123,6 @@ async function activateLegacy(ext: ExtensionState): Promise { // Feature specific registrations. unitTestsRegisterTypes(serviceManager); lintersRegisterTypes(serviceManager); - formattersRegisterTypes(serviceManager); installerRegisterTypes(serviceManager); commonRegisterTerminalTypes(serviceManager); debugConfigurationRegisterTypes(serviceManager); @@ -134,7 +131,6 @@ async function activateLegacy(ext: ExtensionState): Promise { const extensions = serviceContainer.get(IExtensions); await setDefaultLanguageServer(extensions, serviceManager); - const configuration = serviceManager.get(IConfigurationService); // Settings are dependent on Experiment service, so we need to initialize it after experiments are activated. serviceContainer.get(IConfigurationService).getSettings().register(); @@ -165,20 +161,9 @@ async function activateLegacy(ext: ExtensionState): Promise { serviceContainer.get(IApplicationDiagnostics).register(); serviceManager.get(ITerminalAutoActivation).register(); - const pythonSettings = configuration.getSettings(); serviceManager.get(ICodeExecutionManager).registerCommands(); - if ( - pythonSettings && - pythonSettings.formatting && - pythonSettings.formatting.provider !== 'internalConsole' - ) { - const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); - disposables.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); - disposables.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); - } - disposables.push(new ReplProvider(serviceContainer)); const terminalProvider = new TerminalProvider(serviceContainer); @@ -200,7 +185,7 @@ async function activateLegacy(ext: ExtensionState): Promise { ), ); - registerInstallFormatterPrompt(serviceContainer); + logAndNotifyOnFormatterSetting(); registerCreateEnvironmentTriggers(disposables); initializePersistentStateForTriggers(ext.context); } diff --git a/src/client/formatters/autoPep8Formatter.ts b/src/client/formatters/autoPep8Formatter.ts deleted file mode 100644 index bf1285a60b58..000000000000 --- a/src/client/formatters/autoPep8Formatter.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as vscode from 'vscode'; -import { Product } from '../common/installer/productInstaller'; -import { IConfigurationService } from '../common/types'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class AutoPep8Formatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('autopep8', Product.autopep8, serviceContainer); - } - - public formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = - Array.isArray(settings.formatting.autopep8Args) && settings.formatting.autopep8Args.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - const autoPep8Args = ['--diff']; - if (formatSelection) { - autoPep8Args.push( - ...['--line-range', (range!.start.line + 1).toString(), (range!.end.line + 1).toString()], - ); - } - const promise = super.provideDocumentFormattingEdits(document, options, token, autoPep8Args); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { - tool: 'autopep8', - hasCustomArgs, - formatSelection, - }); - return promise; - } -} diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts deleted file mode 100644 index 64e7d15a3d45..000000000000 --- a/src/client/formatters/baseFormatter.ts +++ /dev/null @@ -1,149 +0,0 @@ -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../common/application/types'; -import '../common/extensions'; -import { isNotInstalledError } from '../common/helpers'; -import { IFileSystem } from '../common/platform/types'; -import { IPythonToolExecutionService } from '../common/process/types'; -import { IDisposableRegistry, IInstaller, Product } from '../common/types'; -import { isNotebookCell } from '../common/utils/misc'; -import { IServiceContainer } from '../ioc/types'; -import { traceError } from '../logging'; -import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor'; -import { IFormatterHelper } from './types'; -import { IInstallFormatterPrompt } from '../providers/prompts/types'; - -export abstract class BaseFormatter { - protected readonly workspace: IWorkspaceService; - private readonly helper: IFormatterHelper; - private errorShown: boolean = false; - - constructor(public Id: string, private product: Product, protected serviceContainer: IServiceContainer) { - this.helper = serviceContainer.get(IFormatterHelper); - this.workspace = serviceContainer.get(IWorkspaceService); - } - - public abstract formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable; - protected getDocumentPath(document: vscode.TextDocument, fallbackPath: string) { - if (path.basename(document.uri.fsPath) === document.uri.fsPath) { - return fallbackPath; - } - return path.dirname(document.fileName); - } - protected getWorkspaceUri(document: vscode.TextDocument) { - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - if (workspaceFolder) { - return workspaceFolder.uri; - } - const folders = this.workspace.workspaceFolders; - if (Array.isArray(folders) && folders.length > 0) { - return folders[0].uri; - } - return vscode.Uri.file(__dirname); - } - protected async provideDocumentFormattingEdits( - document: vscode.TextDocument, - _options: vscode.FormattingOptions, - token: vscode.CancellationToken, - args: string[], - cwd?: string, - ): Promise { - if (typeof cwd !== 'string' || cwd.length === 0) { - cwd = this.getWorkspaceUri(document).fsPath; - } - - // autopep8 and yapf have the ability to read from the process input stream and return the formatted code out of the output stream. - // However they don't support returning the diff of the formatted text when reading data from the input stream. - // Yet getting text formatted that way avoids having to create a temporary file, however the diffing will have - // to be done here in node (extension), i.e. extension CPU, i.e. less responsive solution. - // Also, always create temp files for Notebook cells. - const tempFile = await this.createTempFile(document); - if (this.checkCancellation(document.fileName, tempFile, token)) { - return []; - } - - const executionInfo = this.helper.getExecutionInfo(this.product, args, document.uri); - executionInfo.args.push(tempFile); - const pythonToolsExecutionService = this.serviceContainer.get( - IPythonToolExecutionService, - ); - const promise = pythonToolsExecutionService - .exec(executionInfo, { cwd, throwOnStdErr: false, token }, document.uri) - .then((output) => output.stdout) - .then((data) => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - return getTextEditsFromPatch(document.getText(), data); - }) - .catch((error) => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - - this.handleError(this.Id, error, document.uri).catch(() => {}); - return [] as vscode.TextEdit[]; - }) - .then((edits) => { - this.deleteTempFile(document.fileName, tempFile).ignoreErrors(); - return edits; - }); - - const appShell = this.serviceContainer.get(IApplicationShell); - const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); - const disposable = appShell.setStatusBarMessage(`Formatting with ${this.Id}`, promise); - disposableRegistry.push(disposable); - return promise; - } - - protected async handleError(_expectedFileName: string, error: Error, resource?: vscode.Uri) { - if (isNotInstalledError(error)) { - const prompt = this.serviceContainer.get(IInstallFormatterPrompt); - if (!(await prompt.showInstallFormatterPrompt(resource))) { - const installer = this.serviceContainer.get(IInstaller); - const isInstalled = await installer.isInstalled(this.product, resource); - if (!isInstalled && !this.errorShown) { - traceError( - `\nPlease install '${this.Id}' into your environment.`, - "\nIf you don't want to use it you can turn it off or use another formatter in the settings.", - ); - this.errorShown = true; - } - } - } - - traceError(`Formatting with ${this.Id} failed:\n${error}`); - } - - /** - * Always create a temporary file when formatting notebook cells. - * This is because there is no physical file associated with notebook cells (they are all virtual). - */ - private async createTempFile(document: vscode.TextDocument): Promise { - const fs = this.serviceContainer.get(IFileSystem); - return document.isDirty || isNotebookCell(document) - ? getTempFileWithDocumentContents(document, fs) - : document.fileName; - } - - private deleteTempFile(originalFile: string, tempFile: string): Promise { - if (originalFile !== tempFile) { - const fs = this.serviceContainer.get(IFileSystem); - return fs.deleteFile(tempFile); - } - return Promise.resolve(); - } - - private checkCancellation(originalFile: string, tempFile: string, token?: vscode.CancellationToken): boolean { - if (token && token.isCancellationRequested) { - this.deleteTempFile(originalFile, tempFile).ignoreErrors(); - return true; - } - return false; - } -} diff --git a/src/client/formatters/blackFormatter.ts b/src/client/formatters/blackFormatter.ts deleted file mode 100644 index 0a8109e163e0..000000000000 --- a/src/client/formatters/blackFormatter.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IApplicationShell } from '../common/application/types'; -import { Product } from '../common/installer/productInstaller'; -import { IConfigurationService } from '../common/types'; -import { noop } from '../common/utils/misc'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class BlackFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('black', Product.black, serviceContainer); - } - - public async formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Promise { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.blackArgs) && settings.formatting.blackArgs.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - if (formatSelection) { - const shell = this.serviceContainer.get(IApplicationShell); - // Black does not support partial formatting on purpose. - shell - .showErrorMessage(vscode.l10n.t('Black does not support the "Format Selection" command')) - .then(noop, noop); - return []; - } - - const blackArgs = ['--diff', '--quiet']; - - if (path.extname(document.fileName) === '.pyi') { - blackArgs.push('--pyi'); - } - - const promise = super.provideDocumentFormattingEdits(document, options, token, blackArgs); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'black', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/src/client/formatters/dummyFormatter.ts b/src/client/formatters/dummyFormatter.ts deleted file mode 100644 index b4fdba9fbc0f..000000000000 --- a/src/client/formatters/dummyFormatter.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as vscode from 'vscode'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseFormatter } from './baseFormatter'; - -export class DummyFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('none', Product.yapf, serviceContainer); - } - - public formatDocument( - _document: vscode.TextDocument, - _options: vscode.FormattingOptions, - _token: vscode.CancellationToken, - _range?: vscode.Range, - ): Thenable { - return Promise.resolve([]); - } -} diff --git a/src/client/formatters/helper.ts b/src/client/formatters/helper.ts deleted file mode 100644 index ac305b51e785..000000000000 --- a/src/client/formatters/helper.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { ExecutionInfo, IConfigurationService, IFormattingSettings, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { FormatterId, FormatterSettingsPropertyNames, IFormatterHelper } from './types'; - -@injectable() -export class FormatterHelper implements IFormatterHelper { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} - public translateToId(formatter: Product): FormatterId { - switch (formatter) { - case Product.autopep8: - return 'autopep8'; - case Product.black: - return 'black'; - case Product.yapf: - return 'yapf'; - default: { - throw new Error(`Unrecognized Formatter '${formatter}'`); - } - } - } - public getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames { - const id = this.translateToId(formatter); - return { - argsName: `${id}Args` as keyof IFormattingSettings, - pathName: `${id}Path` as keyof IFormattingSettings, - }; - } - public getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo { - const settings = this.serviceContainer.get(IConfigurationService).getSettings(resource); - const names = this.getSettingsPropertyNames(formatter); - - const execPath = settings.formatting[names.pathName] as string; - let args: string[] = Array.isArray(settings.formatting[names.argsName]) - ? (settings.formatting[names.argsName] as string[]) - : []; - args = args.concat(customArgs); - - let moduleName: string | undefined; - - // If path information is not available, then treat it as a module, - if (path.basename(execPath) === execPath) { - moduleName = execPath; - } - - return { execPath, moduleName, args, product: formatter }; - } -} diff --git a/src/client/formatters/serviceRegistry.ts b/src/client/formatters/serviceRegistry.ts deleted file mode 100644 index 196e6c806b5f..000000000000 --- a/src/client/formatters/serviceRegistry.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IServiceManager } from '../ioc/types'; -import { FormatterHelper } from './helper'; -import { IFormatterHelper } from './types'; - -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IFormatterHelper, FormatterHelper); -} diff --git a/src/client/formatters/types.ts b/src/client/formatters/types.ts deleted file mode 100644 index 7f4bcf5b7524..000000000000 --- a/src/client/formatters/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { ExecutionInfo, IFormattingSettings, Product } from '../common/types'; - -export const IFormatterHelper = Symbol('IFormatterHelper'); - -export type FormatterId = 'autopep8' | 'black' | 'yapf'; - -export type FormatterSettingsPropertyNames = { - argsName: keyof IFormattingSettings; - pathName: keyof IFormattingSettings; -}; - -export interface IFormatterHelper { - translateToId(formatter: Product): FormatterId; - getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames; - getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo; -} diff --git a/src/client/formatters/yapfFormatter.ts b/src/client/formatters/yapfFormatter.ts deleted file mode 100644 index 08729a97694f..000000000000 --- a/src/client/formatters/yapfFormatter.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as vscode from 'vscode'; -import { IConfigurationService, Product } from '../common/types'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class YapfFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('yapf', Product.yapf, serviceContainer); - } - - public formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.yapfArgs) && settings.formatting.yapfArgs.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - const yapfArgs = ['--diff']; - if (formatSelection && range !== undefined) { - yapfArgs.push(...['--lines', `${range.start.line + 1}-${range.end.line + 1}`]); - } - // Yapf starts looking for config file starting from the file path. - const fallbarFolder = this.getWorkspaceUri(document).fsPath; - const cwd = this.getDocumentPath(document, fallbarFolder); - const promise = super.provideDocumentFormattingEdits(document, options, token, yapfArgs, cwd); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'yapf', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/src/client/logging/settingLogs.ts b/src/client/logging/settingLogs.ts new file mode 100644 index 000000000000..721ab80d4500 --- /dev/null +++ b/src/client/logging/settingLogs.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { l10n } from 'vscode'; +import { traceError, traceInfo } from '.'; +import { Commands, PVSC_EXTENSION_ID } from '../common/constants'; +import { showErrorMessage } from '../common/vscodeApis/windowApis'; +import { getConfiguration, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; +import { Common } from '../common/utils/localize'; +import { executeCommand } from '../common/vscodeApis/commandApis'; + +export function logAndNotifyOnFormatterSetting(): void { + getWorkspaceFolders()?.forEach(async (workspace) => { + let config = getConfiguration('editor', { uri: workspace.uri, languageId: 'python' }); + if (!config) { + config = getConfiguration('editor', workspace.uri); + if (!config) { + traceError('Unable to get editor configuration'); + } + } + const formatter = config.get('defaultFormatter', ''); + traceInfo(`Default formatter is set to ${formatter} for workspace ${workspace.uri.fsPath}`); + if (formatter === PVSC_EXTENSION_ID) { + traceError('Formatting features have been moved to separate formatter extensions.'); + traceError('Please install the formatter extension you prefer and set it as the default formatter.'); + traceError('For `autopep8` use: https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8'); + traceError( + 'For `black` use: https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter', + ); + traceError('For `yapf` use: https://marketplace.visualstudio.com/items?itemName=eeyore.yapf'); + const response = await showErrorMessage( + l10n.t( + 'Formatting features have been moved to separate formatter extensions. Please install the formatter extension you prefer and set it as the default formatter.', + ), + Common.showLogs, + ); + if (response === Common.showLogs) { + executeCommand(Commands.ViewOutput); + } + } + }); +} diff --git a/src/client/providers/formatProvider.ts b/src/client/providers/formatProvider.ts deleted file mode 100644 index 1ea239c03bec..000000000000 --- a/src/client/providers/formatProvider.ts +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { PYTHON_LANGUAGE } from '../common/constants'; -import { IConfigurationService } from '../common/types'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { AutoPep8Formatter } from '../formatters/autoPep8Formatter'; -import { BaseFormatter } from '../formatters/baseFormatter'; -import { BlackFormatter } from '../formatters/blackFormatter'; -import { DummyFormatter } from '../formatters/dummyFormatter'; -import { YapfFormatter } from '../formatters/yapfFormatter'; - -export class PythonFormattingEditProvider - implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider, vscode.Disposable { - private readonly config: IConfigurationService; - - private readonly workspace: IWorkspaceService; - - private readonly documentManager: IDocumentManager; - - private readonly commands: ICommandManager; - - private formatters = new Map(); - - private disposables: vscode.Disposable[] = []; - - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - private documentVersionBeforeFormatting = -1; - - private formatterMadeChanges = false; - - private saving = false; - - public constructor(_context: vscode.ExtensionContext, serviceContainer: IServiceContainer) { - const yapfFormatter = new YapfFormatter(serviceContainer); - const autoPep8 = new AutoPep8Formatter(serviceContainer); - const black = new BlackFormatter(serviceContainer); - const dummy = new DummyFormatter(serviceContainer); - this.formatters.set(yapfFormatter.Id, yapfFormatter); - this.formatters.set(black.Id, black); - this.formatters.set(autoPep8.Id, autoPep8); - this.formatters.set(dummy.Id, dummy); - - this.commands = serviceContainer.get(ICommandManager); - this.workspace = serviceContainer.get(IWorkspaceService); - this.documentManager = serviceContainer.get(IDocumentManager); - this.config = serviceContainer.get(IConfigurationService); - const interpreterService = serviceContainer.get(IInterpreterService); - this.disposables.push( - this.documentManager.onDidSaveTextDocument(async (document) => this.onSaveDocument(document)), - ); - this.disposables.push( - interpreterService.onDidChangeInterpreter(async () => { - if (this.documentManager.activeTextEditor) { - return this.onSaveDocument(this.documentManager.activeTextEditor.document); - } - - return undefined; - }), - ); - } - - public dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } - - public provideDocumentFormattingEdits( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - ): Promise { - return this.provideDocumentRangeFormattingEdits(document, undefined, options, token); - } - - public async provideDocumentRangeFormattingEdits( - document: vscode.TextDocument, - range: vscode.Range | undefined, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - ): Promise { - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - // VSC rejects 'format on save' promise in 750 ms. Python formatting may take quite a bit longer. - // Workaround is to resolve promise to nothing here, then execute format document and force new save. - // However, we need to know if this is 'format document' or formatting on save. - - if (this.saving || document.languageId !== PYTHON_LANGUAGE) { - // We are saving after formatting (see onSaveDocument below) - // so we do not want to format again. - return []; - } - - // Remember content before formatting so we can detect if - // formatting edits have been really applied - const editorConfig = this.workspace.getConfiguration('editor', document.uri); - if (editorConfig.get('formatOnSave') === true) { - this.documentVersionBeforeFormatting = document.version; - } - - const settings = this.config.getSettings(document.uri); - const formatter = this.formatters.get(settings.formatting.provider)!; - const edits = await formatter.formatDocument(document, options, token, range); - - this.formatterMadeChanges = edits.length > 0; - return edits; - } - - private async onSaveDocument(document: vscode.TextDocument): Promise { - // Promise was rejected = formatting took too long. - // Don't format inside the event handler, do it on timeout - setTimeout(() => { - try { - if ( - this.formatterMadeChanges && - !document.isDirty && - document.version === this.documentVersionBeforeFormatting - ) { - // Formatter changes were not actually applied due to the timeout on save. - // Force formatting now and then save the document. - this.commands.executeCommand('editor.action.formatDocument').then(async () => { - this.saving = true; - await document.save(); - this.saving = false; - }); - } - } finally { - this.documentVersionBeforeFormatting = -1; - this.saving = false; - this.formatterMadeChanges = false; - } - }, 50); - } -} diff --git a/src/client/providers/prompts/installFormatterPrompt.ts b/src/client/providers/prompts/installFormatterPrompt.ts deleted file mode 100644 index 5743f8402053..000000000000 --- a/src/client/providers/prompts/installFormatterPrompt.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { inject, injectable } from 'inversify'; -import { IDisposableRegistry } from '../../common/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { getConfiguration, onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; -import { IServiceContainer } from '../../ioc/types'; -import { - doNotShowPromptState, - inFormatterExtensionExperiment, - installFormatterExtension, - updateDefaultFormatter, -} from './promptUtils'; -import { AUTOPEP8_EXTENSION, BLACK_EXTENSION, IInstallFormatterPrompt } from './types'; - -const SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY = 'showFormatterExtensionInstallPrompt'; - -@injectable() -export class InstallFormatterPrompt implements IInstallFormatterPrompt { - private currentlyShown = false; - - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} - - /* - * This method is called when the user saves a python file or a cell. - * Returns true if an extension was selected. Otherwise returns false. - */ - public async showInstallFormatterPrompt(resource?: Uri): Promise { - if (!inFormatterExtensionExperiment(this.serviceContainer)) { - return false; - } - - const promptState = doNotShowPromptState(SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY, this.serviceContainer); - if (this.currentlyShown || promptState.value) { - return false; - } - - const config = getConfiguration('python', resource); - const formatter = config.get('formatting.provider', 'none'); - if (!['autopep8', 'black'].includes(formatter)) { - return false; - } - - const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' }); - const defaultFormatter = editorConfig.get('defaultFormatter', ''); - if ([BLACK_EXTENSION, AUTOPEP8_EXTENSION].includes(defaultFormatter)) { - return false; - } - - const black = isExtensionEnabled(BLACK_EXTENSION); - const autopep8 = isExtensionEnabled(AUTOPEP8_EXTENSION); - - let selection: string | undefined; - - if (black || autopep8) { - this.currentlyShown = true; - if (black && autopep8) { - selection = await showInformationMessage( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } else if (black) { - selection = await showInformationMessage( - ToolsExtensions.selectBlackFormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ); - if (selection === Common.bannerLabelYes) { - selection = 'Black'; - } - } else if (autopep8) { - selection = await showInformationMessage( - ToolsExtensions.selectAutopep8FormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ); - if (selection === Common.bannerLabelYes) { - selection = 'Autopep8'; - } - } - } else if (formatter === 'black' && !black) { - this.currentlyShown = true; - selection = await showInformationMessage( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } else if (formatter === 'autopep8' && !autopep8) { - this.currentlyShown = true; - selection = await showInformationMessage( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } - - let userSelectedAnExtension = false; - if (selection === 'Black') { - if (black) { - userSelectedAnExtension = true; - await updateDefaultFormatter(BLACK_EXTENSION, resource); - } else { - userSelectedAnExtension = true; - await installFormatterExtension(BLACK_EXTENSION, resource); - } - } else if (selection === 'Autopep8') { - if (autopep8) { - userSelectedAnExtension = true; - await updateDefaultFormatter(AUTOPEP8_EXTENSION, resource); - } else { - userSelectedAnExtension = true; - await installFormatterExtension(AUTOPEP8_EXTENSION, resource); - } - } else if (selection === Common.doNotShowAgain) { - userSelectedAnExtension = false; - await promptState.updateValue(true); - } else { - userSelectedAnExtension = false; - } - - this.currentlyShown = false; - return userSelectedAnExtension; - } -} - -export function registerInstallFormatterPrompt(serviceContainer: IServiceContainer): void { - const disposables = serviceContainer.get(IDisposableRegistry); - const installFormatterPrompt = serviceContainer.get(IInstallFormatterPrompt); - disposables.push( - onDidSaveTextDocument(async (e) => { - const editorConfig = getConfiguration('editor', { uri: e.uri, languageId: 'python' }); - if (e.languageId === 'python' && editorConfig.get('formatOnSave')) { - await installFormatterPrompt.showInstallFormatterPrompt(e.uri); - } - }), - ); -} diff --git a/src/client/providers/prompts/promptUtils.ts b/src/client/providers/prompts/promptUtils.ts deleted file mode 100644 index 05b1b28f061a..000000000000 --- a/src/client/providers/prompts/promptUtils.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { ConfigurationTarget, Uri } from 'vscode'; -import { ShowFormatterExtensionPrompt } from '../../common/experiments/groups'; -import { IExperimentService, IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { isInsider } from '../../common/vscodeApis/extensionsApi'; -import { getConfiguration, getWorkspaceFolder } from '../../common/vscodeApis/workspaceApis'; -import { IServiceContainer } from '../../ioc/types'; - -export function inFormatterExtensionExperiment(serviceContainer: IServiceContainer): boolean { - const experiment = serviceContainer.get(IExperimentService); - return experiment.inExperimentSync(ShowFormatterExtensionPrompt.experiment); -} - -export function doNotShowPromptState(key: string, serviceContainer: IServiceContainer): IPersistentState { - const persistFactory = serviceContainer.get(IPersistentStateFactory); - const promptState = persistFactory.createWorkspacePersistentState(key, false); - return promptState; -} - -export async function updateDefaultFormatter(extensionId: string, resource?: Uri): Promise { - const scope = getWorkspaceFolder(resource) ? ConfigurationTarget.Workspace : ConfigurationTarget.Global; - - const config = getConfiguration('python', resource); - const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' }); - await editorConfig.update('defaultFormatter', extensionId, scope, true); - await config.update('formatting.provider', 'none', scope); -} - -export async function installFormatterExtension(extensionId: string, resource?: Uri): Promise { - await executeCommand('workbench.extensions.installExtension', extensionId, { - installPreReleaseVersion: isInsider(), - }); - - await updateDefaultFormatter(extensionId, resource); -} diff --git a/src/client/providers/prompts/types.ts b/src/client/providers/prompts/types.ts deleted file mode 100644 index 4edaadb46b46..000000000000 --- a/src/client/providers/prompts/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; - -export const BLACK_EXTENSION = 'ms-python.black-formatter'; -export const AUTOPEP8_EXTENSION = 'ms-python.autopep8'; - -export const IInstallFormatterPrompt = Symbol('IInstallFormatterPrompt'); -export interface IInstallFormatterPrompt { - showInstallFormatterPrompt(resource?: Uri): Promise; -} diff --git a/src/client/providers/serviceRegistry.ts b/src/client/providers/serviceRegistry.ts index 70fc6dc34135..a96ec14ff5e9 100644 --- a/src/client/providers/serviceRegistry.ts +++ b/src/client/providers/serviceRegistry.ts @@ -6,13 +6,10 @@ import { IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { CodeActionProviderService } from './codeActionProvider/main'; -import { InstallFormatterPrompt } from './prompts/installFormatterPrompt'; -import { IInstallFormatterPrompt } from './prompts/types'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton( IExtensionSingleActivationService, CodeActionProviderService, ); - serviceManager.addSingleton(IInstallFormatterPrompt, InstallFormatterPrompt); } diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 5c1842a2c97c..15c745cbd64f 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -28,11 +28,7 @@ import { EditorUtils } from '../../client/common/editor'; import { ExperimentService } from '../../client/common/experiments/service'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; +import { LinterProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IInstallationChannelManager, @@ -131,7 +127,6 @@ suite('Installer', () => { ioc.registerFileSystemTypes(); ioc.registerVariableTypes(); ioc.registerLinterTypes(); - ioc.registerFormatterTypes(); ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); @@ -159,11 +154,6 @@ suite('Installer', () => { ioc.registerMockProcessTypes(); ioc.serviceManager.addSingletonInstance(IsWindows, false); ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); ioc.serviceManager.addSingleton( IProductPathService, LinterProductPathService, diff --git a/src/test/common/installer/channelManager.unit.test.ts b/src/test/common/installer/channelManager.unit.test.ts index 319a9647fec7..9789f9f18718 100644 --- a/src/test/common/installer/channelManager.unit.test.ts +++ b/src/test/common/installer/channelManager.unit.test.ts @@ -57,7 +57,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); expect(channel).to.equal(undefined, 'should be undefined'); assert.ok(showNoInstallersMessage.calledOnceWith(resource)); }); @@ -79,7 +79,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); assert.ok(showNoInstallersMessage.notCalled); appShell.verifyAll(); expect(channel).to.equal(undefined, 'Channel should not be set'); @@ -107,7 +107,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); assert.ok(showNoInstallersMessage.notCalled); appShell.verifyAll(); expect(channel).to.not.equal(undefined, 'Channel should be set'); diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts index 0f627289da70..8e65f3a5caed 100644 --- a/src/test/common/installer/productPath.unit.test.ts +++ b/src/test/common/installer/productPath.unit.test.ts @@ -12,21 +12,12 @@ import '../../../client/common/extensions'; import { ProductInstaller } from '../../../client/common/installer/productInstaller'; import { BaseProductPathsService, - FormatterProductPathService, LinterProductPathService, TestFrameworkProductPathService, } from '../../../client/common/installer/productPath'; import { ProductService } from '../../../client/common/installer/productService'; import { IProductService } from '../../../client/common/installer/types'; -import { - IConfigurationService, - IFormattingSettings, - IInstaller, - IPythonSettings, - Product, - ProductType, -} from '../../../client/common/types'; -import { IFormatterHelper } from '../../../client/formatters/types'; +import { IConfigurationService, IInstaller, IPythonSettings, Product, ProductType } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { ILinterInfo, ILinterManager } from '../../../client/linters/types'; import { ITestsHelper } from '../../../client/testing/common/types'; @@ -44,7 +35,6 @@ suite('Product Path', () => { } } let serviceContainer: TypeMoq.IMock; - let formattingSettings: TypeMoq.IMock; let unitTestSettings: TypeMoq.IMock; let configService: TypeMoq.IMock; let productInstaller: ProductInstaller; @@ -54,12 +44,10 @@ suite('Product Path', () => { } serviceContainer = TypeMoq.Mock.ofType(); configService = TypeMoq.Mock.ofType(); - formattingSettings = TypeMoq.Mock.ofType(); unitTestSettings = TypeMoq.Mock.ofType(); productInstaller = new ProductInstaller(serviceContainer.object); const pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup((p) => p.formatting).returns(() => formattingSettings.object); pythonSettings.setup((p) => p.testing).returns(() => unitTestSettings.object); configService .setup((s) => s.getSettings(TypeMoq.It.isValue(resource))) @@ -99,37 +87,6 @@ suite('Product Path', () => { }); const productType = new ProductService().getProductType(product.value); switch (productType) { - case ProductType.Formatter: { - test(`Ensure path is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new FormatterProductPathService(serviceContainer.object); - const formatterHelper = TypeMoq.Mock.ofType(); - const expectedPath = 'Some Path'; - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IFormatterHelper), TypeMoq.It.isAny())) - .returns(() => formatterHelper.object); - formattingSettings - .setup((f) => f.autopep8Path) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - formatterHelper - .setup((f) => f.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - pathName: 'autopep8Path', - argsName: 'autopep8Args', - }; - }) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - formattingSettings.verifyAll(); - formatterHelper.verifyAll(); - }); - break; - } case ProductType.Linter: { test(`Ensure path is returned for ${product.name} (${ resource ? 'With a resource' : 'without a resource' diff --git a/src/test/common/installer/serviceRegistry.unit.test.ts b/src/test/common/installer/serviceRegistry.unit.test.ts index a23cff298d6c..5b971790fa9a 100644 --- a/src/test/common/installer/serviceRegistry.unit.test.ts +++ b/src/test/common/installer/serviceRegistry.unit.test.ts @@ -10,7 +10,6 @@ import { PipEnvInstaller } from '../../../client/common/installer/pipEnvInstalle import { PipInstaller } from '../../../client/common/installer/pipInstaller'; import { PoetryInstaller } from '../../../client/common/installer/poetryInstaller'; import { - FormatterProductPathService, LinterProductPathService, TestFrameworkProductPathService, } from '../../../client/common/installer/productPath'; @@ -46,13 +45,6 @@ suite('Common installer Service Registry', () => { ), ).once(); verify(serviceManager.addSingleton(IProductService, ProductService)).once(); - verify( - serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ), - ).once(); verify( serviceManager.addSingleton( IProductPathService, diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index a6b647ad181d..2f73bc520307 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -147,7 +147,6 @@ suite('Module Installer', () => { ioc.registerUnitTestTypes(); ioc.registerVariableTypes(); ioc.registerLinterTypes(); - ioc.registerFormatterTypes(); ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts deleted file mode 100644 index 40131be24ec2..000000000000 --- a/src/test/format/extension.format.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { CancellationTokenSource, Position, Uri, window, workspace } from 'vscode'; -import { IProcessServiceFactory } from '../../client/common/process/types'; -import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; -import { BlackFormatter } from '../../client/formatters/blackFormatter'; -import { YapfFormatter } from '../../client/formatters/yapfFormatter'; -import { isPythonVersionInProcess } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { MockProcessService } from '../mocks/proc'; -import { registerForIOC } from '../pythonEnvironments/legacyIOC'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; -import { compareFiles } from '../textUtils'; - -const ch = window.createOutputChannel('Tests'); -const formatFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); -const workspaceRootPath = path.join(__dirname, '..', '..', '..', 'src', 'test'); -const originalUnformattedFile = path.join(formatFilesPath, 'fileToFormat.py'); - -const autoPep8FileToFormat = path.join(formatFilesPath, 'autoPep8FileToFormat.py'); -const autoPep8Formatted = path.join(formatFilesPath, 'autoPep8Formatted.py'); -const blackFileToFormat = path.join(formatFilesPath, 'blackFileToFormat.py'); -const blackFormatted = path.join(formatFilesPath, 'blackFormatted.py'); -const yapfFileToFormat = path.join(formatFilesPath, 'yapfFileToFormat.py'); -const yapfFormatted = path.join(formatFilesPath, 'yapfFormatted.py'); - -let formattedYapf = ''; -let formattedBlack = ''; -let formattedAutoPep8 = ''; - -suite('Formatting - General', () => { - let ioc: UnitTestIocContainer; - - suiteSetup(async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - // Skipping one test in the file is resulting in the next one failing, so skipping the entire suiteuntil further investigation. - - return this.skip(); - await initialize(); - await initializeDI(); - [autoPep8FileToFormat, blackFileToFormat, yapfFileToFormat].forEach((file) => { - fs.copySync(originalUnformattedFile, file, { overwrite: true }); - }); - formattedYapf = fs.readFileSync(yapfFormatted).toString(); - formattedAutoPep8 = fs.readFileSync(autoPep8Formatted).toString(); - formattedBlack = fs.readFileSync(blackFormatted).toString(); - }); - - async function formattingTestIsBlackSupported(): Promise { - const processService = await ioc.serviceContainer - .get(IProcessServiceFactory) - .create(Uri.file(workspaceRootPath)); - return !(await isPythonVersionInProcess(processService, '2', '3.0', '3.1', '3.2', '3.3', '3.4', '3.5')); - } - - setup(async () => { - await initializeTest(); - await initializeDI(); - }); - suiteTeardown(async () => { - [autoPep8FileToFormat, blackFileToFormat, yapfFileToFormat].forEach((file) => { - if (fs.existsSync(file)) { - fs.unlinkSync(file); - } - }); - ch.dispose(); - await closeActiveWindows(); - }); - teardown(async () => { - await ioc.dispose(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerUnitTestTypes(); - ioc.registerFormatterTypes(); - ioc.registerInterpreterStorageTypes(); - - // Mocks. - ioc.registerMockProcessTypes(); - await ioc.registerMockInterpreterTypes(); - - await registerForIOC(ioc.serviceManager, ioc.serviceContainer); - } - - async function injectFormatOutput(outputFileName: string) { - const procService = (await ioc.serviceContainer - .get(IProcessServiceFactory) - .create()) as MockProcessService; - procService.onExecObservable((_file, args, _options, callback) => { - if (args.indexOf('--diff') >= 0) { - callback({ - out: fs.readFileSync(path.join(formatFilesPath, outputFileName), 'utf8'), - source: 'stdout', - }); - } - }); - } - - async function testFormatting( - formatter: AutoPep8Formatter | BlackFormatter | YapfFormatter, - formattedContents: string, - fileToFormat: string, - outputFileName: string, - ) { - const textDocument = await workspace.openTextDocument(fileToFormat); - const textEditor = await window.showTextDocument(textDocument); - const options = { - insertSpaces: textEditor.options.insertSpaces! as boolean, - tabSize: textEditor.options.tabSize! as number, - }; - - await injectFormatOutput(outputFileName); - - const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); - await textEditor.edit((editBuilder) => { - edits.forEach((edit) => editBuilder.replace(edit.range, edit.newText)); - }); - compareFiles(formattedContents, textEditor.document.getText()); - } - - test('AutoPep8', async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - - return this.skip(); - await testFormatting( - new AutoPep8Formatter(ioc.serviceContainer), - formattedAutoPep8, - autoPep8FileToFormat, - 'autopep8.output', - ); - }); - - test('Black', async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - - return this.skip(); - if (!(await formattingTestIsBlackSupported())) { - // Skip for versions of python below 3.6, as Black doesn't support them at all. - - return this.skip(); - } - await testFormatting( - new BlackFormatter(ioc.serviceContainer), - formattedBlack, - blackFileToFormat, - 'black.output', - ); - }); - test('Yapf', async () => - testFormatting(new YapfFormatter(ioc.serviceContainer), formattedYapf, yapfFileToFormat, 'yapf.output')); - - test('Yapf on dirty file', async () => { - const sourceDir = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); - const targetDir = path.join(__dirname, '..', 'pythonFiles', 'formatting'); - - const originalName = 'formatWhenDirty.py'; - const resultsName = 'formatWhenDirtyResult.py'; - const fileToFormat = path.join(targetDir, originalName); - const formattedFile = path.join(targetDir, resultsName); - - if (!fs.pathExistsSync(targetDir)) { - fs.mkdirpSync(targetDir); - } - fs.copySync(path.join(sourceDir, originalName), fileToFormat, { overwrite: true }); - fs.copySync(path.join(sourceDir, resultsName), formattedFile, { overwrite: true }); - - const textDocument = await workspace.openTextDocument(fileToFormat); - const textEditor = await window.showTextDocument(textDocument); - await textEditor.edit((builder) => { - // Make file dirty. Trailing blanks will be removed. - builder.insert(new Position(0, 0), '\n \n'); - }); - - const dir = path.dirname(fileToFormat); - const configFile = path.join(dir, '.style.yapf'); - try { - // Create yapf configuration file - const content = '[style]\nbased_on_style = pep8\nindent_width=5\n'; - fs.writeFileSync(configFile, content); - - const options = { insertSpaces: textEditor.options.insertSpaces! as boolean, tabSize: 1 }; - const formatter = new YapfFormatter(ioc.serviceContainer); - const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); - await textEditor.edit((editBuilder) => { - edits.forEach((edit) => editBuilder.replace(edit.range, edit.newText)); - }); - - const expected = fs.readFileSync(formattedFile).toString(); - const actual = textEditor.document.getText(); - compareFiles(expected, actual); - } finally { - if (fs.existsSync(configFile)) { - fs.unlinkSync(configFile); - } - } - }); -}); diff --git a/src/test/format/format.helper.test.ts b/src/test/format/format.helper.test.ts deleted file mode 100644 index 50000f1af867..000000000000 --- a/src/test/format/format.helper.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import * as assert from 'assert'; -import * as TypeMoq from 'typemoq'; -import { IConfigurationService, IFormattingSettings, Product } from '../../client/common/types'; -import * as EnumEx from '../../client/common/utils/enum'; -import { FormatterHelper } from '../../client/formatters/helper'; -import { FormatterId } from '../../client/formatters/types'; -import { getExtensionSettings } from '../extensionSettings'; -import { initialize } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -suite('Formatting - Helper', () => { - let ioc: UnitTestIocContainer; - let formatHelper: FormatterHelper; - - suiteSetup(initialize); - setup(() => { - ioc = new UnitTestIocContainer(); - - const config = TypeMoq.Mock.ofType(); - config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => getExtensionSettings(undefined)); - - ioc.serviceManager.addSingletonInstance(IConfigurationService, config.object); - formatHelper = new FormatterHelper(ioc.serviceManager); - }); - - test('Ensure product is set in Execution Info', async () => { - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const info = formatHelper.getExecutionInfo(formatter, []); - assert.strictEqual( - info.product, - formatter, - `Incorrect products for ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure executable is set in Execution Info', async () => { - const settings = getExtensionSettings(undefined); - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const info = formatHelper.getExecutionInfo(formatter, []); - const names = formatHelper.getSettingsPropertyNames(formatter); - const execPath = settings.formatting[names.pathName] as string; - - assert.strictEqual( - info.execPath, - execPath, - `Incorrect executable paths for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure arguments are set in Execution Info', async () => { - const settings = getExtensionSettings(undefined); - const customArgs = ['1', '2', '3']; - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const names = formatHelper.getSettingsPropertyNames(formatter); - const args: string[] = Array.isArray(settings.formatting[names.argsName]) - ? (settings.formatting[names.argsName] as string[]) - : []; - const expectedArgs = args.concat(customArgs).join(','); - - assert.strictEqual( - expectedArgs.endsWith(customArgs.join(',')), - true, - `Incorrect custom arguments for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure correct setting names are returned', async () => { - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const translatedId = formatHelper.translateToId(formatter)!; - const settings = { - argsName: `${translatedId}Args` as keyof IFormattingSettings, - pathName: `${translatedId}Path` as keyof IFormattingSettings, - }; - - assert.deepEqual( - formatHelper.getSettingsPropertyNames(formatter), - settings, - `Incorrect settings for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure translation of ids works', async () => { - const formatterMapping = new Map(); - formatterMapping.set(Product.autopep8, 'autopep8'); - formatterMapping.set(Product.black, 'black'); - formatterMapping.set(Product.yapf, 'yapf'); - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const translatedId = formatHelper.translateToId(formatter); - assert.strictEqual( - translatedId, - formatterMapping.get(formatter)!, - `Incorrect translation for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - EnumEx.getValues(Product).forEach((product) => { - const formatterMapping = new Map(); - formatterMapping.set(Product.autopep8, 'autopep8'); - formatterMapping.set(Product.black, 'black'); - formatterMapping.set(Product.yapf, 'yapf'); - if (formatterMapping.has(product)) { - return; - } - - test(`Ensure translation of ids throws exceptions for unknown formatters (${product})`, async () => { - assert.throws(() => formatHelper.translateToId(product)); - }); - }); -}); diff --git a/src/test/format/formatter.unit.test.ts b/src/test/format/formatter.unit.test.ts deleted file mode 100644 index 05970d0c71f6..000000000000 --- a/src/test/format/formatter.unit.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { anything, capture, instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CancellationTokenSource, FormattingOptions, TextDocument, Uri } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { PythonSettings } from '../../client/common/configSettings'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { IPythonToolExecutionService } from '../../client/common/process/types'; -import { - ExecutionInfo, - IConfigurationService, - IDisposableRegistry, - IFormattingSettings, - ILogOutputChannel, - IPythonSettings, -} from '../../client/common/types'; -import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; -import { BaseFormatter } from '../../client/formatters/baseFormatter'; -import { BlackFormatter } from '../../client/formatters/blackFormatter'; -import { FormatterHelper } from '../../client/formatters/helper'; -import { IFormatterHelper } from '../../client/formatters/types'; -import { YapfFormatter } from '../../client/formatters/yapfFormatter'; -import { ServiceContainer } from '../../client/ioc/container'; -import { IServiceContainer } from '../../client/ioc/types'; -import { noop } from '../core'; -import { MockOutputChannel } from '../mockClasses'; - -suite('Formatting - Test Arguments', () => { - let container: IServiceContainer; - let outputChannel: ILogOutputChannel; - let workspace: IWorkspaceService; - let settings: IPythonSettings; - const workspaceUri = Uri.file(__dirname); - let document: typemoq.IMock; - const docUri = Uri.file(__filename); - let pythonToolExecutionService: IPythonToolExecutionService; - const options: FormattingOptions = { insertSpaces: false, tabSize: 1 }; - const formattingSettingsWithPath: IFormattingSettings = { - autopep8Args: ['1', '2'], - autopep8Path: path.join('a', 'exe'), - blackArgs: ['1', '2'], - blackPath: path.join('a', 'exe'), - provider: '', - yapfArgs: ['1', '2'], - yapfPath: path.join('a', 'exe'), - }; - - const formattingSettingsWithModuleName: IFormattingSettings = { - autopep8Args: ['1', '2'], - autopep8Path: 'module_name', - blackArgs: ['1', '2'], - blackPath: 'module_name', - provider: '', - yapfArgs: ['1', '2'], - yapfPath: 'module_name', - }; - - setup(() => { - container = mock(ServiceContainer); - outputChannel = mock(MockOutputChannel); - workspace = mock(WorkspaceService); - settings = mock(PythonSettings); - document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => ''); - document.setup((doc) => doc.isDirty).returns(() => false); - document.setup((doc) => doc.fileName).returns(() => docUri.fsPath); - document.setup((doc) => doc.uri).returns(() => docUri); - pythonToolExecutionService = mock(PythonToolExecutionService); - - const configService = mock(ConfigurationService); - const formatterHelper = new FormatterHelper(instance(container)); - - const appShell = mock(ApplicationShell); - when(appShell.setStatusBarMessage(anything(), anything())).thenReturn({ dispose: noop }); - - when(configService.getSettings(anything())).thenReturn(instance(settings)); - when(workspace.getWorkspaceFolder(anything())).thenReturn({ name: '', index: 0, uri: workspaceUri }); - when(container.get(ILogOutputChannel)).thenReturn(instance(outputChannel)); - when(container.get(IApplicationShell)).thenReturn(instance(appShell)); - when(container.get(IFormatterHelper)).thenReturn(formatterHelper); - when(container.get(IWorkspaceService)).thenReturn(instance(workspace)); - when(container.get(IConfigurationService)).thenReturn(instance(configService)); - when(container.get(IPythonToolExecutionService)).thenReturn( - instance(pythonToolExecutionService), - ); - when(container.get(IDisposableRegistry)).thenReturn([]); - }); - - async function setupFormatter( - formatter: BaseFormatter, - formattingSettings: IFormattingSettings, - ): Promise { - const { token } = new CancellationTokenSource(); - when(settings.formatting).thenReturn(formattingSettings); - when(pythonToolExecutionService.exec(anything(), anything(), anything())).thenResolve({ stdout: '' }); - - await formatter.formatDocument(document.object, options, token); - - const args = capture(pythonToolExecutionService.exec).first(); - return args[0]; - } - test('Ensure blackPath and args used to launch the formatter', async () => { - const formatter = new BlackFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.blackPath); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual( - execInfo.args, - formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath]), - ); - }); - test('Ensure black modulename and args used to launch the formatter', async () => { - const formatter = new BlackFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.blackPath); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.blackPath); - assert.deepEqual( - execInfo.args, - formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath]), - ); - }); - test('Ensure autopep8path and args used to launch the formatter', async () => { - const formatter = new AutoPep8Formatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.autopep8Path); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); - }); - test('Ensure autpep8 modulename and args used to launch the formatter', async () => { - const formatter = new AutoPep8Formatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.autopep8Path); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.autopep8Path); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); - }); - test('Ensure yapfpath and args used to launch the formatter', async () => { - const formatter = new YapfFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.yapfPath); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); - }); - test('Ensure yapf modulename and args used to launch the formatter', async () => { - const formatter = new YapfFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.yapfPath); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.yapfPath); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); - }); -}); diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts index f89ee86c0b42..5c1cae31d158 100644 --- a/src/test/linters/lint.multiroot.test.ts +++ b/src/test/linters/lint.multiroot.test.ts @@ -3,11 +3,7 @@ import * as path from 'path'; import { CancellationTokenSource, ConfigurationTarget, Uri, workspace } from 'vscode'; import { LanguageServerType } from '../../client/activation/types'; import { PythonSettings } from '../../client/common/configSettings'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; +import { LinterProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IProductPathService, IProductService } from '../../client/common/installer/types'; import { IConfigurationService, Product, ProductType } from '../../client/common/types'; @@ -72,11 +68,6 @@ suite('Multiroot Linting', () => { ); ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); ioc.serviceManager.addSingleton( IProductPathService, LinterProductPathService, diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts index d2eef3c9e321..837830f0c499 100644 --- a/src/test/linters/lint.test.ts +++ b/src/test/linters/lint.test.ts @@ -6,11 +6,7 @@ import * as assert from 'assert'; import { ConfigurationTarget } from 'vscode'; import { Product } from '../../client/common/installer/productInstaller'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; +import { LinterProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IProductPathService, IProductService } from '../../client/common/installer/types'; import { IConfigurationService, ILintingSettings, ProductType } from '../../client/common/types'; @@ -50,11 +46,6 @@ suite('Linting Settings', () => { configService = ioc.serviceContainer.get(IConfigurationService); linterManager = new LinterManager(configService); ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); ioc.serviceManager.addSingleton( IProductPathService, LinterProductPathService, diff --git a/src/test/providers/prompt/installFormatterPrompt.unit.test.ts b/src/test/providers/prompt/installFormatterPrompt.unit.test.ts deleted file mode 100644 index fbd3a72d8cef..000000000000 --- a/src/test/providers/prompt/installFormatterPrompt.unit.test.ts +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { WorkspaceConfiguration } from 'vscode'; -import { IPersistentState } from '../../../client/common/types'; -import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; -import * as windowApis from '../../../client/common/vscodeApis/windowApis'; -import * as extensionsApi from '../../../client/common/vscodeApis/extensionsApi'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { InstallFormatterPrompt } from '../../../client/providers/prompts/installFormatterPrompt'; -import * as promptUtils from '../../../client/providers/prompts/promptUtils'; -import { AUTOPEP8_EXTENSION, BLACK_EXTENSION, IInstallFormatterPrompt } from '../../../client/providers/prompts/types'; -import { Common, ToolsExtensions } from '../../../client/common/utils/localize'; - -suite('Formatter Extension prompt tests', () => { - let inFormatterExtensionExperimentStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let prompt: IInstallFormatterPrompt; - let serviceContainer: TypeMoq.IMock; - let persistState: TypeMoq.IMock>; - let getConfigurationStub: sinon.SinonStub; - let isExtensionEnabledStub: sinon.SinonStub; - let pythonConfig: TypeMoq.IMock; - let editorConfig: TypeMoq.IMock; - let showInformationMessageStub: sinon.SinonStub; - let installFormatterExtensionStub: sinon.SinonStub; - let updateDefaultFormatterStub: sinon.SinonStub; - - setup(() => { - inFormatterExtensionExperimentStub = sinon.stub(promptUtils, 'inFormatterExtensionExperiment'); - inFormatterExtensionExperimentStub.returns(true); - - doNotShowPromptStateStub = sinon.stub(promptUtils, 'doNotShowPromptState'); - persistState = TypeMoq.Mock.ofType>(); - doNotShowPromptStateStub.returns(persistState.object); - - getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); - pythonConfig = TypeMoq.Mock.ofType(); - editorConfig = TypeMoq.Mock.ofType(); - getConfigurationStub.callsFake((section: string) => { - if (section === 'python') { - return pythonConfig.object; - } - return editorConfig.object; - }); - isExtensionEnabledStub = sinon.stub(extensionsApi, 'isExtensionEnabled'); - showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); - installFormatterExtensionStub = sinon.stub(promptUtils, 'installFormatterExtension'); - updateDefaultFormatterStub = sinon.stub(promptUtils, 'updateDefaultFormatter'); - - serviceContainer = TypeMoq.Mock.ofType(); - - prompt = new InstallFormatterPrompt(serviceContainer.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Not in experiment', async () => { - inFormatterExtensionExperimentStub.returns(false); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(doNotShowPromptStateStub.notCalled); - }); - - test('Do not show was set', async () => { - persistState.setup((p) => p.value).returns(() => true); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(getConfigurationStub.notCalled); - }); - - test('Formatting provider is set to none', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'none'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to yapf', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'yapf'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to autopep8, and autopep8 extension is set as default formatter', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => AUTOPEP8_EXTENSION); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to black, and black extension is set as default formatter', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => BLACK_EXTENSION); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Prompt: user selects do not show', async () => { - persistState.setup((p) => p.value).returns(() => false); - persistState - .setup((p) => p.updateValue(true)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.atLeastOnce()); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves(Common.doNotShowAgain); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - persistState.verifyAll(); - }); - - test('Prompt (autopep8): user selects Autopep8', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (autopep8): user selects Black', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(BLACK_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (black): user selects Autopep8', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (black): user selects Black', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(BLACK_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt: Black and Autopep8 installed user selects Black as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns({}); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(BLACK_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Black and Autopep8 installed user selects Autopep8 as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns({}); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Black installed user selects Black as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.callsFake((extensionId) => { - if (extensionId === BLACK_EXTENSION) { - return {}; - } - return undefined; - }); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectBlackFormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(BLACK_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Autopep8 installed user selects Autopep8 as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.callsFake((extensionId) => { - if (extensionId === AUTOPEP8_EXTENSION) { - return {}; - } - return undefined; - }); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectAutopep8FormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); -}); diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index c20a84b1e25a..e7b11d2b745b 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -33,7 +33,6 @@ import { ITestOutputChannel, } from '../client/common/types'; import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; -import { registerTypes as formattersRegisterTypes } from '../client/formatters/serviceRegistry'; import { EnvironmentActivationService } from '../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../client/interpreter/activation/types'; import { @@ -147,10 +146,6 @@ export class IocContainer { lintersRegisterTypes(this.serviceManager); } - public registerFormatterTypes(): void { - formattersRegisterTypes(this.serviceManager); - } - public registerPlatformTypes(): void { platformRegisterTypes(this.serviceManager); } From 1dd8a4bdb16d0a7e79082c75d8eb55a142a48fc2 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 11 Oct 2023 13:44:13 -0700 Subject: [PATCH 326/336] switch testing output to test result panel (#22039) closes https://github.com/microsoft/vscode-python/issues/21861 and related issues --------- Co-authored-by: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> --- .../tests/pytestadapter/.data/test_logging.py | 35 ++++++++ .../expected_execution_test_output.py | 28 ++++++ pythonFiles/tests/pytestadapter/helpers.py | 1 + .../tests/pytestadapter/test_execution.py | 27 +++--- pythonFiles/unittestadapter/execution.py | 2 - .../vscode_pytest/run_pytest_script.py | 2 - .../testController/common/resultResolver.ts | 48 ++++------ .../testing/testController/common/server.ts | 79 +++++++++------- .../testing/testController/common/utils.ts | 10 +++ .../pytest/pytestDiscoveryAdapter.ts | 19 +++- .../pytest/pytestExecutionAdapter.ts | 10 ++- .../testing/common/testingAdapter.test.ts | 90 ++++++++++++++++++- .../loggingWorkspace/test_logging.py | 13 +++ .../smallWorkspace/test_simple.py | 17 +++- 14 files changed, 298 insertions(+), 83 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/test_logging.py create mode 100644 src/testTestingRootWkspc/loggingWorkspace/test_logging.py diff --git a/pythonFiles/tests/pytestadapter/.data/test_logging.py b/pythonFiles/tests/pytestadapter/.data/test_logging.py new file mode 100644 index 000000000000..058ad8075718 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/test_logging.py @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging +import sys + + +def test_logging2(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + + # Printing to stdout and stderr + print("This is a stdout message.") + print("This is a stderr message.", file=sys.stderr) + assert False + + +def test_logging(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + + # Printing to stdout and stderr + print("This is a stdout message.") + print("This is a stderr message.", file=sys.stderr) diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index 76d21b3e2518..3fdb7b45a0c0 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -596,3 +596,31 @@ "subtest": None, } } + + +# This is the expected output for the test logging file. +# └── test_logging.py +# └── test_logging2: failure +# └── test_logging: success +test_logging_path = TEST_DATA_PATH / "test_logging.py" + +logging_test_expected_execution_output = { + get_absolute_test_id("test_logging.py::test_logging2", test_logging_path): { + "test": get_absolute_test_id( + "test_logging.py::test_logging2", test_logging_path + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_logging.py::test_logging", test_logging_path): { + "test": get_absolute_test_id( + "test_logging.py::test_logging", test_logging_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index b534e950945a..2d36da59956b 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -129,6 +129,7 @@ def runner_with_cwd( "pytest", "-p", "vscode_pytest", + "-s", ] + args listener: socket.socket = create_server() _, port = listener.getsockname() diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 37a392f66d4b..98698d8cdd7c 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -215,23 +215,30 @@ def test_bad_id_error_execution(): ], expected_execution_test_output.doctest_pytest_expected_execution_output, ), + ( + ["test_logging.py::test_logging2", "test_logging.py::test_logging"], + expected_execution_test_output.logging_test_expected_execution_output, + ), ], ) def test_pytest_execution(test_ids, expected_const): """ Test that pytest discovery works as expected where run pytest is always successful but the actual test results are both successes and failures.: - 1. uf_execution_expected_output: unittest tests run on multiple files. - 2. uf_single_file_expected_output: test run on a single file. - 3. uf_single_method_execution_expected_output: test run on a single method in a file. - 4. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. - 5. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. - 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file + 1: skip_tests_execution_expected_output: test run on a file with skipped tests. + 2. error_raised_exception_execution_expected_output: test run on a file that raises an exception. + 3. uf_execution_expected_output: unittest tests run on multiple files. + 4. uf_single_file_expected_output: test run on a single file. + 5. uf_single_method_execution_expected_output: test run on a single method in a file. + 6. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. + 7. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. + 8. dual_level_nested_folder_execution_expected_output: test run on a file with one test file at the top level and one test file in a nested folder. - 7. double_nested_folder_expected_execution_output: test run on a double nested folder. - 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. - 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. - 10. doctest_pytest_expected_execution_output: test run on doctest file. + 9. double_nested_folder_expected_execution_output: test run on a double nested folder. + 10. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. + 11. single_parametrize_tests_expected_execution_output: test run on single parametrize test. + 12. doctest_pytest_expected_execution_output: test run on doctest file. + 13. logging_test_expected_execution_output: test run on a file with logging. Keyword arguments: diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 5f46bda95328..e5758118b951 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -293,8 +293,6 @@ def post_response( ) # Clear the buffer as complete JSON object is received buffer = b"" - - # Process the JSON data break except json.JSONDecodeError: # JSON decoding error, the complete JSON object is not yet received diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py index 0fca8208a406..c3720c8ab8d0 100644 --- a/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -51,8 +51,6 @@ ) # Clear the buffer as complete JSON object is received buffer = b"" - - # Process the JSON data print("Received JSON data in run script") break except json.JSONDecodeError: diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index cf757d77243d..aaf82b143823 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -20,7 +20,7 @@ import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testI import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { splitLines } from '../../../common/stringUtils'; -import { buildErrorNodeOptions, fixLogLines, populateTestTree, splitTestNameWithRegex } from './utils'; +import { buildErrorNodeOptions, populateTestTree, splitTestNameWithRegex } from './utils'; import { Deferred } from '../../../common/utils/async'; export class PythonResultResolver implements ITestResultResolver { @@ -151,15 +151,16 @@ export class PythonResultResolver implements ITestResultResolver { const tempArr: TestItem[] = getTestCaseNodes(i); testCases.push(...tempArr); }); + const testItem = rawTestExecData.result[keyTemp]; - if (rawTestExecData.result[keyTemp].outcome === 'error') { - const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; + if (testItem.outcome === 'error') { + const rawTraceback = testItem.traceback ?? ''; const traceback = splitLines(rawTraceback, { trim: false, removeEmptyEntries: true, }).join('\r\n'); - const text = `${rawTestExecData.result[keyTemp].test} failed with error: ${ - rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome + const text = `${testItem.test} failed with error: ${ + testItem.message ?? testItem.outcome }\r\n${traceback}\r\n`; const message = new TestMessage(text); @@ -170,23 +171,17 @@ export class PythonResultResolver implements ITestResultResolver { if (indiItem.uri && indiItem.range) { message.location = new Location(indiItem.uri, indiItem.range); runInstance.errored(indiItem, message); - runInstance.appendOutput(fixLogLines(text)); } } }); - } else if ( - rawTestExecData.result[keyTemp].outcome === 'failure' || - rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' - ) { - const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; + } else if (testItem.outcome === 'failure' || testItem.outcome === 'passed-unexpected') { + const rawTraceback = testItem.traceback ?? ''; const traceback = splitLines(rawTraceback, { trim: false, removeEmptyEntries: true, }).join('\r\n'); - const text = `${rawTestExecData.result[keyTemp].test} failed: ${ - rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome - }\r\n${traceback}\r\n`; + const text = `${testItem.test} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}\r\n`; const message = new TestMessage(text); // note that keyTemp is a runId for unittest library... @@ -197,14 +192,10 @@ export class PythonResultResolver implements ITestResultResolver { if (indiItem.uri && indiItem.range) { message.location = new Location(indiItem.uri, indiItem.range); runInstance.failed(indiItem, message); - runInstance.appendOutput(fixLogLines(text)); } } }); - } else if ( - rawTestExecData.result[keyTemp].outcome === 'success' || - rawTestExecData.result[keyTemp].outcome === 'expected-failure' - ) { + } else if (testItem.outcome === 'success' || testItem.outcome === 'expected-failure') { const grabTestItem = this.runIdToTestItem.get(keyTemp); const grabVSid = this.runIdToVSid.get(keyTemp); if (grabTestItem !== undefined) { @@ -216,7 +207,7 @@ export class PythonResultResolver implements ITestResultResolver { } }); } - } else if (rawTestExecData.result[keyTemp].outcome === 'skipped') { + } else if (testItem.outcome === 'skipped') { const grabTestItem = this.runIdToTestItem.get(keyTemp); const grabVSid = this.runIdToVSid.get(keyTemp); if (grabTestItem !== undefined) { @@ -228,11 +219,11 @@ export class PythonResultResolver implements ITestResultResolver { } }); } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { + } else if (testItem.outcome === 'subtest-failure') { // split on [] or () based on how the subtest is setup. const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - const data = rawTestExecData.result[keyTemp]; + const data = testItem; // find the subtest's parent test item if (parentTestItem) { const subtestStats = this.subTestStats.get(parentTestCaseId); @@ -243,20 +234,19 @@ export class PythonResultResolver implements ITestResultResolver { failed: 1, passed: 0, }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); } const subTestItem = this.testController?.createTestItem(subtestId, subtestId); - runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); // create a new test item for the subtest if (subTestItem) { const traceback = data.traceback ?? ''; - const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; - runInstance.appendOutput(fixLogLines(text)); + const text = `${data.subtest} failed: ${ + testItem.message ?? testItem.outcome + }\r\n${traceback}\r\n`; parentTestItem.children.add(subTestItem); runInstance.started(subTestItem); - const message = new TestMessage(rawTestExecData?.result[keyTemp].message ?? ''); + const message = new TestMessage(text); if (parentTestItem.uri && parentTestItem.range) { message.location = new Location(parentTestItem.uri, parentTestItem.range); } @@ -267,7 +257,7 @@ export class PythonResultResolver implements ITestResultResolver { } else { throw new Error('Parent test item not found'); } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { + } else if (testItem.outcome === 'subtest-success') { // split on [] or () based on how the subtest is setup. const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); @@ -279,7 +269,6 @@ export class PythonResultResolver implements ITestResultResolver { subtestStats.passed += 1; } else { this.subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); } @@ -289,7 +278,6 @@ export class PythonResultResolver implements ITestResultResolver { parentTestItem.children.add(subTestItem); runInstance.started(subTestItem); runInstance.passed(subTestItem); - runInstance.appendOutput(fixLogLines(`${subtestId} Passed\r\n`)); } else { throw new Error('Unable to create new child node for subtest'); } diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 50ae1f3f7536..e496860526e4 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -17,10 +17,12 @@ import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { + MESSAGE_ON_TESTING_OUTPUT_MOVE, createDiscoveryErrorPayload, createEOTPayload, createExecutionErrorPayload, extractJsonPayload, + fixLogLinesNoTrailing, } from './utils'; import { createDeferred } from '../../../common/utils/async'; import { EnvironmentVariables } from '../../../api/types'; @@ -173,7 +175,7 @@ export class PythonTestServer implements ITestServer, Disposable { callback?: () => void, ): Promise { const { uuid } = options; - // get and edit env vars + const isDiscovery = (testIds === undefined || testIds.length === 0) && runTestIdPort === undefined; const mutableEnv = { ...env }; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [options.cwd, ...pythonPathParts].join(path.delimiter); @@ -196,7 +198,6 @@ export class PythonTestServer implements ITestServer, Disposable { resource: options.workspaceFolder, }; const execService = await this.executionFactory.createActivatedEnvironment(creationOptions); - const args = [options.command.script].concat(options.command.args); if (options.outChannel) { @@ -244,23 +245,55 @@ export class PythonTestServer implements ITestServer, Disposable { const result = execService?.execObservable(args, spawnOptions); resultProc = result?.proc; - // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. - result?.proc?.stdout?.on('data', (data) => { - spawnOptions?.outputChannel?.append(data.toString()); - }); - result?.proc?.stderr?.on('data', (data) => { - spawnOptions?.outputChannel?.append(data.toString()); - }); - result?.proc?.on('exit', (code, signal) => { - if (code !== 0) { - traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}`); - } - }); + // TODO: after a release, remove discovery output from the "Python Test Log" channel and send it to the "Python" channel instead. + // TODO: after a release, remove run output from the "Python Test Log" channel and send it to the "Test Result" channel instead. + if (isDiscovery) { + result?.proc?.stdout?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + spawnOptions?.outputChannel?.append(`${out}`); + traceInfo(out); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + spawnOptions?.outputChannel?.append(`${out}`); + traceError(out); + }); + } else { + result?.proc?.stdout?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(`${out}`); + spawnOptions?.outputChannel?.append(out); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(`${out}`); + spawnOptions?.outputChannel?.append(out); + }); + } result?.proc?.on('exit', (code, signal) => { // if the child has testIds then this is a run request - if (code !== 0 && testIds && testIds?.length !== 0) { + spawnOptions?.outputChannel?.append(MESSAGE_ON_TESTING_OUTPUT_MOVE); + if (isDiscovery) { + if (code !== 0) { + // This occurs when we are running discovery + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + ); + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createDiscoveryErrorPayload(code, signal, options.cwd)), + }); + // then send a EOT payload + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); + } + } else if (code !== 0 && testIds) { + // This occurs when we are running the test and there is an error which occurs. + traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, ); @@ -274,22 +307,8 @@ export class PythonTestServer implements ITestServer, Disposable { uuid, data: JSON.stringify(createEOTPayload(true)), }); - } else if (code !== 0) { - // This occurs when we are running discovery - traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, - ); - this._onDiscoveryDataReceived.fire({ - uuid, - data: JSON.stringify(createDiscoveryErrorPayload(code, signal, options.cwd)), - }); - // then send a EOT payload - this._onDiscoveryDataReceived.fire({ - uuid, - data: JSON.stringify(createEOTPayload(true)), - }); } - deferredTillExecClose.resolve({ stdout: '', stderr: '' }); + deferredTillExecClose.resolve(); }); await deferredTillExecClose.promise; } diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index f5f416529c42..5022fa5a44e6 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -23,6 +23,11 @@ export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}\r\n`; } + +export function fixLogLinesNoTrailing(content: string): string { + const lines = content.split(/\r?\n/g); + return `${lines.join('\r\n')}`; +} export interface IJSONRPCData { extractedJSON: string; remainingRawData: string; @@ -42,6 +47,11 @@ export interface ExtractOutput { export const JSONRPC_UUID_HEADER = 'Request-uuid'; export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length'; export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type'; +export const MESSAGE_ON_TESTING_OUTPUT_MOVE = + 'Starting now, all test run output will be sent to the Test Result panel,' + + ' while test discovery output will be sent to the "Python" output channel instead of the "Python Test Log" channel.' + + ' The "Python Test Log" channel will be deprecated within the next month.' + + 'See https://github.com/microsoft/vscode-python/wiki/New-Method-for-Output-Handling-in-Python-Testing for details.'; export function createTestingDeferred(): Deferred { return createDeferred(); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 4ed2570ba7cc..92bd9f04834e 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -18,7 +18,13 @@ import { ITestResultResolver, ITestServer, } from '../common/types'; -import { createDiscoveryErrorPayload, createEOTPayload, createTestingDeferred } from '../common/utils'; +import { + MESSAGE_ON_TESTING_OUTPUT_MOVE, + createDiscoveryErrorPayload, + createEOTPayload, + createTestingDeferred, + fixLogLinesNoTrailing, +} from '../common/utils'; import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; /** @@ -95,13 +101,20 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + // TODO: after a release, remove discovery output from the "Python Test Log" channel and send it to the "Python" channel instead. + result?.proc?.stdout?.on('data', (data) => { - spawnOptions.outputChannel?.append(data.toString()); + const out = fixLogLinesNoTrailing(data.toString()); + traceInfo(out); + spawnOptions?.outputChannel?.append(`${out}`); }); result?.proc?.stderr?.on('data', (data) => { - spawnOptions.outputChannel?.append(data.toString()); + const out = fixLogLinesNoTrailing(data.toString()); + traceError(out); + spawnOptions?.outputChannel?.append(`${out}`); }); result?.proc?.on('exit', (code, signal) => { + this.outputChannel?.append(MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0) { traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index eb8e9b6f935a..5c04aabab845 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -190,13 +190,19 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + // TODO: after a release, remove run output from the "Python Test Log" channel and send it to the "Test Result" channel instead. result?.proc?.stdout?.on('data', (data) => { - this.outputChannel?.append(data.toString()); + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(out); + this.outputChannel?.append(out); }); result?.proc?.stderr?.on('data', (data) => { - this.outputChannel?.append(data.toString()); + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(out); + this.outputChannel?.append(out); }); result?.proc?.on('exit', (code, signal) => { + this.outputChannel?.append(utils.MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0 && testIds) { traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); } diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 3b5ef0062a98..519a60e3f0f7 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -275,7 +275,7 @@ suite('End to End Tests: test adapters', () => { assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); - test('unittest execution adapter small workspace', async () => { + test('unittest execution adapter small workspace with correct output', async () => { // result resolver and saved data for assertions resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; @@ -319,12 +319,34 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); await executionAdapter .runTests(workspaceUri, ['test_simple.SimpleClass.test_simple_unit'], false, testRun.object) .finally(() => { // verify that the _resolveExecution was called once per test assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for stdout and stderr as well as unittest output + assert.ok( + collectedOutput.includes('expected printed output, stdout'), + 'The test string does not contain the expected stdout output.', + ); + assert.ok( + collectedOutput.includes('expected printed output, stderr'), + 'The test string does not contain the expected stderr output.', + ); + assert.ok( + collectedOutput.includes('Ran 1 test in'), + 'The test string does not contain the expected unittest output.', + ); }); }); test('unittest execution adapter large workspace', async () => { @@ -372,15 +394,33 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); await executionAdapter .runTests(workspaceUri, ['test_parameterized_subtest.NumbersTest.test_even'], false, testRun.object) .then(() => { // verify that the _resolveExecution was called once per test assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output + assert.ok( + collectedOutput.includes('test_parameterized_subtest.py'), + 'The test string does not contain the correct test name which should be printed', + ); + assert.ok( + collectedOutput.includes('FAILED (failures=1000)'), + 'The test string does not contain the last of the unittest output', + ); }); }); - test('pytest execution adapter small workspace', async () => { + test('pytest execution adapter small workspace with correct output', async () => { // result resolver and saved data for assertions resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; @@ -423,6 +463,14 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); await executionAdapter .runTests( workspaceUri, @@ -435,6 +483,30 @@ suite('End to End Tests: test adapters', () => { // verify that the _resolveExecution was called once per test assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for stdout and stderr as well as pytest output + assert.ok( + collectedOutput.includes('test session starts'), + 'The test string does not contain the expected stdout output.', + ); + assert.ok( + collectedOutput.includes('Captured log call'), + 'The test string does not contain the expected log section.', + ); + const searchStrings = [ + 'This is a warning message.', + 'This is an error message.', + 'This is a critical message.', + ]; + let searchString: string; + for (searchString of searchStrings) { + const count: number = (collectedOutput.match(new RegExp(searchString, 'g')) || []).length; + assert.strictEqual( + count, + 2, + `The test string does not contain two instances of ${searchString}. Should appear twice from logging output and stack trace`, + ); + } }); }); test('pytest execution adapter large workspace', async () => { @@ -488,10 +560,24 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).then(() => { // verify that the _resolveExecution was called once per test assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for large repo + assert.ok( + collectedOutput.includes('test session starts'), + 'The test string does not contain the expected stdout output from pytest.', + ); }); }); test('unittest discovery adapter seg fault error handling', async () => { diff --git a/src/testTestingRootWkspc/loggingWorkspace/test_logging.py b/src/testTestingRootWkspc/loggingWorkspace/test_logging.py new file mode 100644 index 000000000000..a3e77f06ae78 --- /dev/null +++ b/src/testTestingRootWkspc/loggingWorkspace/test_logging.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +import logging + + +def test_logging(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") diff --git a/src/testTestingRootWkspc/smallWorkspace/test_simple.py b/src/testTestingRootWkspc/smallWorkspace/test_simple.py index 6b4f7bd2f8a6..f68a0d7d0d93 100644 --- a/src/testTestingRootWkspc/smallWorkspace/test_simple.py +++ b/src/testTestingRootWkspc/smallWorkspace/test_simple.py @@ -1,12 +1,25 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import unittest +import logging +import sys -def test_a(): - assert 1 == 1 +def test_a(caplog): + logger = logging.getLogger(__name__) + # caplog.set_level(logging.ERROR) # Set minimum log level to capture + logger.setLevel(logging.WARN) + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + assert False class SimpleClass(unittest.TestCase): def test_simple_unit(self): + print("expected printed output, stdout") + print("expected printed output, stderr", file=sys.stderr) assert True From d1e4562b64e38045f549ca00025c4620a6a89567 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 12 Oct 2023 07:55:14 +1100 Subject: [PATCH 327/336] Move tensorboard support into a separate extension (#22197) * No need of experiments (if users install extension, then it works) * If tensorboard extension is installed the we rely on tensorboard extension to handle everything * For final deplayment we can decide whether to just remove this feature altogether or prompt users to install tensorboard extension or to go with an experiment, for now I wanted to keep this super simple (this shoudl not affect anyone as no one will have a tensorboard extension except us) * Simple private API for tensorboard extension, untill Python ext exposes a stable API * API is similar to Jupyter, scoped to Tensorboard ext --- package.json | 4 +- src/client/api.ts | 17 +++ src/client/common/constants.ts | 1 + .../nbextensionCodeLensProvider.ts | 4 + src/client/tensorBoard/serviceRegistry.ts | 2 + .../tensorBoard/tensorBoardFileWatcher.ts | 4 + .../tensorBoardImportCodeLensProvider.ts | 4 + src/client/tensorBoard/tensorBoardPrompt.ts | 2 +- src/client/tensorBoard/tensorBoardSession.ts | 9 +- .../tensorBoard/tensorBoardSessionProvider.ts | 7 +- .../tensorBoard/tensorBoardUsageTracker.ts | 4 + .../tensorBoard/tensorboarExperiment.ts | 8 ++ .../tensorboardDependencyChecker.ts | 60 +++++++++++ .../tensorBoard/tensorboardIntegration.ts | 102 ++++++++++++++++++ src/client/tensorBoard/terminalWatcher.ts | 4 + src/client/tensorBoard/types.ts | 7 +- .../tensorBoardUsageTracker.unit.test.ts | 7 ++ src/test/vscode-mock.ts | 43 ++++---- 18 files changed, 256 insertions(+), 33 deletions(-) create mode 100644 src/client/tensorBoard/tensorboarExperiment.ts create mode 100644 src/client/tensorBoard/tensorboardDependencyChecker.ts create mode 100644 src/client/tensorBoard/tensorboardIntegration.ts diff --git a/package.json b/package.json index 490e615f26f2..88578a687853 100644 --- a/package.json +++ b/package.json @@ -1836,7 +1836,7 @@ "category": "Python", "command": "python.launchTensorBoard", "title": "%python.command.python.launchTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled" }, { "category": "Python", @@ -1844,7 +1844,7 @@ "enablement": "python.hasActiveTensorBoardSession", "icon": "$(refresh)", "title": "%python.command.python.refreshTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled" }, { "category": "Python", diff --git a/src/client/api.ts b/src/client/api.ts index 23b2553c93d2..81a5f676cc22 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -21,6 +21,7 @@ import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { buildEnvironmentApi } from './environmentApi'; import { ApiForPylance } from './pylanceApi'; import { getTelemetryReporter } from './telemetry'; +import { TensorboardExtensionIntegration } from './tensorBoard/tensorboardIntegration'; export function buildApi( ready: Promise, @@ -31,7 +32,14 @@ export function buildApi( const configurationService = serviceContainer.get(IConfigurationService); const interpreterService = serviceContainer.get(IInterpreterService); serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); + serviceManager.addSingleton( + TensorboardExtensionIntegration, + TensorboardExtensionIntegration, + ); const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); + const tensorboardIntegration = serviceContainer.get( + TensorboardExtensionIntegration, + ); const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); const api: PythonExtension & { @@ -41,6 +49,12 @@ export function buildApi( jupyter: { registerHooks(): void; }; + /** + * Internal API just for Tensorboard, hence don't include in the official types. + */ + tensorboard: { + registerHooks(): void; + }; } & { /** * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an @@ -92,6 +106,9 @@ export function buildApi( jupyter: { registerHooks: () => jupyterIntegration.integrateWithJupyterExtension(), }, + tensorboard: { + registerHooks: () => tensorboardIntegration.integrateWithTensorboardExtension(), + }, debug: { async getRemoteLauncherCommand( host: string, diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index cd6d305f624a..6fc743fb8a0a 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -23,6 +23,7 @@ export const PYTHON_NOTEBOOKS = [ export const PVSC_EXTENSION_ID = 'ms-python.python'; export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance'; export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; +export const TENSORBOARD_EXTENSION_ID = 'ms-toolsai.tensorboard'; export const AppinsightsKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255'; export type Channel = 'stable' | 'insiders'; diff --git a/src/client/tensorBoard/nbextensionCodeLensProvider.ts b/src/client/tensorBoard/nbextensionCodeLensProvider.ts index 6d4c844cd392..7b9a116ee144 100644 --- a/src/client/tensorBoard/nbextensionCodeLensProvider.ts +++ b/src/client/tensorBoard/nbextensionCodeLensProvider.ts @@ -12,6 +12,7 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { containsNotebookExtension } from './helpers'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; @injectable() export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleActivationService { @@ -27,6 +28,9 @@ export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleA constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/serviceRegistry.ts b/src/client/tensorBoard/serviceRegistry.ts index 8d16766f70c5..dd193f528eea 100644 --- a/src/client/tensorBoard/serviceRegistry.ts +++ b/src/client/tensorBoard/serviceRegistry.ts @@ -10,6 +10,7 @@ import { TensorBoardPrompt } from './tensorBoardPrompt'; import { TensorBoardSessionProvider } from './tensorBoardSessionProvider'; import { TensorBoardNbextensionCodeLensProvider } from './nbextensionCodeLensProvider'; import { TerminalWatcher } from './terminalWatcher'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(TensorBoardSessionProvider, TensorBoardSessionProvider); @@ -32,4 +33,5 @@ export function registerTypes(serviceManager: IServiceManager): void { ); serviceManager.addBinding(TensorBoardNbextensionCodeLensProvider, IExtensionSingleActivationService); serviceManager.addSingleton(IExtensionSingleActivationService, TerminalWatcher); + serviceManager.addSingleton(TensorboardDependencyChecker, TensorboardDependencyChecker); } diff --git a/src/client/tensorBoard/tensorBoardFileWatcher.ts b/src/client/tensorBoard/tensorBoardFileWatcher.ts index 81c62f1f8de3..dccdb95290ec 100644 --- a/src/client/tensorBoard/tensorBoardFileWatcher.ts +++ b/src/client/tensorBoard/tensorBoardFileWatcher.ts @@ -8,6 +8,7 @@ import { IWorkspaceService } from '../common/application/types'; import { IDisposableRegistry } from '../common/types'; import { TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; @injectable() export class TensorBoardFileWatcher implements IExtensionSingleActivationService { @@ -24,6 +25,9 @@ export class TensorBoardFileWatcher implements IExtensionSingleActivationService ) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts b/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts index cac29b1d7e7a..d6dc8d7e82e5 100644 --- a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts +++ b/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts @@ -12,6 +12,7 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { containsTensorBoardImport } from './helpers'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; @injectable() export class TensorBoardImportCodeLensProvider implements IExtensionSingleActivationService { @@ -27,6 +28,9 @@ export class TensorBoardImportCodeLensProvider implements IExtensionSingleActiva constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/tensorBoardPrompt.ts b/src/client/tensorBoard/tensorBoardPrompt.ts index 1c03a696dc1d..d42101cb51d6 100644 --- a/src/client/tensorBoard/tensorBoardPrompt.ts +++ b/src/client/tensorBoard/tensorBoardPrompt.ts @@ -84,7 +84,7 @@ export class TensorBoardPrompt { } } - private isPromptEnabled(): boolean { + public isPromptEnabled(): boolean { return this.state.value; } diff --git a/src/client/tensorBoard/tensorBoardSession.ts b/src/client/tensorBoard/tensorBoardSession.ts index 1d24e8c313f7..fb54ad6f32e6 100644 --- a/src/client/tensorBoard/tensorBoardSession.ts +++ b/src/client/tensorBoard/tensorBoardSession.ts @@ -100,7 +100,10 @@ export class TensorBoardSession { private readonly globalMemento: IPersistentState, private readonly multiStepFactory: IMultiStepInputFactory, private readonly configurationService: IConfigurationService, - ) {} + ) { + this.disposables.push(this.onDidChangeViewStateEventEmitter); + this.disposables.push(this.onDidDisposeEventEmitter); + } public get onDidDispose(): Event { return this.onDidDisposeEventEmitter.event; @@ -189,10 +192,10 @@ export class TensorBoardSession { // to start a TensorBoard session. If the user has a torch import in // any of their open documents, also try to install the torch-tb-plugin // package, but don't block if installing that fails. - private async ensurePrerequisitesAreInstalled() { + public async ensurePrerequisitesAreInstalled(resource?: Uri): Promise { traceVerbose('Ensuring TensorBoard package is installed into active interpreter'); const interpreter = - (await this.interpreterService.getActiveInterpreter()) || + (await this.interpreterService.getActiveInterpreter(resource)) || (await this.commandManager.executeCommand('python.setInterpreter')); if (!interpreter) { return false; diff --git a/src/client/tensorBoard/tensorBoardSessionProvider.ts b/src/client/tensorBoard/tensorBoardSessionProvider.ts index 53878bd543c2..c81059654075 100644 --- a/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ b/src/client/tensorBoard/tensorBoardSessionProvider.ts @@ -22,8 +22,9 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardSession } from './tensorBoardSession'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; -const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; +export const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; @injectable() export class TensorBoardSessionProvider implements IExtensionSingleActivationService { @@ -58,6 +59,10 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer } public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } + this.disposables.push( this.commandManager.registerCommand( Commands.LaunchTensorBoard, diff --git a/src/client/tensorBoard/tensorBoardUsageTracker.ts b/src/client/tensorBoard/tensorBoardUsageTracker.ts index 99d82949dcfd..7c8ea7b00961 100644 --- a/src/client/tensorBoard/tensorBoardUsageTracker.ts +++ b/src/client/tensorBoard/tensorBoardUsageTracker.ts @@ -12,6 +12,7 @@ import { getDocumentLines } from '../telemetry/importTracker'; import { TensorBoardEntrypointTrigger } from './constants'; import { containsTensorBoardImport } from './helpers'; import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; const testExecution = isTestExecution(); @@ -28,6 +29,9 @@ export class TensorBoardUsageTracker implements IExtensionSingleActivationServic ) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } if (testExecution) { await this.activateInternal(); } else { diff --git a/src/client/tensorBoard/tensorboarExperiment.ts b/src/client/tensorBoard/tensorboarExperiment.ts new file mode 100644 index 000000000000..25eac8db71da --- /dev/null +++ b/src/client/tensorBoard/tensorboarExperiment.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { extensions } from 'vscode'; + +export function useNewTensorboardExtension(): boolean { + return !!extensions.getExtension('ms-toolsai.tensorboard'); +} diff --git a/src/client/tensorBoard/tensorboardDependencyChecker.ts b/src/client/tensorBoard/tensorboardDependencyChecker.ts new file mode 100644 index 000000000000..5c377e1d2455 --- /dev/null +++ b/src/client/tensorBoard/tensorboardDependencyChecker.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri, ViewColumn } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { + IInstaller, + IPersistentState, + IPersistentStateFactory, + IConfigurationService, + IDisposable, +} from '../common/types'; +import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; +import { IInterpreterService } from '../interpreter/contracts'; +import { TensorBoardSession } from './tensorBoardSession'; +import { disposeAll } from '../common/utils/resourceLifecycle'; +import { PREFERRED_VIEWGROUP } from './tensorBoardSessionProvider'; + +@injectable() +export class TensorboardDependencyChecker { + private preferredViewGroupMemento: IPersistentState; + + constructor( + @inject(IInstaller) private readonly installer: IInstaller, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, + @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, + @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + ) { + this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( + PREFERRED_VIEWGROUP, + ViewColumn.Active, + ); + } + + public async ensureDependenciesAreInstalled(resource?: Uri): Promise { + const disposables: IDisposable[] = []; + const newSession = new TensorBoardSession( + this.installer, + this.interpreterService, + this.workspaceService, + this.pythonExecFactory, + this.commandManager, + disposables, + this.applicationShell, + this.preferredViewGroupMemento, + this.multiStepFactory, + this.configurationService, + ); + const result = await newSession.ensurePrerequisitesAreInstalled(resource); + disposeAll(disposables); + return result; + } +} diff --git a/src/client/tensorBoard/tensorboardIntegration.ts b/src/client/tensorBoard/tensorboardIntegration.ts new file mode 100644 index 000000000000..74f69afab84f --- /dev/null +++ b/src/client/tensorBoard/tensorboardIntegration.ts @@ -0,0 +1,102 @@ +/* eslint-disable comma-dangle */ + +/* eslint-disable implicit-arrow-linebreak */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Extension, Uri, commands } from 'vscode'; +import { IWorkspaceService } from '../common/application/types'; +import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; +import { IDisposableRegistry, IExtensions, Resource } from '../common/types'; +import { IEnvironmentActivationService } from '../interpreter/activation/types'; +import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; + +type PythonApiForTensorboardExtension = { + /** + * Gets activated env vars for the active Python Environment for the given resource. + */ + getActivatedEnvironmentVariables(resource: Resource): Promise; + /** + * Ensures that the dependencies required for TensorBoard are installed in Active Environment for the given resource. + */ + ensureDependenciesAreInstalled(resource?: Uri): Promise; + /** + * Whether to allow displaying tensorboard prompt. + */ + isPromptEnabled(): boolean; +}; + +type TensorboardExtensionApi = { + /** + * Registers python extension specific parts with the tensorboard extension + */ + registerPythonApi(interpreterService: PythonApiForTensorboardExtension): void; +}; + +@injectable() +export class TensorboardExtensionIntegration { + private tensorboardExtension: Extension | undefined; + + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(TensorboardDependencyChecker) private readonly dependencyChcker: TensorboardDependencyChecker, + @inject(TensorBoardPrompt) private readonly tensorBoardPrompt: TensorBoardPrompt, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + ) { + this.hideCommands(); + extensions.onDidChange(this.hideCommands, this, disposables); + } + + public registerApi(tensorboardExtensionApi: TensorboardExtensionApi): TensorboardExtensionApi | undefined { + this.hideCommands(); + if (!this.workspaceService.isTrusted) { + this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi)); + return undefined; + } + tensorboardExtensionApi.registerPythonApi({ + getActivatedEnvironmentVariables: async (resource: Resource) => + this.envActivation.getActivatedEnvironmentVariables(resource, undefined, true), + ensureDependenciesAreInstalled: async (resource?: Uri): Promise => + this.dependencyChcker.ensureDependenciesAreInstalled(resource), + isPromptEnabled: () => this.tensorBoardPrompt.isPromptEnabled(), + }); + return undefined; + } + + public hideCommands(): void { + if (this.extensions.getExtension(TENSORBOARD_EXTENSION_ID)) { + console.error('TensorBoard extension is installed'); + void commands.executeCommand('setContext', 'python.tensorboardExtInstalled', true); + } else { + console.error('TensorBoard extension not installed'); + } + } + + public async integrateWithTensorboardExtension(): Promise { + const api = await this.getExtensionApi(); + if (api) { + this.registerApi(api); + } + } + + private async getExtensionApi(): Promise { + if (!this.tensorboardExtension) { + const extension = this.extensions.getExtension(TENSORBOARD_EXTENSION_ID); + if (!extension) { + return undefined; + } + await extension.activate(); + if (extension.isActive) { + this.tensorboardExtension = extension; + return this.tensorboardExtension.exports; + } + } else { + return this.tensorboardExtension.exports; + } + return undefined; + } +} diff --git a/src/client/tensorBoard/terminalWatcher.ts b/src/client/tensorBoard/terminalWatcher.ts index 5aadc12dc4c0..30ccf7e1726a 100644 --- a/src/client/tensorBoard/terminalWatcher.ts +++ b/src/client/tensorBoard/terminalWatcher.ts @@ -4,6 +4,7 @@ import { IExtensionSingleActivationService } from '../activation/types'; import { IDisposable, IDisposableRegistry } from '../common/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; // Every 5 min look, through active terminals to see if any are running `tensorboard` @injectable() @@ -15,6 +16,9 @@ export class TerminalWatcher implements IExtensionSingleActivationService, IDisp constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } const handle = setInterval(() => { // When user runs a command in VSCode terminal, the terminal's name // becomes the program that is currently running. Since tensorboard diff --git a/src/client/tensorBoard/types.ts b/src/client/tensorBoard/types.ts index 6e2c274d63f4..a11659015da8 100644 --- a/src/client/tensorBoard/types.ts +++ b/src/client/tensorBoard/types.ts @@ -1,9 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event } from 'vscode'; +import { Event, Uri } from 'vscode'; export const ITensorBoardImportTracker = Symbol('ITensorBoardImportTracker'); export interface ITensorBoardImportTracker { onDidImportTensorBoard: Event; } + +export const ITensorboardDependencyChecker = Symbol('ITensorboardDependencyChecker'); +export interface ITensorboardDependencyChecker { + ensureDependenciesAreInstalled(resource?: Uri): Promise; +} diff --git a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts b/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts index b6efad083a57..ff187dd2afc1 100644 --- a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts +++ b/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts @@ -1,9 +1,11 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; +import { anything, reset, when } from 'ts-mockito'; import { TensorBoardUsageTracker } from '../../client/tensorBoard/tensorBoardUsageTracker'; import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; import { MockDocumentManager } from '../mocks/mockDocumentManager'; import { createTensorBoardPromptWithMocks } from './helpers'; +import { mockedVSCodeNamespaces } from '../vscode-mock'; suite('TensorBoard usage tracker', () => { let documentManager: MockDocumentManager; @@ -11,6 +13,11 @@ suite('TensorBoard usage tracker', () => { let prompt: TensorBoardPrompt; let showNativeTensorBoardPrompt: sinon.SinonSpy; + suiteSetup(() => { + reset(mockedVSCodeNamespaces.extensions); + when(mockedVSCodeNamespaces.extensions?.getExtension(anything())).thenReturn(undefined); + }); + suiteTeardown(() => reset(mockedVSCodeNamespaces.extensions)); setup(() => { documentManager = new MockDocumentManager(); prompt = createTensorBoardPromptWithMocks(); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 44518e7575a7..ec44d302d063 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -3,21 +3,21 @@ 'use strict'; -import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; import * as vscodeMocks from './mocks/vsc'; import { vscMockTelemetryReporter } from './mocks/vsc/telemetryReporter'; +import { anything, instance, mock, when } from 'ts-mockito'; const Module = require('module'); type VSCode = typeof vscode; const mockedVSCode: Partial = {}; -export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: TypeMoq.IMock } = {}; +export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: VSCode[P] } = {}; const originalLoad = Module._load; function generateMock(name: K): void { - const mockedObj = TypeMoq.Mock.ofType(); - (mockedVSCode as any)[name] = mockedObj.object; + const mockedObj = mock(); + (mockedVSCode as any)[name] = instance(mockedObj); mockedVSCodeNamespaces[name] = mockedObj as any; } @@ -35,15 +35,26 @@ export function initialize() { generateMock('window'); generateMock('commands'); generateMock('languages'); + generateMock('extensions'); generateMock('env'); generateMock('debug'); generateMock('scm'); - generateNotebookMocks(); + generateMock('notebooks'); // Use mock clipboard fo testing purposes. const clipboard = new MockClipboard(); - mockedVSCodeNamespaces.env?.setup((e) => e.clipboard).returns(() => clipboard); - mockedVSCodeNamespaces.env?.setup((e) => e.appName).returns(() => 'Insider'); + when(mockedVSCodeNamespaces.env!.clipboard).thenReturn(clipboard); + when(mockedVSCodeNamespaces.env!.appName).thenReturn('Insider'); + + // This API is used in src/client/telemetry/telemetry.ts + const extension = mock>(); + const packageJson = mock(); + const contributes = mock(); + when(extension.packageJSON).thenReturn(instance(packageJson)); + when(packageJson.contributes).thenReturn(instance(contributes)); + when(contributes.debuggers).thenReturn([{ aiKey: '' }]); + when(mockedVSCodeNamespaces.extensions!.getExtension(anything())).thenReturn(instance(extension)); + when(mockedVSCodeNamespaces.extensions!.all).thenReturn([]); // When upgrading to npm 9-10, this might have to change, as we could have explicit imports (named imports). Module._load = function (request: any, _parent: any) { @@ -122,21 +133,3 @@ mockedVSCode.LogLevel = vscodeMocks.LogLevel; (mockedVSCode as any).ProtocolTypeHierarchyItem = vscodeMocks.vscMockExtHostedTypes.ProtocolTypeHierarchyItem; (mockedVSCode as any).CancellationError = vscodeMocks.vscMockExtHostedTypes.CancellationError; (mockedVSCode as any).LSPCancellationError = vscodeMocks.vscMockExtHostedTypes.LSPCancellationError; - -// This API is used in src/client/telemetry/telemetry.ts -const extensions = TypeMoq.Mock.ofType(); -extensions.setup((e) => e.all).returns(() => []); -const extension = TypeMoq.Mock.ofType>(); -const packageJson = TypeMoq.Mock.ofType(); -const contributes = TypeMoq.Mock.ofType(); -extension.setup((e) => e.packageJSON).returns(() => packageJson.object); -packageJson.setup((p) => p.contributes).returns(() => contributes.object); -contributes.setup((p) => p.debuggers).returns(() => [{ aiKey: '' }]); -extensions.setup((e) => e.getExtension(TypeMoq.It.isAny())).returns(() => extension.object); -mockedVSCode.extensions = extensions.object; - -function generateNotebookMocks() { - const mockedObj = TypeMoq.Mock.ofType<{}>(); - (mockedVSCode as any).notebook = mockedObj.object; - (mockedVSCodeNamespaces as any).notebook = mockedObj as any; -} From 65c8ac6e3f272d76c9775ad1163a18c61d473119 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 11 Oct 2023 21:43:50 -0700 Subject: [PATCH 328/336] Remove formatting settings (#22202) Closes https://github.com/microsoft/vscode-python/issues/22183 --- README.md | 9 +-- build/test-requirements.txt | 5 -- package.json | 71 ------------------- package.nls.json | 21 ------ resources/report_issue_user_settings.json | 9 --- src/client/common/configSettings.ts | 31 -------- src/client/common/types.ts | 9 --- src/client/common/utils/localize.ts | 20 ------ src/client/telemetry/constants.ts | 2 - src/client/telemetry/index.ts | 39 +--------- src/test/.vscode/settings.json | 1 - .../configSettings.unit.test.ts | 61 ---------------- src/test/common/productsToTest.ts | 16 +---- src/testMultiRootWkspc/multi.code-workspace | 1 - 14 files changed, 6 insertions(+), 289 deletions(-) diff --git a/README.md b/README.md index 0a8766f086af..8029aa096587 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/L | `Python: Select Interpreter` | Switch between Python interpreters, versions, and environments. | | `Python: Start REPL` | Start an interactive Python REPL using the selected interpreter in the VS Code terminal. | | `Python: Run Python File in Terminal` | Runs the active Python file in the VS Code terminal. You can also run a Python file by right-clicking on the file and selecting `Run Python File in Terminal`. | -| `Format Document` | Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/editing#_formatting) in the `settings.json` file. | +| `Format Document` | Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/formatting) in the `settings.json` file. | | `Python: Configure Tests` | Select a test framework and configure it to display the Test Explorer. | To see all available Python commands, open the Command Palette and type `Python`. For Jupyter extension commands, just type `Jupyter`. @@ -71,16 +71,11 @@ Learn more about the rich features of the Python extension: - [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense): Edit your code with auto-completion, code navigation, syntax checking and more - [Linting](https://code.visualstudio.com/docs/python/linting): Get additional code analysis with Pylint, Flake8 and more -- [Code formatting](https://code.visualstudio.com/docs/python/editing#_formatting): Format your code with black, autopep or yapf - +- [Code formatting](https://code.visualstudio.com/docs/python/formatting): Format your code with black, autopep or yapf - [Debugging](https://code.visualstudio.com/docs/python/debugging): Debug your Python scripts, web apps, remote or multi-threaded processes - - [Testing](https://code.visualstudio.com/docs/python/unit-testing): Run and debug tests through the Test Explorer with unittest or pytest. - - [Jupyter Notebooks](https://code.visualstudio.com/docs/python/jupyter-support): Create and edit Jupyter Notebooks, add and run code cells, render plots, visualize variables through the variable explorer, visualize dataframes with the data viewer, and more - - [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments - - [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). diff --git a/build/test-requirements.txt b/build/test-requirements.txt index 433bd0f86682..0650e86fb3d3 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -1,13 +1,8 @@ # pin setoptconf to prevent issue with 'use_2to3' setoptconf==0.3.0 -# Install flake8 first, as both flake8 and autopep8 require pycodestyle, -# but flake8 has a tighter pinning. flake8 -autopep8 bandit -black -yapf pylint pycodestyle pydocstyle diff --git a/package.json b/package.json index 88578a687853..20401cc43762 100644 --- a/package.json +++ b/package.json @@ -577,77 +577,6 @@ "type": "array", "uniqueItems": true }, - "python.formatting.autopep8Args": { - "default": [], - "description": "%python.formatting.autopep8Args.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.formatting.autopep8Args.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.autopep8Args.deprecationMessage%" - }, - "python.formatting.autopep8Path": { - "default": "autopep8", - "description": "%python.formatting.autopep8Path.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.autopep8Path.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.autopep8Path.deprecationMessage%" - }, - "python.formatting.blackArgs": { - "default": [], - "description": "%python.formatting.blackArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.formatting.blackArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.blackArgs.deprecationMessage%" - }, - "python.formatting.blackPath": { - "default": "black", - "description": "%python.formatting.blackPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.blackPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.blackPath.deprecationMessage%" - }, - "python.formatting.provider": { - "default": "autopep8", - "description": "%python.formatting.provider.description%", - "enum": [ - "autopep8", - "black", - "none", - "yapf" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.provider.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.provider.deprecationMessage%" - }, - "python.formatting.yapfArgs": { - "default": [], - "description": "%python.formatting.yapfArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.formatting.yapfArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.yapfArgs.deprecationMessage%" - }, - "python.formatting.yapfPath": { - "default": "yapf", - "description": "%python.formatting.yapfPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.yapfPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.yapfPath.deprecationMessage%" - }, "python.globalModuleInstallation": { "default": false, "description": "%python.globalModuleInstallation.description%", diff --git a/package.nls.json b/package.nls.json index 5687e51ab9df..f843399e09c5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -42,27 +42,6 @@ "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", "python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.", - "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.autopep8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.autopep8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.autopep8Path.description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", - "python.formatting.autopep8Path.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.autopep8Path.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.blackArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.blackArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.blackPath.description": "Path to Black, you can use a custom version of Black by modifying this setting to include the full path.", - "python.formatting.blackPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.blackPath.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.provider.description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", - "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension.
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.yapfArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.yapfArgs.markdownDeprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfArgs.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.yapfPath.description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", - "python.formatting.yapfPath.markdownDeprecationMessage": "Yapf support will soon be deprecated.
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfPath.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServer.description": "Defines type of the language server.", "python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.", diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index 677e58d83f21..eea4ca007da6 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -69,15 +69,6 @@ "memory": true, "symbolsHierarchyDepthLimit": false }, - "formatting": { - "autopep8Args": "placeholder", - "autopep8Path": "placeholder", - "provider": true, - "blackArgs": "placeholder", - "blackPath": "placeholder", - "yapfArgs": "placeholder", - "yapfPath": "placeholder" - }, "testing": { "cwd": "placeholder", "debugPort": true, diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 3e4b75b8b087..cadc1515f7e6 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -27,7 +27,6 @@ import { IAutoCompleteSettings, IDefaultLanguageServer, IExperiments, - IFormattingSettings, IInterpreterPathService, IInterpreterSettings, ILintingSettings, @@ -109,8 +108,6 @@ export class PythonSettings implements IPythonSettings { public linting!: ILintingSettings; - public formatting!: IFormattingSettings; - public autoComplete!: IAutoCompleteSettings; public tensorBoard: ITensorBoardSettings | undefined; @@ -395,34 +392,6 @@ export class PythonSettings implements IPythonSettings { this.linting.cwd = getAbsolutePath(systemVariables.resolveAny(this.linting.cwd), workspaceRoot); } - const formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; - if (this.formatting) { - Object.assign(this.formatting, formattingSettings); - } else { - this.formatting = formattingSettings; - } - // Support for travis. - this.formatting = this.formatting - ? this.formatting - : { - autopep8Args: [], - autopep8Path: 'autopep8', - provider: 'autopep8', - blackArgs: [], - blackPath: 'black', - yapfArgs: [], - yapfPath: 'yapf', - }; - this.formatting.autopep8Path = getAbsolutePath( - systemVariables.resolveAny(this.formatting.autopep8Path), - workspaceRoot, - ); - this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); - this.formatting.blackPath = getAbsolutePath( - systemVariables.resolveAny(this.formatting.blackPath), - workspaceRoot, - ); - const testSettings = systemVariables.resolveAny(pythonSettings.get('testing'))!; if (this.testing) { Object.assign(this.testing, testSettings); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 8b90443703c6..a33f437622fa 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -258,15 +258,6 @@ export interface ILintingSettings { banditArgs: string[]; banditPath: string; } -export interface IFormattingSettings { - readonly provider: string; - autopep8Path: string; - readonly autopep8Args: string[]; - blackPath: string; - readonly blackArgs: string[]; - yapfPath: string; - readonly yapfArgs: string[]; -} export interface ITerminalSettings { readonly executeInFileDir: boolean; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index c6086071363f..bbb55a79ce40 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -516,24 +516,4 @@ export namespace ToolsExtensions { ); export const installPylintExtension = l10n.t('Install Pylint extension'); export const installFlake8Extension = l10n.t('Install Flake8 extension'); - - export const selectBlackFormatterPrompt = l10n.t( - 'You have the Black formatter extension installed, would you like to use that as the default formatter?', - ); - - export const selectAutopep8FormatterPrompt = l10n.t( - 'You have the Autopep8 formatter extension installed, would you like to use that as the default formatter?', - ); - - export const selectMultipleFormattersPrompt = l10n.t( - 'You have multiple formatters installed, would you like to select one as the default formatter?', - ); - - export const installBlackFormatterPrompt = l10n.t( - 'You triggered formatting with Black, would you like to install one of our new formatter extensions? This will also set it as the default formatter for Python.', - ); - - export const installAutopep8FormatterPrompt = l10n.t( - 'You triggered formatting with Autopep8, would you like to install one of our new formatter extension? This will also set it as the default formatter for Python.', - ); } diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index c680b91094cb..301502a0f6fa 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -4,8 +4,6 @@ 'use strict'; export enum EventName { - FORMAT_SORT_IMPORTS = 'FORMAT.SORT_IMPORTS', - FORMAT = 'FORMAT.FORMAT', FORMAT_ON_TYPE = 'FORMAT.FORMAT_ON_TYPE', EDITOR_LOAD = 'EDITOR.LOAD', LINTING = 'LINTING', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 95496c828018..f69da6046254 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -859,33 +859,7 @@ export interface IEventNamePropertyMapping { */ scope: 'file' | 'selection'; }; - /** - * Telemetry event sent with details when formatting a document - */ - /* __GDPR__ - "format.format" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "karthiknadig" }, - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "hascustomargs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "formatselection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.FORMAT]: { - /** - * Tool being used to format - */ - tool: 'autopep8' | 'black' | 'yapf'; - /** - * If arguments for formatter is provided in resource settings - */ - hasCustomArgs: boolean; - /** - * Carries `true` when formatting a selection of text, `false` otherwise - */ - formatSelection: boolean; - }; + /** * Telemetry event sent with the value of setting 'Format on type' */ @@ -902,16 +876,6 @@ export interface IEventNamePropertyMapping { */ enabled: boolean; }; - /** - * Telemetry event sent when sorting imports using formatter - */ - /* __GDPR__ - "format.sort_imports" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "karthiknadig" }, - "originaleventname" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.FORMAT_SORT_IMPORTS]: never | undefined; /** * Telemetry event sent with details when tracking imports @@ -921,7 +885,6 @@ export interface IEventNamePropertyMapping { "hashedname" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" } } */ - [EventName.HASHED_PACKAGE_NAME]: { /** * Hash of the package name diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index 771962b5a909..faeb48ffa29c 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -11,7 +11,6 @@ "python.linting.pylamaEnabled": false, "python.linting.mypyEnabled": false, "python.linting.banditEnabled": false, - "python.formatting.provider": "yapf", // Don't set this to `Pylance`, for CI we want to use the LS that ships with the extension. "python.languageServer": "Jedi", "python.pythonPath": "C:\\GIT\\s p\\vscode-python\\.venv\\Scripts\\python.exe" diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 113770122fbc..e43ac7b7fbd8 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -19,7 +19,6 @@ import { PersistentStateFactory } from '../../../client/common/persistentState'; import { IAutoCompleteSettings, IExperiments, - IFormattingSettings, IInterpreterSettings, ILintingSettings, ITerminalSettings, @@ -117,7 +116,6 @@ suite('Python Settings', async () => { // complex settings config.setup((c) => c.get('interpreter')).returns(() => sourceSettings.interpreter); config.setup((c) => c.get('linting')).returns(() => sourceSettings.linting); - config.setup((c) => c.get('formatting')).returns(() => sourceSettings.formatting); config.setup((c) => c.get('autoComplete')).returns(() => sourceSettings.autoComplete); config.setup((c) => c.get('testing')).returns(() => sourceSettings.testing); config.setup((c) => c.get('terminal')).returns(() => sourceSettings.terminal); @@ -264,63 +262,4 @@ suite('Python Settings', async () => { test('Experiments (not enabled)', () => testExperiments(false)); test('Experiments (enabled)', () => testExperiments(true)); - - test('Formatter Paths and args', () => { - expected.pythonPath = 'python3'; - - expected.formatting = { - autopep8Args: ['1', '2'], - autopep8Path: 'one', - blackArgs: ['3', '4'], - blackPath: 'two', - yapfArgs: ['5', '6'], - yapfPath: 'three', - provider: '', - }; - expected.formatting.blackPath = 'spam'; - initializeConfig(expected); - config - .setup((c) => c.get('formatting')) - .returns(() => expected.formatting) - .verifiable(TypeMoq.Times.once()); - - settings.update(config.object); - - for (const key of Object.keys(expected.formatting)) { - expect((settings.formatting as any)[key]).to.be.deep.equal((expected.formatting as any)[key]); - } - config.verifyAll(); - }); - test('Formatter Paths (paths relative to home)', () => { - expected.pythonPath = 'python3'; - - expected.formatting = { - autopep8Args: [], - autopep8Path: path.join('~', 'one'), - blackArgs: [], - blackPath: path.join('~', 'two'), - yapfArgs: [], - yapfPath: path.join('~', 'three'), - provider: '', - }; - expected.formatting.blackPath = 'spam'; - initializeConfig(expected); - config - .setup((c) => c.get('formatting')) - .returns(() => expected.formatting) - .verifiable(TypeMoq.Times.once()); - - settings.update(config.object); - - for (const key of Object.keys(expected.formatting)) { - if (!key.endsWith('path')) { - continue; - } - - const expectedPath = untildify((expected.formatting as any)[key]); - - expect((settings.formatting as any)[key]).to.be.equal(expectedPath); - } - config.verifyAll(); - }); }); diff --git a/src/test/common/productsToTest.ts b/src/test/common/productsToTest.ts index 861bab898509..e82d12bbd9eb 100644 --- a/src/test/common/productsToTest.ts +++ b/src/test/common/productsToTest.ts @@ -7,18 +7,8 @@ import { getNamesAndValues } from '../../client/common/utils/enum'; export function getProductsForInstallerTests(): { name: string; value: Product }[] { return getNamesAndValues(Product).filter( (p) => - ![ - 'pylint', - 'flake8', - 'pycodestyle', - 'pylama', - 'prospector', - 'pydocstyle', - 'yapf', - 'autopep8', - 'mypy', - 'black', - 'bandit', - ].includes(p.name), + !['pylint', 'flake8', 'pycodestyle', 'pylama', 'prospector', 'pydocstyle', 'mypy', 'bandit'].includes( + p.name, + ), ); } diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace index 9d5c8ac77475..51d218783041 100644 --- a/src/testMultiRootWkspc/multi.code-workspace +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -37,7 +37,6 @@ "python.linting.pylintEnabled": true, "python.linting.pycodestyleEnabled": false, "python.linting.prospectorEnabled": false, - "python.formatting.provider": "yapf", "python.linting.lintOnSave": false, "python.linting.enabled": true, "python.pythonPath": "python" From 9b5f58afc0acacacd45c035c4e1a78622944407d Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 12 Oct 2023 08:53:10 -0700 Subject: [PATCH 329/336] Add logging for failure to retrieve environment variables, testing rewrite (#22203) --- pythonFiles/unittestadapter/discovery.py | 12 +++++++++--- pythonFiles/unittestadapter/execution.py | 19 ++++++++++++++++--- pythonFiles/vscode_pytest/__init__.py | 18 +++++++++++++++++- .../vscode_pytest/run_pytest_script.py | 2 ++ .../pytest/pytestDiscoveryAdapter.ts | 2 +- .../pytest/pytestExecutionAdapter.ts | 1 + 6 files changed, 46 insertions(+), 8 deletions(-) diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index 7e07e45d1202..7525f33cda61 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -19,7 +19,7 @@ # If I use from utils then there will be an import error in test_discovery.py. from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args -DEFAULT_PORT = "45454" +DEFAULT_PORT = 45454 class PayloadDict(TypedDict): @@ -121,12 +121,18 @@ def post_response( start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) - # Perform test discovery. testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) testUuid = os.environ.get("TEST_UUID") - # Post this discovery payload. + if testPort is DEFAULT_PORT: + print( + "Error[vscode-unittest]: TEST_PORT is not set.", + " TEST_UUID = ", + testUuid, + ) if testUuid is not None: + # Perform test discovery. payload = discover_tests(start_dir, pattern, top_level_dir, testUuid) + # Post this discovery payload. post_response(payload, testPort, testUuid) # Post EOT token. eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index e5758118b951..2a22bfff3486 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -21,14 +21,13 @@ from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args -DEFAULT_PORT = "45454" - ErrorType = Union[ Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] ] testPort = 0 testUuid = 0 START_DIR = "" +DEFAULT_PORT = 45454 class TestOutcomeEnum(str, enum.Enum): @@ -269,7 +268,8 @@ def post_response( run_test_ids_port_int = ( int(run_test_ids_port) if run_test_ids_port is not None else 0 ) - + if run_test_ids_port_int == 0: + print("Error[vscode-unittest]: RUN_TEST_IDS_PORT env var is not set.") # get data from socket test_ids_from_buffer = [] try: @@ -303,6 +303,19 @@ def post_response( testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) testUuid = os.environ.get("TEST_UUID") + if testPort is DEFAULT_PORT: + print( + "Error[vscode-unittest]: TEST_PORT is not set.", + " TEST_UUID = ", + testUuid, + ) + if testUuid is None: + print( + "Error[vscode-unittest]: TEST_UUID is not set.", + " TEST_PORT = ", + testPort, + ) + testUuid = "unknown" if test_ids_from_buffer: # Perform test execution. payload = run_tests( diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 2fab4d77c2f8..8349e1aa893d 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -20,6 +20,8 @@ from testing_tools import socket_manager from typing_extensions import Literal, TypedDict +DEFAULT_PORT = 45454 + class TestData(TypedDict): """A general class that all test objects inherit from.""" @@ -683,8 +685,22 @@ def send_post_request( payload -- the payload data to be sent. cls_encoder -- a custom encoder if needed. """ - testPort = os.getenv("TEST_PORT", 45454) + testPort = os.getenv("TEST_PORT") testUuid = os.getenv("TEST_UUID") + if testPort is None: + print( + "Error[vscode-pytest]: TEST_PORT is not set.", + " TEST_UUID = ", + testUuid, + ) + testPort = DEFAULT_PORT + if testUuid is None: + print( + "Error[vscode-pytest]: TEST_UUID is not set.", + " TEST_PORT = ", + testPort, + ) + testUuid = "unknown" addr = ("localhost", int(testPort)) global __socket diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py index c3720c8ab8d0..e60ee91f096e 100644 --- a/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -28,6 +28,8 @@ run_test_ids_port_int = ( int(run_test_ids_port) if run_test_ids_port is not None else 0 ) + if run_test_ids_port_int == 0: + print("Error[vscode-pytest]: RUN_TEST_IDS_PORT env var is not set.") test_ids_from_buffer = [] try: client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 92bd9f04834e..09ca36849000 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -78,7 +78,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { mutableEnv.PYTHONPATH = pythonPathCommand; mutableEnv.TEST_UUID = uuid.toString(); mutableEnv.TEST_PORT = this.testServer.getPort().toString(); - + traceInfo(`All environment variables set for pytest discovery: ${JSON.stringify(mutableEnv)}`); const spawnOptions: SpawnOptions = { cwd, throwOnStdErr: true, diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 5c04aabab845..fd61251d33fc 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -140,6 +140,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // add port with run test ids to env vars const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); mutableEnv.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); + traceInfo(`All environment variables set for pytest execution: ${JSON.stringify(mutableEnv)}`); const spawnOptions: SpawnOptions = { cwd, From ec001a0b1503555e685996baab8fda4f0648c454 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 12 Oct 2023 14:11:15 -0700 Subject: [PATCH 330/336] Fix for webpack warning with LSP types (#22211) --- build/webpack/webpack.extension.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/build/webpack/webpack.extension.config.js b/build/webpack/webpack.extension.config.js index f496aa32ee26..7003ffa277d2 100644 --- a/build/webpack/webpack.extension.config.js +++ b/build/webpack/webpack.extension.config.js @@ -69,6 +69,7 @@ const config = { resolve: { extensions: ['.ts', '.js'], plugins: [new tsconfig_paths_webpack_plugin.TsconfigPathsPlugin({ configFile: configFileName })], + conditionNames: ['require', 'node'], }, output: { filename: '[name].js', From 6c23e4335db10e900ea0ca2402e267322c3a2e69 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:44:50 -0700 Subject: [PATCH 331/336] Handle white spaces for list along with dictionary (#22209) Legacy normalization script leaves unnecessary white spaces for dictionary as well as list. While there are multiple correct usage of it such as for after a function, for more intuitive REPL experience. We want to keep previous normalization style and white space format EXCEPT for dictionary and list case. Dictionary case is handled, but this is the PR to handle the elimination of extra white spaces for list as well. Closes: #22208 --- pythonFiles/normalizeSelection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonFiles/normalizeSelection.py b/pythonFiles/normalizeSelection.py index 0ac47ab5dc3b..7ace42daa901 100644 --- a/pythonFiles/normalizeSelection.py +++ b/pythonFiles/normalizeSelection.py @@ -119,7 +119,7 @@ def normalize_lines(selection): # Insert a newline between each top-level statement, and append a newline to the selection. source = "\n".join(statements) + "\n" - if selection[-2] == "}": + if selection[-2] == "}" or selection[-2] == "]": source = source[:-1] except Exception: # If there's a problem when parsing statements, From bdb8efb9dd20cefd56a11687ed7f7dfc89f9f15d Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 12 Oct 2023 17:18:49 -0700 Subject: [PATCH 332/336] Try using `import` in webpack condition names (#22212) --- build/webpack/webpack.extension.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/webpack/webpack.extension.config.js b/build/webpack/webpack.extension.config.js index 7003ffa277d2..a33508e5d96a 100644 --- a/build/webpack/webpack.extension.config.js +++ b/build/webpack/webpack.extension.config.js @@ -69,7 +69,7 @@ const config = { resolve: { extensions: ['.ts', '.js'], plugins: [new tsconfig_paths_webpack_plugin.TsconfigPathsPlugin({ configFile: configFileName })], - conditionNames: ['require', 'node'], + conditionNames: ['import', 'require', 'node'], }, output: { filename: '[name].js', From 76ae73a46bea7e722bf43e4d5550b3d895066d90 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 12 Oct 2023 21:52:38 -0700 Subject: [PATCH 333/336] Skip setting `PYTHONUTF8` when activating terminals (#22213) Closes https://github.com/microsoft/vscode-python/issues/22205 --- .../activation/terminalEnvVarCollectionService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index c11ec221d4d7..92e97c95e468 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -394,7 +394,12 @@ function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariabl } function shouldSkip(env: string) { - return ['_', 'SHLVL'].includes(env); + return [ + '_', + 'SHLVL', + // Even though this maybe returned, setting it can result in output encoding errors in terminal. + 'PYTHONUTF8', + ].includes(env); } function getPromptForEnv(interpreter: PythonEnvironment | undefined) { From eada0f1ab940729ae335389c34899d64b56edd35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 10:32:09 -0700 Subject: [PATCH 334/336] Bump microvenv from 2023.2.0 to 2023.3.post1 (#22204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [microvenv](https://github.com/brettcannon/microvenv) from 2023.2.0 to 2023.3.post1.
Release notes

Sourced from microvenv's releases.

2023.3.post1

What's Changed

⚠️ Breaking Changes

🎉 New Features

Full Changelog: https://github.com/brettcannon/microvenv/compare/v2023.2.0...v2023.3.post1

Commits
  • bf19f92 Update the docs due to Windows support
  • 0c5436d Fix docs.yml
  • 00d43b4 Update the version for release
  • 2e8d62e Add a release.yml workflow
  • 7b9ca8a Add support for Windows (except for create()) (#55)
  • 7085e42 Drop CI path requirements
  • f9dc600 Add mypy's stubtest to linting
  • 54e08f8 Clarify that _create.py is self-contained
  • 3145fcb Merge branch 'main' of github.com:brettcannon/microvenv
  • 6c0529e Drop the static HTML directory
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=microvenv&package-manager=pip&previous-version=2023.2.0&new-version=2023.3.post1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 205b9fc4804c..31765898ab59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,9 @@ importlib-metadata==6.7.0 \ --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 # via -r requirements.in -microvenv==2023.2.0 \ - --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ - --hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3 +microvenv==2023.3.post1 \ + --hash=sha256:67f0a48511cf16d6a2a45137175d0ddc36a657b91459b598cfbe976ef2afd596 \ + --hash=sha256:6e8c80ccfe813b00b77ab9cc2e5af3fd44e2fe540df176509fda97123f8b8290 # via -r requirements.in packaging==23.2 \ --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ From ed155afa4bf6acdbd0341d093b5a00e13237985e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 13 Oct 2023 16:24:39 -0700 Subject: [PATCH 335/336] remove asserts from catchable code for testing (#22210) some asserts were inside functions / mocking and with this then the extension code catches the exception and doesn't error out as the test. Bring the asserts out of the functions into the test so the asserts work as expected. --- .../testing/common/testingAdapter.test.ts | 50 ++++++++++++------- .../testing/common/testingPayloadsEot.test.ts | 14 ++++-- .../testController/server.unit.test.ts | 7 ++- .../testExecutionAdapter.unit.test.ts | 18 +++++-- 4 files changed, 62 insertions(+), 27 deletions(-) diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 519a60e3f0f7..a9ed25194fa9 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -653,17 +653,21 @@ suite('End to End Tests: test adapters', () => { if (data.error === undefined) { // Dereference a NULL pointer const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); - assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); - } else { - assert.ok(data.error, "Expected errors in 'error' field"); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected test to have a null pointer'; + } + } else if (data.error.length === 0) { + failureOccurred = true; + failureMsg = "Expected errors in 'error' field"; } } else { const indexOfTest = JSON.stringify(data.tests).search('error'); - assert.notDeepEqual( - indexOfTest, - -1, - 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', - ); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.'; + } } } catch (err) { failureMsg = err ? (err as Error).toString() : ''; @@ -705,22 +709,32 @@ suite('End to End Tests: test adapters', () => { if (data.error === undefined) { // Dereference a NULL pointer const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); - assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); - } else { - assert.ok(data.error, "Expected errors in 'error' field"); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected test to have a null pointer'; + } + } else if (data.error.length === 0) { + failureOccurred = true; + failureMsg = "Expected errors in 'error' field"; } } else { const indexOfTest = JSON.stringify(data.result).search('error'); - assert.notDeepEqual( - indexOfTest, - -1, - 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', - ); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.'; + } + } + if (data.result === undefined) { + failureOccurred = true; + failureMsg = 'Expected results to be present'; } - assert.ok(data.result, 'Expected results to be present'); // make sure the testID is found in the results const indexOfTest = JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'); - assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected testId to be present'; + } } catch (err) { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; diff --git a/src/test/testing/common/testingPayloadsEot.test.ts b/src/test/testing/common/testingPayloadsEot.test.ts index a30b1efe288c..2b8b9c0667df 100644 --- a/src/test/testing/common/testingPayloadsEot.test.ts +++ b/src/test/testing/common/testingPayloadsEot.test.ts @@ -165,13 +165,20 @@ suite('EOT tests', () => { mockProc.emit('close', 0, null); client.end(); }); - + let errorBool = false; + let errorMessage = ''; resultResolver = new PythonResultResolver(testController, PYTEST_PROVIDER, workspaceUri); resultResolver._resolveExecution = async (payload, _token?) => { // the payloads that get to the _resolveExecution are all data and should be successful. actualCollectedResult = actualCollectedResult + JSON.stringify(payload.result); - assert.strictEqual(payload.status, 'success', "Expected status to be 'success'"); - assert.ok(payload.result, 'Expected results to be present'); + if (payload.status !== 'success') { + errorBool = true; + errorMessage = "Expected status to be 'success'"; + } + if (!payload.result) { + errorBool = true; + errorMessage = 'Expected results to be present'; + } return Promise.resolve(); }; @@ -208,6 +215,7 @@ suite('EOT tests', () => { actualCollectedResult, "Expected collected result to match 'data'", ); + assert.strictEqual(errorBool, false, errorMessage); }); }); }); diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 742492b33ba8..62f5b8327219 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -117,13 +117,15 @@ suite('Python Test Server, DataWithPayloadChunks', () => { const dataWithPayloadChunks = testCaseDataObj; await server.serverReady(); - + let errorOccur = false; + let errorMessage = ''; server.onRunDataReceived(({ data }) => { try { const resultData = JSON.parse(data).result; eventData = eventData + JSON.stringify(resultData); } catch (e) { - assert(false, 'Error parsing data'); + errorOccur = true; + errorMessage = 'Error parsing data'; } deferred.resolve(); }); @@ -143,6 +145,7 @@ suite('Python Test Server, DataWithPayloadChunks', () => { await deferred.promise; const expectedResult = dataWithPayloadChunks.data; assert.deepStrictEqual(eventData, expectedResult); + assert.deepStrictEqual(errorOccur, false, errorMessage); }); }); }); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index 4d4a8d0ebee4..e2903d353bbf 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -31,12 +31,16 @@ suite('Unittest test execution adapter', () => { test('runTests should send the run command to the test server', async () => { let options: TestCommandOptions | undefined; - + let errorBool = false; + let errorMessage = ''; const stubTestServer = ({ sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { delete opt.outChannel; options = opt; - assert(runTestIdPort !== undefined); + if (runTestIdPort === undefined) { + errorBool = true; + errorMessage = 'runTestIdPort is undefined'; + } return Promise.resolve(); }, onRunDataReceived: () => { @@ -60,6 +64,7 @@ suite('Unittest test execution adapter', () => { testIds, }; assert.deepStrictEqual(options, expectedOptions); + assert.equal(errorBool, false, errorMessage); }); }); test('runTests should respect settings.testing.cwd when present', async () => { @@ -69,12 +74,16 @@ suite('Unittest test execution adapter', () => { }), } as unknown) as IConfigurationService; let options: TestCommandOptions | undefined; - + let errorBool = false; + let errorMessage = ''; const stubTestServer = ({ sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { delete opt.outChannel; options = opt; - assert(runTestIdPort !== undefined); + if (runTestIdPort === undefined) { + errorBool = true; + errorMessage = 'runTestIdPort is undefined'; + } return Promise.resolve(); }, onRunDataReceived: () => { @@ -99,6 +108,7 @@ suite('Unittest test execution adapter', () => { testIds, }; assert.deepStrictEqual(options, expectedOptions); + assert.equal(errorBool, false, errorMessage); }); }); }); From e95ea9a5e5cdb9734fb6c85b4569a6f1b37b382b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 17 Oct 2023 09:32:17 +1100 Subject: [PATCH 336/336] Deprecate the log directory setting --- package.json | 4 +++- package.nls.json | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 20401cc43762..4d97fe1363b2 100644 --- a/package.json +++ b/package.json @@ -1098,7 +1098,9 @@ "default": "", "description": "%python.tensorBoard.logDirectory.description%", "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.tensorBoard.logDirectory.markdownDeprecationMessage%", + "deprecationMessage": "%python.tensorBoard.logDirectory.deprecationMessage%" }, "python.terminal.activateEnvInCurrentTerminal": { "default": false, diff --git a/package.nls.json b/package.nls.json index f843399e09c5..dd1b9af2a3bf 100644 --- a/package.nls.json +++ b/package.nls.json @@ -181,6 +181,8 @@ "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", + "python.tensorBoard.logDirectory.markdownDeprecationMessage": "Tensorboard support has been moved to the extension [Tensorboard extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard). Instead use the setting `tensorBoard.logDirectory`.", + "python.tensorBoard.logDirectory.deprecationMessage": "Tensorboard support has been moved to the extension Tensorboard extension. Instead use the setting `tensorBoard.logDirectory`.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.", "python.terminal.executeInFileDir.description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.",
Release notes

Sourced from brettcannon/check-for-changed-files's releases.

v1.1.1

What's Changed

Full Changelog: https://github.com/brettcannon/check-for-changed-files/compare/v1...v1.1.1